1
0
forked from lug/matterbridge

Compare commits

...

42 Commits

Author SHA1 Message Date
Wim
222cccf388 Release v1.8.0 2018-02-21 20:42:26 +01:00
Wim
bab308508e Fix the UseInsecureURL text (telegram). Closes #184 2018-02-21 13:30:38 +01:00
Wim
dedb83c867 Add ssh-chat to README 2018-02-21 01:42:43 +01:00
Wim
723a90cdd6 Exclude gofmt test from travis for now 2018-02-21 01:20:38 +01:00
Wim
67d2398fa8 Make matterclient work with prefixed log 2018-02-21 01:11:41 +01:00
Wim
5f3b6ec007 Disable echo banner and output (api) 2018-02-21 00:49:10 +01:00
Wim
55ab0c12f1 Update vendor labstack/echo 2018-02-21 00:48:10 +01:00
Wim
d1227b5fc9 Use prefixed-formatter for better logging 2018-02-21 00:20:25 +01:00
Wim
6ea368c383 Move Sirupsen => sirupsen 2018-02-20 23:41:09 +01:00
Wim
e92b6de09f Add more debug 2018-02-20 23:36:29 +01:00
Wim
e622587db4 Add label support in RemoteNickFormat 2018-02-20 18:57:46 +01:00
Wim
f2efc06d1f Give api access to whole config.Message (and events). Closes #374 2018-02-20 18:36:44 +01:00
Wim
a2b94452db Add more debug (telegram) 2018-02-20 17:51:23 +01:00
Wim
4c506f7cc3 Use MediaServerDownload instead of MediaServerUpload for avatars 2018-02-20 17:15:54 +01:00
Wim
7886f05e88 Download (and upload) avatar images from mattermost and telegram when mediaserver is configured. Closes #362
An extra avatarMap (cache) is created for mattermost and telegram.
If MediaServerUpload is configured, the avatar images of users are downloaded the first time a
user sends a message.
If this download succeeds a message with EVENT_AVATAR_DOWNLOAD is sent to the originating protocol.
This message also contains a SHA field (in msg.Extra["file"]), if this is not empty, the sha will
be added to the avatarMap. (so we now have a userid-sha cache)

Next time this user sends a message, the MediaServerUpload/sha/userid.png URL will be used as the
avatar field.
2018-02-20 01:15:25 +01:00
Wim
f58be0d1c1 Add SHA to FileInfo 2018-02-15 23:18:58 +01:00
Wim
1152394bc1 Update issue template 2018-02-15 22:35:29 +01:00
Wim
a082b5a590 Remove unused code 2018-02-15 00:07:25 +01:00
Wim
bae9484df2 Use discordgo ContentWithMoreMentionsReplace (discord) 2018-02-14 23:05:50 +01:00
Wim
6f78485878 Fix role replace 2018-02-14 23:05:16 +01:00
Wim
fd0fe3390b Update vendor bwmarrin/discordgo 2018-02-14 22:22:35 +01:00
Wim
2522158127 Add avator to fileinfo 2018-02-14 22:20:27 +01:00
Wim
8be107cecc Fix mattermost API change 2018-02-09 00:11:20 +01:00
Wim
5aab158c0b Update vendor (github.com/mattermost) 2018-02-09 00:11:04 +01:00
tsudoko
1d33e60e36 Truncate messages sent to IRC based on byte count (#368)
* Truncate messages sent to IRC based on byte count

* Avoid unnecessary string allocations
2018-02-08 23:28:33 +01:00
Wim
83c28cb857 Check for a valid WebhookURL (discord). Closes #367 2018-02-07 14:57:38 +01:00
Wim
df5bce27b0 Fix panic on nil messages (telegram). Closes #366 2018-02-07 14:28:48 +01:00
Wim
2b15739b48 Remove double close 2018-02-07 00:05:10 +01:00
Wim
3480c88e90 Do not close body on err. Closes #364 2018-02-07 00:04:02 +01:00
Wim
432cd0f99d Add more parsemode debug (telegram) 2018-02-04 17:55:20 +01:00
Wim
e8b3e9b22d Update readme 2018-02-04 16:07:37 +01:00
Wim
d4a47671ea Add markdown support (telegram). #355 2018-02-03 23:31:21 +01:00
Wim
0bcd1e62f3 Add channel_purpose to ShowTopicChange. Ignore (un)pinned_item (slack). #353 2018-02-03 01:15:57 +01:00
Wim
80822b7fff Send chat notification if media is too big to be re-uploaded to MediaServer. See #359 2018-02-03 01:11:11 +01:00
Wim
78f1011f52 Add support for file comments (slack). Closes #346 2018-02-02 23:16:10 +01:00
Wim
67f6257617 Add ShowTopicChange option. Allow/disable topic change messages (currently only from slack). Closes #353 2018-02-02 21:08:13 +01:00
Wim
169c614489 Download files and reupload to supported bridges (mattermost). Closes #357 2018-02-02 20:23:55 +01:00
ValdikSS
da908c438a Add space between colon and URL for uploaded media (#360) 2018-02-01 17:46:10 +01:00
Wim
9c9c4bf1f9 Fix build 2018-02-01 01:01:25 +01:00
Wim
7764493298 Add comment to file upload from telegram. Show comments on all bridges. Closes #358 2018-02-01 00:41:09 +01:00
Wim
64a20ee61b Add URL to message in webhook if available (mattermost). See #356 2018-01-31 17:35:13 +01:00
Wim
62d1af8c37 Bump version 2018-01-29 12:41:35 +01:00
359 changed files with 39052 additions and 6727 deletions

View File

@@ -1,22 +1,36 @@
If you have a configuration problem, please first try to create a basic configuration following the instructions on [the wiki](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) before filing an issue. <!-- This is a bug report template. By following the instructions below and
filling out the sections with your information, you will help the us to get all
the necessary data to fix your issue.
Please answer the following questions. You can also preview your report before submitting it.
### Which version of matterbridge are you using? Text between <!-- and --> marks will be invisible in the report.
run ```matterbridge -version``` -->
### If you're having problems with mattermost please specify mattermost version. <!-- If you have a configuration problem, please first try to create a basic configuration following the instructions on [the wiki](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) before filing an issue. -->
### Environment
<!-- run `matterbridge -version` -->
<!-- If you're having problems with mattermost also specify the mattermost version. -->
Version:
<!-- What operating system are you using ? (be as specific as possible) -->
Operating system:
<!-- If you compiled matterbridge yourself:
* Specify the output of `go version`
* Specify the output of `git rev-parse HEAD` -->
### Please describe the expected behavior. ### Please describe the expected behavior.
### Please describe the actual behavior. ### Please describe the actual behavior.
#### Use logs from running ```matterbridge -debug``` if possible. <!-- Use logs from running `matterbridge -debug` if possible. -->
### Any steps to reproduce the behavior? ### Any steps to reproduce the behavior?
### Please add your configuration file ### Please add your configuration file
#### (be sure to exclude or anonymize private data (tokens/passwords)) <!-- (be sure to exclude or anonymize private data (tokens/passwords)) -->

View File

@@ -34,7 +34,7 @@ before_script:
# flunk the build and immediately stop. It's sorta like having # flunk the build and immediately stop. It's sorta like having
# set -e enabled in bash. # set -e enabled in bash.
script: script:
- test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt #- test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt
- go test -v -race $PKGS # Run all the tests with the race detector enabled - go test -v -race $PKGS # Run all the tests with the race detector enabled
- go vet $PKGS # go vet is the official Go static analyzer - go vet $PKGS # go vet is the official Go static analyzer
- megacheck $PKGS # "go vet on steroids" + linter - megacheck $PKGS # "go vet on steroids" + linter

View File

@@ -7,12 +7,12 @@ Click on one of the badges below to join the chat
![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif) ![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif)
Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix and Steam. Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix, Steam and ssh-chat
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)
# Table of Contents # Table of Contents
* [Features](#features) * [Features](https://github.com/42wim/matterbridge/wiki/Features)
* [Requirements](#requirements) * [Requirements](#requirements)
* [Screenshots](https://github.com/42wim/matterbridge/wiki/) * [Screenshots](https://github.com/42wim/matterbridge/wiki/)
* [Installing](#installing) * [Installing](#installing)
@@ -28,13 +28,13 @@ Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterL
* [Thanks](#thanks) * [Thanks](#thanks)
# Features # Features
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat (via xmpp), Matrix and Steam. * [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
Pick and mix. * [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
* Support private groups on your mattermost/slack. * [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
* Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts. * [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
* The bridge is now a gateway which has support multiple in and out bridges. (and supports multiple gateways). * [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
* Edits and delete messages across bridges that support it (mattermost,slack,discord,gitter,telegram) * [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
* REST API to read/post messages to bridges (WIP). * [API](https://github.com/42wim/matterbridge/wiki/Features#api)
## API ## API
The API is very basic at the moment and rather undocumented. The API is very basic at the moment and rather undocumented.
@@ -58,13 +58,14 @@ Accounts to one of the supported bridges
* [Matrix](https://matrix.org) * [Matrix](https://matrix.org)
* [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)
# Screenshots # Screenshots
See https://github.com/42wim/matterbridge/wiki See https://github.com/42wim/matterbridge/wiki
# Installing # Installing
## Binaries ## Binaries
* Latest stable release [v1.7.1](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v1.8.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

View File

@@ -3,9 +3,9 @@ package api
import ( import (
"encoding/json" "encoding/json"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/labstack/echo/middleware" "github.com/labstack/echo/middleware"
log "github.com/sirupsen/logrus"
"github.com/zfjagann/golang-ring" "github.com/zfjagann/golang-ring"
"net/http" "net/http"
"sync" "sync"
@@ -30,12 +30,14 @@ var flog *log.Entry
var protocol = "api" var protocol = "api"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Api { func New(cfg *config.BridgeConfig) *Api {
b := &Api{BridgeConfig: cfg} b := &Api{BridgeConfig: cfg}
e := echo.New() e := echo.New()
e.HideBanner = true
e.HidePort = true
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
b.Messages.SetCapacity(b.Config.Buffer) b.Messages.SetCapacity(b.Config.Buffer)
if b.Config.Token != "" { if b.Config.Token != "" {
@@ -47,6 +49,10 @@ func New(cfg *config.BridgeConfig) *Api {
e.GET("/api/stream", b.handleStream) e.GET("/api/stream", b.handleStream)
e.POST("/api/message", b.handlePostMessage) e.POST("/api/message", b.handlePostMessage)
go func() { go func() {
if b.Config.BindAddress == "" {
flog.Fatalf("No BindAddress configured.")
}
flog.Infof("Listening on %s", b.Config.BindAddress)
flog.Fatal(e.Start(b.Config.BindAddress)) flog.Fatal(e.Start(b.Config.BindAddress))
}() }()
return b return b
@@ -76,21 +82,18 @@ func (b *Api) Send(msg config.Message) (string, error) {
} }
func (b *Api) handlePostMessage(c echo.Context) error { func (b *Api) handlePostMessage(c echo.Context) error {
message := &ApiMessage{} message := config.Message{}
if err := c.Bind(message); err != nil { if err := c.Bind(&message); err != nil {
return err return err
} }
// these values are fixed
message.Channel = "api"
message.Protocol = "api"
message.Account = b.Account
message.ID = ""
message.Timestamp = time.Now()
flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api") flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
b.Remote <- config.Message{ b.Remote <- message
Text: message.Text,
Username: message.Username,
UserID: message.UserID,
Channel: "api",
Avatar: message.Avatar,
Account: b.Account,
Gateway: message.Gateway,
Protocol: "api",
}
return c.JSON(http.StatusOK, message) return c.JSON(http.StatusOK, message)
} }

View File

@@ -14,7 +14,7 @@ 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"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
"strings" "strings"
) )
@@ -36,6 +36,12 @@ type Bridge struct {
Joined map[string]bool Joined map[string]bool
} }
var flog *log.Entry
func init() {
flog = log.WithFields(log.Fields{"prefix": "bridge"})
}
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge { func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge {
b := new(Bridge) b := new(Bridge)
b.Channels = make(map[string]config.ChannelInfo) b.Channels = make(map[string]config.ChannelInfo)
@@ -100,7 +106,7 @@ func (b *Bridge) JoinChannels() error {
func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error { func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
for ID, channel := range channels { for ID, channel := range channels {
if !exists[ID] { if !exists[ID] {
log.Infof("%s: joining %s (%s)", b.Account, channel.Name, ID) flog.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID)
err := b.JoinChannel(channel) err := b.JoinChannel(channel)
if err != nil { if err != nil {
return err return err

View File

@@ -11,7 +11,10 @@ import (
const ( const (
EVENT_JOIN_LEAVE = "join_leave" EVENT_JOIN_LEAVE = "join_leave"
EVENT_TOPIC_CHANGE = "topic_change"
EVENT_FAILURE = "failure" EVENT_FAILURE = "failure"
EVENT_FILE_FAILURE_SIZE = "file_failure_size"
EVENT_AVATAR_DOWNLOAD = "avatar_download"
EVENT_REJOIN_CHANNELS = "rejoin_channels" EVENT_REJOIN_CHANNELS = "rejoin_channels"
EVENT_USER_ACTION = "user_action" EVENT_USER_ACTION = "user_action"
EVENT_MSG_DELETE = "msg_delete" EVENT_MSG_DELETE = "msg_delete"
@@ -37,6 +40,9 @@ type FileInfo struct {
Data *[]byte Data *[]byte
Comment string Comment string
URL string URL string
Size int64
Avatar bool
SHA string
} }
type ChannelInfo struct { type ChannelInfo struct {
@@ -60,6 +66,7 @@ type Protocol struct {
IgnoreNicks string // all protocols IgnoreNicks string // all protocols
IgnoreMessages string // all protocols IgnoreMessages string // all protocols
Jid string // xmpp Jid string // xmpp
Label string // all protocols
Login string // mattermost, matrix Login string // mattermost, matrix
MediaDownloadSize int // all protocols MediaDownloadSize int // all protocols
MediaServerDownload string MediaServerDownload string
@@ -88,6 +95,7 @@ type Protocol struct {
RemoteNickFormat string // all protocols RemoteNickFormat string // all protocols
Server string // IRC,mattermost,XMPP,discord Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols ShowJoinPart bool // all protocols
ShowTopicChange bool // slack
ShowEmbeds bool // discord ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost SkipTLSVerify bool // IRC, mattermost
StripNick bool // all protocols StripNick bool // all protocols

View File

@@ -3,8 +3,9 @@ package bdiscord
import ( import (
"bytes" "bytes"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
log "github.com/sirupsen/logrus"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@@ -28,7 +29,7 @@ var flog *log.Entry
var protocol = "discord" var protocol = "discord"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *bdiscord { func New(cfg *config.BridgeConfig) *bdiscord {
@@ -139,6 +140,9 @@ func (b *bdiscord) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text)
}
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
var err error var err error
@@ -198,6 +202,7 @@ func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat
} }
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
var err error
// not relay our own messages // not relay our own messages
if m.Author.Username == b.Nick { if m.Author.Username == b.Nick {
return return
@@ -216,13 +221,14 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
var text string var text string
if m.Content != "" { if m.Content != "" {
flog.Debugf("Receiving message %#v", m.Message) flog.Debugf("Receiving message %#v", m.Message)
if len(m.MentionRoles) > 0 {
m.Message.Content = b.replaceRoleMentions(m.Message.Content)
}
m.Message.Content = b.stripCustomoji(m.Message.Content) m.Message.Content = b.stripCustomoji(m.Message.Content)
m.Message.Content = b.replaceChannelMentions(m.Message.Content) m.Message.Content = b.replaceChannelMentions(m.Message.Content)
text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil {
flog.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
text = m.ContentWithMentionsReplaced() text = m.ContentWithMentionsReplaced()
} }
}
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg",
UserID: m.Author.ID, ID: m.ID} UserID: m.Author.ID, ID: m.ID}
@@ -318,18 +324,6 @@ func (b *bdiscord) getChannelName(id string) string {
return "" return ""
} }
func (b *bdiscord) replaceRoleMentions(text string) string {
roles, err := b.c.GuildRoles(b.guildID)
if err != nil {
flog.Debugf("%#v", string(err.(*discordgo.RESTError).ResponseBody))
return text
}
for _, role := range roles {
text = strings.Replace(text, "<@&"+role.ID+">", "@"+role.Name, -1)
}
return text
}
func (b *bdiscord) replaceChannelMentions(text string) string { func (b *bdiscord) replaceChannelMentions(text string) string {
var err error var err error
re := regexp.MustCompile("<#[0-9]+>") re := regexp.MustCompile("<#[0-9]+>")
@@ -365,6 +359,9 @@ func (b *bdiscord) stripCustomoji(text string) string {
// splitURL splits a webhookURL and returns the id and token // splitURL splits a webhookURL and returns the id and token
func (b *bdiscord) splitURL(url string) (string, string) { func (b *bdiscord) splitURL(url string) (string, string) {
webhookURLSplit := strings.Split(url, "/") webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != 7 {
log.Fatalf("%s is no correct discord WebhookURL", url)
}
return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1] return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1]
} }

View File

@@ -4,7 +4,8 @@ import (
"fmt" "fmt"
"github.com/42wim/go-gitter" "github.com/42wim/go-gitter"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/sirupsen/logrus"
"strings" "strings"
) )
@@ -20,7 +21,7 @@ var flog *log.Entry
var protocol = "gitter" var protocol = "gitter"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Bgitter { func New(cfg *config.BridgeConfig) *Bgitter {
@@ -121,9 +122,15 @@ func (b *Bgitter) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
}
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" { if fi.URL != "" {
msg.Text = fi.URL msg.Text = fi.URL
} }

View File

@@ -2,6 +2,8 @@ package helper
import ( import (
"bytes" "bytes"
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"io" "io"
"net/http" "net/http"
"time" "time"
@@ -18,12 +20,11 @@ func DownloadFile(url string) (*[]byte, error) {
} }
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
resp.Body.Close()
return nil, err return nil, err
} }
defer resp.Body.Close()
io.Copy(&buf, resp.Body) io.Copy(&buf, resp.Body)
data := buf.Bytes() data := buf.Bytes()
resp.Body.Close()
return &data, nil return &data, nil
} }
@@ -38,3 +39,25 @@ func SplitStringLength(input string, length int) string {
} }
return str return str
} }
// handle all the stuff we put into extra
func HandleExtra(msg *config.Message, general *config.Protocol) []config.Message {
extra := msg.Extra
rmsg := []config.Message{}
if len(extra[config.EVENT_FILE_FAILURE_SIZE]) > 0 {
for _, f := range extra[config.EVENT_FILE_FAILURE_SIZE] {
fi := f.(config.FileInfo)
text := fmt.Sprintf("file %s too big to download (%#v > allowed size: %#v)", fi.Name, fi.Size, general.MediaDownloadSize)
rmsg = append(rmsg, config.Message{Text: text, Username: "<system> ", Channel: msg.Channel})
}
return rmsg
}
return rmsg
}
func GetAvatar(av map[string]string, userid string, general *config.Protocol) string {
if sha, ok := av[userid]; ok {
return general.MediaServerDownload + "/" + sha + "/" + userid + ".png"
}
return ""
}

View File

@@ -6,11 +6,11 @@ import (
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
"github.com/lrstanley/girc" "github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset" "github.com/paulrosania/go-charset/charset"
_ "github.com/paulrosania/go-charset/data" _ "github.com/paulrosania/go-charset/data"
"github.com/saintfish/chardet" "github.com/saintfish/chardet"
log "github.com/sirupsen/logrus"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@@ -19,6 +19,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8"
) )
type Birc struct { type Birc struct {
@@ -36,7 +37,7 @@ var flog *log.Entry
var protocol = "irc" var protocol = "irc"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Birc { func New(cfg *config.BridgeConfig) *Birc {
@@ -177,9 +178,15 @@ func (b *Birc) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.Local <- rmsg
}
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" { if fi.URL != "" {
msg.Text = fi.URL msg.Text = fi.URL
} }
@@ -194,9 +201,12 @@ func (b *Birc) Send(msg config.Message) (string, error) {
msg.Text = helper.SplitStringLength(msg.Text, b.Config.MessageLength) msg.Text = helper.SplitStringLength(msg.Text, b.Config.MessageLength)
} }
for _, text := range strings.Split(msg.Text, "\n") { for _, text := range strings.Split(msg.Text, "\n") {
input := []rune(text)
if len(text) > b.Config.MessageLength { if len(text) > b.Config.MessageLength {
text = string(input[:b.Config.MessageLength]) + " <message clipped>" text = text[:b.Config.MessageLength-len(" <message clipped>")]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-size]
}
text += " <message clipped>"
} }
if len(b.Local) < b.Config.MessageQueue { if len(b.Local) < b.Config.MessageQueue {
if len(b.Local) == b.Config.MessageQueue-1 { if len(b.Local) == b.Config.MessageQueue-1 {

View File

@@ -9,7 +9,7 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
matrix "github.com/matterbridge/gomatrix" matrix "github.com/matterbridge/gomatrix"
) )
@@ -25,7 +25,7 @@ var flog *log.Entry
var protocol = "matrix" var protocol = "matrix"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Bmatrix { func New(cfg *config.BridgeConfig) *Bmatrix {
@@ -98,6 +98,9 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.mc.SendText(channel, rmsg.Username+rmsg.Text)
}
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
@@ -107,6 +110,12 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
mtype := mime.TypeByExtension("." + sp[len(sp)-1]) mtype := mime.TypeByExtension("." + sp[len(sp)-1])
if strings.Contains(mtype, "image") || if strings.Contains(mtype, "image") ||
strings.Contains(mtype, "video") { strings.Contains(mtype, "video") {
if fi.Comment != "" {
_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
if err != nil {
flog.Errorf("file comment failed: %#v", err)
}
}
flog.Debugf("uploading file: %s %s", fi.Name, mtype) flog.Debugf("uploading file: %s %s", fi.Name, mtype)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
if err != nil { if err != nil {
@@ -228,6 +237,10 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url)) flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
rmsg.Extra["file"] = append(rmsg.Extra["file"], config.FileInfo{Name: name, Data: data}) rmsg.Extra["file"] = append(rmsg.Extra["file"], config.FileInfo{Name: name, Data: data})
} }
} else {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, b.General.MediaDownloadSize)
rmsg.Event = config.EVENT_FILE_FAILURE_SIZE
rmsg.Extra[rmsg.Event] = append(rmsg.Extra[rmsg.Event], config.FileInfo{Name: name, Size: int64(size)})
} }
rmsg.Text = "" rmsg.Text = ""
} }

View File

@@ -4,9 +4,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient" "github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
"strings" "strings"
) )
@@ -34,17 +35,18 @@ type Bmattermost struct {
MMapi MMapi
TeamId string TeamId string
*config.BridgeConfig *config.BridgeConfig
avatarMap map[string]string
} }
var flog *log.Entry var flog *log.Entry
var protocol = "mattermost" var protocol = "mattermost"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Bmattermost { func New(cfg *config.BridgeConfig) *Bmattermost {
b := &Bmattermost{BridgeConfig: cfg} b := &Bmattermost{BridgeConfig: cfg, avatarMap: make(map[string]string)}
b.mmMap = make(map[string]string) b.mmMap = make(map[string]string)
return b return b
} }
@@ -148,17 +150,46 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
message := msg.Text message := msg.Text
channel := msg.Channel channel := msg.Channel
// map the file SHA to our user (caches the avatar)
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server,
so we can now cache the sha */
if fi.SHA != "" {
flog.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return "", nil
}
if b.Config.PrefixMessagesWithNick { if b.Config.PrefixMessagesWithNick {
message = nick + message message = nick + message
} }
if b.Config.WebhookURL != "" { if b.Config.WebhookURL != "" {
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: channel, UserName: rmsg.Username,
Text: rmsg.Text, Props: make(map[string]interface{})}
matterMessage.Props["matterbridge"] = true
b.mh.Send(matterMessage)
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
message += fi.URL
}
}
}
}
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.IconURL = msg.Avatar matterMessage.IconURL = msg.Avatar
matterMessage.Channel = channel matterMessage.Channel = channel
matterMessage.UserName = nick matterMessage.UserName = nick
matterMessage.Type = "" matterMessage.Type = ""
matterMessage.Text = message matterMessage.Text = message
matterMessage.Text = message
matterMessage.Props = make(map[string]interface{}) matterMessage.Props = make(map[string]interface{})
matterMessage.Props["matterbridge"] = true matterMessage.Props["matterbridge"] = true
err := b.mh.Send(matterMessage) err := b.mh.Send(matterMessage)
@@ -175,6 +206,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
return msg.ID, b.mc.DeleteMessage(msg.ID) return msg.ID, b.mc.DeleteMessage(msg.ID)
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), rmsg.Username+rmsg.Text)
}
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
var err error var err error
var res, id string var res, id string
@@ -214,7 +248,8 @@ func (b *Bmattermost) handleMatter() {
go b.handleMatterClient(mchan) go b.handleMatterClient(mchan)
} }
for message := range mchan { for message := range mchan {
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event, Extra: message.Extra} avatar := helper.GetAvatar(b.avatarMap, message.UserID, b.General)
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event, Extra: message.Extra, Avatar: avatar}
text, ok := b.replaceAction(message.Text) text, ok := b.replaceAction(message.Text)
if ok { if ok {
rmsg.Event = config.EVENT_USER_ACTION rmsg.Event = config.EVENT_USER_ACTION
@@ -240,6 +275,11 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
continue continue
} }
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" {
b.handleDownloadAvatar(message.UserID, message.Channel)
}
m := &MMMessage{Extra: make(map[string][]interface{})} m := &MMMessage{Extra: make(map[string][]interface{})}
props := message.Post.Props props := message.Post.Props
@@ -276,8 +316,26 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
m.Event = config.EVENT_MSG_DELETE m.Event = config.EVENT_MSG_DELETE
} }
if len(message.Post.FileIds) > 0 { if len(message.Post.FileIds) > 0 {
for _, link := range b.mc.GetFileLinks(message.Post.FileIds) { for _, id := range message.Post.FileIds {
m.Text = m.Text + "\n" + link url, _ := b.mc.Client.GetFileLink(id)
finfo, resp := b.mc.Client.GetFileInfo(id)
if resp.Error != nil {
continue
}
flog.Debugf("trying to download %#v fileid %#v with size %#v", finfo.Name, finfo.Id, finfo.Size)
if int(finfo.Size) > b.General.MediaDownloadSize {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", finfo.Name, finfo.Size, b.General.MediaDownloadSize)
m.Event = config.EVENT_FILE_FAILURE_SIZE
m.Extra[m.Event] = append(m.Extra[m.Event], config.FileInfo{Name: finfo.Name, Comment: message.Text, Size: int64(finfo.Size)})
continue
}
data, resp := b.mc.Client.DownloadFile(id, true)
if resp.Error != nil {
flog.Errorf("download %s failed %#v", finfo.Name, resp.Error)
continue
}
flog.Debugf("download OK %#v %#v", finfo.Name, len(data))
m.Extra["file"] = append(m.Extra["file"], config.FileInfo{Name: finfo.Name, Data: &data, URL: url, Comment: message.Text})
} }
} }
mchan <- m mchan <- m
@@ -306,6 +364,9 @@ func (b *Bmattermost) apiLogin() error {
b.mc = matterclient.New(b.Config.Login, password, b.mc = matterclient.New(b.Config.Login, password,
b.Config.Team, b.Config.Server) b.Config.Team, b.Config.Server)
if b.General.Debug {
b.mc.SetLogLevel("debug")
}
b.mc.SkipTLSVerify = b.Config.SkipTLSVerify b.mc.SkipTLSVerify = b.Config.SkipTLSVerify
b.mc.NoTLS = b.Config.NoTLS b.mc.NoTLS = b.Config.NoTLS
flog.Infof("Connecting %s (team: %s) on %s", b.Config.Login, b.Config.Team, b.Config.Server) flog.Infof("Connecting %s (team: %s) on %s", b.Config.Login, b.Config.Team, b.Config.Server)
@@ -326,3 +387,27 @@ func (b *Bmattermost) replaceAction(text string) (string, bool) {
} }
return text, false return text, false
} }
// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
var name string
msg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: userid, Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})}
if _, ok := b.avatarMap[userid]; !ok {
data, resp := b.mc.Client.GetProfileImage(userid, "")
if resp.Error != nil {
flog.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
}
if len(data) <= b.General.MediaDownloadSize {
name = userid + ".png"
flog.Debugf("download OK %#v %#v", name, len(data))
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: &data, Avatar: true})
flog.Debugf("Sending avatar download message from %#v on %s to gateway", userid, b.Account)
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
} else {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, len(data), b.General.MediaDownloadSize)
}
}
}

View File

@@ -2,9 +2,10 @@ package brocketchat
import ( import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook" "github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type MMhook struct { type MMhook struct {
@@ -21,7 +22,7 @@ var flog *log.Entry
var protocol = "rocketchat" var protocol = "rocketchat"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Brocketchat { func New(cfg *config.BridgeConfig) *Brocketchat {
@@ -57,6 +58,22 @@ func (b *Brocketchat) Send(msg config.Message) (string, error) {
return "", nil return "", nil
} }
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: rmsg.Channel, UserName: rmsg.Username,
Text: rmsg.Text}
b.mh.Send(matterMessage)
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
}
}
}
}
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = msg.Channel matterMessage.Channel = msg.Channel
matterMessage.UserName = msg.Username matterMessage.UserName = msg.Username

View File

@@ -5,8 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/nlopes/slack" "github.com/nlopes/slack"
"html" "html"
"io" "io"
@@ -39,7 +40,7 @@ var flog *log.Entry
var protocol = "slack" var protocol = "slack"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Bslack { func New(cfg *config.BridgeConfig) *Bslack {
@@ -134,6 +135,22 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
message = nick + " " + message message = nick + " " + message
} }
if b.Config.WebhookURL != "" { if b.Config.WebhookURL != "" {
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: channel, UserName: rmsg.Username,
Text: rmsg.Text}
b.mh.Send(matterMessage)
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
message += fi.URL
}
}
}
}
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = channel matterMessage.Channel = channel
matterMessage.UserName = nick matterMessage.UserName = nick
@@ -183,6 +200,9 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.sc.PostMessage(schannel.ID, rmsg.Username+rmsg.Text, np)
}
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
var err error var err error
@@ -284,20 +304,28 @@ func (b *Bslack) handleSlack() {
msg.Event = config.EVENT_MSG_DELETE msg.Event = config.EVENT_MSG_DELETE
msg.ID = "slack " + message.Raw.DeletedTimestamp msg.ID = "slack " + message.Raw.DeletedTimestamp
} }
if message.Raw.SubType == "channel_topic" || message.Raw.SubType == "channel_purpose" {
msg.Event = config.EVENT_TOPIC_CHANGE
}
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
if message.Raw.File != nil { if message.Raw.File != nil {
// limit to 1MB for now // limit to 1MB for now
if message.Raw.File.Size <= b.General.MediaDownloadSize {
comment := "" comment := ""
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
if err != nil {
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
} else {
results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(msg.Text, -1) results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(msg.Text, -1)
if len(results) > 0 { if len(results) > 0 {
comment = results[0][1] comment = results[0][1]
} }
if message.Raw.File.Size > b.General.MediaDownloadSize {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", message.Raw.File.Name, message.Raw.File.Size, b.General.MediaDownloadSize)
msg.Event = config.EVENT_FILE_FAILURE_SIZE
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: message.Raw.File.Name, Comment: comment, Size: int64(message.Raw.File.Size)})
} else {
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
if err != nil {
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
} else {
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data, Comment: comment}) msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data, Comment: comment})
} }
} }
@@ -314,6 +342,9 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
} }
switch ev := msg.Data.(type) { switch ev := msg.Data.(type) {
case *slack.MessageEvent: case *slack.MessageEvent:
if ev.SubType == "pinned_item" || ev.SubType == "unpinned_item" {
continue
}
if len(ev.Attachments) > 0 { if len(ev.Attachments) > 0 {
// skip messages we made ourselves // skip messages we made ourselves
if ev.Attachments[0].CallbackID == "matterbridge" { if ev.Attachments[0].CallbackID == "matterbridge" {
@@ -337,7 +368,7 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
continue continue
} }
m := &MMMessage{} m := &MMMessage{}
if ev.BotID == "" && ev.SubType != "message_deleted" { if ev.BotID == "" && ev.SubType != "message_deleted" && ev.SubType != "file_comment" {
user, err := b.rtm.GetUserInfo(ev.User) user, err := b.rtm.GetUserInfo(ev.User)
if err != nil { if err != nil {
continue continue
@@ -377,6 +408,11 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
m.UserID = bot.ID m.UserID = bot.ID
} }
} }
if ev.SubType == "file_comment" {
m.Username = "system"
}
mchan <- m mchan <- m
case *slack.OutgoingErrorEvent: case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error()) flog.Debugf("%#v", ev.Error())

View File

@@ -3,7 +3,8 @@ package bsshchat
import ( import (
"bufio" "bufio"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/sirupsen/logrus"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
"io" "io"
"strings" "strings"
@@ -19,7 +20,7 @@ var flog *log.Entry
var protocol = "sshchat" var protocol = "sshchat"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Bsshchat { func New(cfg *config.BridgeConfig) *Bsshchat {
@@ -62,9 +63,15 @@ func (b *Bsshchat) Send(msg config.Message) (string, error) {
} }
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n"))
}
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" { if fi.URL != "" {
msg.Text = fi.URL msg.Text = fi.URL
} }

View File

@@ -6,7 +6,7 @@ import (
"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"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
//"io/ioutil" //"io/ioutil"
"strconv" "strconv"
"sync" "sync"
@@ -25,7 +25,7 @@ var flog *log.Entry
var protocol = "steam" var protocol = "steam"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Bsteam { func New(cfg *config.BridgeConfig) *Bsteam {

View File

@@ -7,24 +7,25 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
"github.com/go-telegram-bot-api/telegram-bot-api" "github.com/go-telegram-bot-api/telegram-bot-api"
log "github.com/sirupsen/logrus"
) )
type Btelegram struct { type Btelegram struct {
c *tgbotapi.BotAPI c *tgbotapi.BotAPI
*config.BridgeConfig *config.BridgeConfig
avatarMap map[string]string // keep cache of userid and avatar sha
} }
var flog *log.Entry var flog *log.Entry
var protocol = "telegram" var protocol = "telegram"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Btelegram { func New(cfg *config.BridgeConfig) *Btelegram {
return &Btelegram{BridgeConfig: cfg} return &Btelegram{BridgeConfig: cfg, avatarMap: make(map[string]string)}
} }
func (b *Btelegram) Connect() error { func (b *Btelegram) Connect() error {
@@ -63,6 +64,18 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
return "", err return "", err
} }
// map the file SHA to our user (caches the avatar)
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server,
so we can now cache the sha */
if fi.SHA != "" {
flog.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return "", nil
}
if b.Config.MessageFormat == "HTML" { if b.Config.MessageFormat == "HTML" {
msg.Text = makeHTML(msg.Text) msg.Text = makeHTML(msg.Text)
} }
@@ -87,8 +100,13 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
} }
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text) m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" { if b.Config.MessageFormat == "HTML" {
flog.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML m.ParseMode = tgbotapi.ModeHTML
} }
if b.Config.MessageFormat == "Markdown" {
flog.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
_, err = b.c.Send(m) _, err = b.c.Send(m)
if err != nil { if err != nil {
return "", err return "", err
@@ -97,6 +115,9 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
} }
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.sendMessage(chatid, rmsg.Username+rmsg.Text)
}
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
var c tgbotapi.Chattable var c tgbotapi.Chattable
@@ -126,6 +147,10 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
for update := range updates { for update := range updates {
flog.Debugf("Receiving from telegram: %#v", update.Message) flog.Debugf("Receiving from telegram: %#v", update.Message)
if update.Message == nil {
flog.Error("Getting nil messages, this shouldn't happen.")
continue
}
var message *tgbotapi.Message var message *tgbotapi.Message
username := "" username := ""
channel := "" channel := ""
@@ -161,28 +186,37 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
} }
text = message.Text text = message.Text
channel = strconv.FormatInt(message.Chat.ID, 10) channel = strconv.FormatInt(message.Chat.ID, 10)
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" {
b.handleDownloadAvatar(message.From.ID, channel)
}
} }
if username == "" { if username == "" {
username = "unknown" username = "unknown"
} }
if message.Sticker != nil { if message.Sticker != nil {
b.handleDownload(message.Sticker, &fmsg) b.handleDownload(message.Sticker, message.Caption, &fmsg)
} }
if message.Video != nil { if message.Video != nil {
b.handleDownload(message.Video, &fmsg) b.handleDownload(message.Video, message.Caption, &fmsg)
} }
if message.Photo != nil { if message.Photo != nil {
b.handleDownload(message.Photo, &fmsg) b.handleDownload(message.Photo, message.Caption, &fmsg)
} }
if message.Document != nil { if message.Document != nil {
b.handleDownload(message.Document, &fmsg) b.handleDownload(message.Document, message.Caption, &fmsg)
} }
if message.Voice != nil { if message.Voice != nil {
b.handleDownload(message.Voice, &fmsg) b.handleDownload(message.Voice, message.Caption, &fmsg)
} }
if message.Audio != nil { if message.Audio != nil {
b.handleDownload(message.Audio, &fmsg) b.handleDownload(message.Audio, message.Caption, &fmsg)
}
// If UseInsecureURL is used we'll have a text in fmsg.Text
if fmsg.Text != "" {
text = text + fmsg.Text
} }
if message.ForwardFrom != nil { if message.ForwardFrom != nil {
@@ -223,8 +257,9 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
} }
if text != "" || len(fmsg.Extra) > 0 { if text != "" || len(fmsg.Extra) > 0 {
avatar := helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID), Extra: fmsg.Extra} msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID), Extra: fmsg.Extra, Avatar: avatar}
flog.Debugf("Message is %#v", msg) flog.Debugf("Message is %#v", msg)
b.Remote <- msg b.Remote <- msg
} }
@@ -239,7 +274,40 @@ func (b *Btelegram) getFileDirectURL(id string) string {
return res return res
} }
func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) { // handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
msg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: strconv.Itoa(userid), Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})}
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
if err != nil {
flog.Errorf("Userprofile download failed for %#v %s", userid, err)
}
if len(photos.Photos) > 0 {
photo := photos.Photos[0][0]
url := b.getFileDirectURL(photo.FileID)
name := strconv.Itoa(userid) + ".png"
flog.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
if photo.FileSize <= b.General.MediaDownloadSize {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, Avatar: true})
flog.Debugf("Sending avatar download message from %#v on %s to gateway", userid, b.Account)
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
}
} else {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, photo.FileSize, b.General.MediaDownloadSize)
}
}
}
}
func (b *Btelegram) handleDownload(file interface{}, comment string, msg *config.Message) {
size := 0 size := 0
url := "" url := ""
name := "" name := ""
@@ -295,11 +363,11 @@ func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) {
fileid = v.FileID fileid = v.FileID
} }
if b.Config.UseInsecureURL { if b.Config.UseInsecureURL {
flog.Debugf("Setting message text to :%s", text)
msg.Text = text msg.Text = text
return return
} }
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
// limit to 1MB for now
flog.Debugf("trying to download %#v fileid %#v with size %#v", name, fileid, size) flog.Debugf("trying to download %#v fileid %#v with size %#v", name, fileid, size)
if size <= b.General.MediaDownloadSize { if size <= b.General.MediaDownloadSize {
data, err := helper.DownloadFile(url) data, err := helper.DownloadFile(url)
@@ -307,16 +375,25 @@ func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) {
flog.Errorf("download %s failed %#v", url, err) flog.Errorf("download %s failed %#v", url, err)
} else { } else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url)) flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data}) msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, Comment: comment})
} }
} else {
flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, b.General.MediaDownloadSize)
msg.Event = config.EVENT_FILE_FAILURE_SIZE
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: name, Comment: comment, Size: int64(size)})
} }
} }
func (b *Btelegram) sendMessage(chatid int64, text string) (string, error) { func (b *Btelegram) sendMessage(chatid int64, text string) (string, error) {
m := tgbotapi.NewMessage(chatid, text) m := tgbotapi.NewMessage(chatid, text)
if b.Config.MessageFormat == "HTML" { if b.Config.MessageFormat == "HTML" {
flog.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML m.ParseMode = tgbotapi.ModeHTML
} }
if b.Config.MessageFormat == "Markdown" {
flog.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
res, err := b.c.Send(m) res, err := b.c.Send(m)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -3,7 +3,8 @@ package bxmpp
import ( import (
"crypto/tls" "crypto/tls"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/sirupsen/logrus"
"github.com/jpillora/backoff" "github.com/jpillora/backoff"
"github.com/mattn/go-xmpp" "github.com/mattn/go-xmpp"
@@ -21,7 +22,7 @@ var flog *log.Entry
var protocol = "xmpp" var protocol = "xmpp"
func init() { func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"prefix": protocol})
} }
func New(cfg *config.BridgeConfig) *Bxmpp { func New(cfg *config.BridgeConfig) *Bxmpp {
@@ -81,11 +82,17 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
} }
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: rmsg.Channel + "@" + b.Config.Muc, Text: rmsg.Username + rmsg.Text})
}
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" { if fi.URL != "" {
msg.Text = fi.URL msg.Text += fi.URL
} }
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text}) b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
} }

View File

@@ -1,3 +1,24 @@
# v1.8.0
## New features
* general: Send chat notification if media is too big to be re-uploaded to MediaServer. See #359
* general: Download (and upload) avatar images from mattermost and telegram when mediaserver is configured. Closes #362
* general: Add label support in RemoteNickFormat
* general: Prettier info/debug log output
* mattermost: Download files and reupload to supported bridges (mattermost). Closes #357
* slack: Add ShowTopicChange option. Allow/disable topic change messages (currently only from slack). Closes #353
* slack: Add support for file comments (slack). Closes #346
* telegram: Add comment to file upload from telegram. Show comments on all bridges. Closes #358
* telegram: Add markdown support (telegram). #355
* api: Give api access to whole config.Message (and events). Closes #374
## Bugfix
* discord: Check for a valid WebhookURL (discord). Closes #367
* discord: Fix role mention replace issues
* irc: Truncate messages sent to IRC based on byte count (#368)
* mattermost: Add file download urls also to mattermost webhooks #356
* telegram: Fix panic on nil messages (telegram). Closes #366
* telegram: Fix the UseInsecureURL text (telegram). Closes #184
# v1.7.1 # v1.7.1
## Bugfix ## Bugfix
* telegram: Enable Long Polling for Telegram. Reduces bandwidth consumption. (#350) * telegram: Enable Long Polling for Telegram. Reduces bandwidth consumption. (#350)

View File

@@ -5,7 +5,7 @@ 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"
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"
@@ -34,6 +34,12 @@ type BrMsgID struct {
ChannelID string ChannelID string
} }
var flog *log.Entry
func init() {
flog = log.WithFields(log.Fields{"prefix": "gateway"})
}
func New(cfg config.Gateway, r *Router) *Gateway { func New(cfg config.Gateway, r *Router) *Gateway {
gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message, gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message,
Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config} Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config}
@@ -78,10 +84,10 @@ func (gw *Gateway) reconnectBridge(br *bridge.Bridge) {
br.Disconnect() br.Disconnect()
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
RECONNECT: RECONNECT:
log.Infof("Reconnecting %s", br.Account) flog.Infof("Reconnecting %s", br.Account)
err := br.Connect() err := br.Connect()
if err != nil { if err != nil {
log.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err) flog.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
time.Sleep(time.Second * 60) time.Sleep(time.Second * 60)
goto RECONNECT goto RECONNECT
} }
@@ -145,7 +151,7 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
continue continue
} }
// do samechannelgateway logic // do samechannelgateway flogic
if channel.SameChannel[msg.Gateway] { if channel.SameChannel[msg.Gateway] {
if msg.Channel == channel.Name && msg.Account != dest.Account { if msg.Channel == channel.Name && msg.Account != dest.Account {
channels = append(channels, *channel) channels = append(channels, *channel)
@@ -171,30 +177,50 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
dest.Protocol != "mattermost" && dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" && dest.Protocol != "telegram" &&
dest.Protocol != "matrix" && dest.Protocol != "matrix" &&
dest.Protocol != "xmpp" { dest.Protocol != "xmpp" &&
len(msg.Extra[config.EVENT_FILE_FAILURE_SIZE]) == 0 {
if msg.Text == "" { if msg.Text == "" {
return brMsgIDs return brMsgIDs
} }
} }
} }
// Avatar downloads are only relevant for telegram and mattermost for now
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
if dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" {
return brMsgIDs
}
}
// only relay join/part when configged // only relay join/part when configged
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart { if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
return brMsgIDs return brMsgIDs
} }
if msg.Event == config.EVENT_TOPIC_CHANGE && !gw.Bridges[dest.Account].Config.ShowTopicChange {
return brMsgIDs
}
// broadcast to every out channel (irc QUIT) // broadcast to every out channel (irc QUIT)
if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE { if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE {
log.Debug("empty channel") flog.Debug("empty channel")
return brMsgIDs return brMsgIDs
} }
originchannel := msg.Channel originchannel := msg.Channel
origmsg := msg origmsg := msg
channels := gw.getDestChannel(&msg, *dest) channels := gw.getDestChannel(&msg, *dest)
for _, channel := range channels { for _, channel := range channels {
// do not send to ourself // Only send the avatar download event to ourselves.
if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
if channel.ID != getChannelID(origmsg) {
continue
}
} else {
// do not send to ourself for any other event
if channel.ID == getChannelID(origmsg) { if channel.ID == getChannelID(origmsg) {
continue continue
} }
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name) }
flog.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
msg.Channel = channel.Name msg.Channel = channel.Name
msg.Avatar = gw.modifyAvatar(origmsg, dest) msg.Avatar = gw.modifyAvatar(origmsg, dest)
msg.Username = gw.modifyUsername(origmsg, dest) msg.Username = gw.modifyUsername(origmsg, dest)
@@ -232,15 +258,18 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
} }
if msg.Text == "" { if msg.Text == "" {
// we have an attachment or actual bytes // we have an attachment or actual bytes
if msg.Extra != nil && (msg.Extra["attachments"] != nil || len(msg.Extra["file"]) > 0) { if msg.Extra != nil &&
(msg.Extra["attachments"] != nil ||
len(msg.Extra["file"]) > 0 ||
len(msg.Extra[config.EVENT_FILE_FAILURE_SIZE]) > 0) {
return false return false
} }
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account) flog.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
return true return true
} }
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) { for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) {
if msg.Username == entry { if msg.Username == entry {
log.Debugf("ignoring %s from %s", msg.Username, msg.Account) flog.Debugf("ignoring %s from %s", msg.Username, msg.Account)
return true return true
} }
} }
@@ -249,11 +278,11 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
if entry != "" { if entry != "" {
re, err := regexp.Compile(entry) re, err := regexp.Compile(entry)
if err != nil { if err != nil {
log.Errorf("incorrect regexp %s for %s", entry, msg.Account) flog.Errorf("incorrect regexp %s for %s", entry, msg.Account)
continue continue
} }
if re.MatchString(msg.Text) { if re.MatchString(msg.Text) {
log.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account) flog.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
return true return true
} }
} }
@@ -280,7 +309,7 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
// TODO move compile to bridge init somewhere // TODO move compile to bridge init somewhere
re, err := regexp.Compile(search) re, err := regexp.Compile(search)
if err != nil { if err != nil {
log.Errorf("regexp in %s failed: %s", msg.Account, err) flog.Errorf("regexp in %s failed: %s", msg.Account, err)
break break
} }
msg.Username = re.ReplaceAllString(msg.Username, replace) msg.Username = re.ReplaceAllString(msg.Username, replace)
@@ -300,6 +329,7 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
} }
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1) nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
nick = strings.Replace(nick, "{LABEL}", br.Config.Label, -1)
nick = strings.Replace(nick, "{NICK}", msg.Username, -1) nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
return nick return nick
} }
@@ -327,7 +357,7 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
// TODO move compile to bridge init somewhere // TODO move compile to bridge init somewhere
re, err := regexp.Compile(search) re, err := regexp.Compile(search)
if err != nil { if err != nil {
log.Errorf("regexp in %s failed: %s", msg.Account, err) flog.Errorf("regexp in %s failed: %s", msg.Account, err)
break break
} }
msg.Text = re.ReplaceAllString(msg.Text, replace) msg.Text = re.ReplaceAllString(msg.Text, replace)
@@ -355,14 +385,17 @@ func (gw *Gateway) handleFiles(msg *config.Message) {
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
msg.Extra["file"][i] = extra
req, _ := http.NewRequest("PUT", url, reader) req, _ := http.NewRequest("PUT", url, reader)
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 {
log.Errorf("mediaserver upload failed: %#v", err) flog.Errorf("mediaserver upload failed: %#v", err)
continue
} }
log.Debugf("mediaserver download URL = %s", durl) flog.Debugf("mediaserver download URL = %s", durl)
// we uploaded the file successfully. Add the SHA
extra.SHA = sha1sum
msg.Extra["file"][i] = extra
} }
} }
} }

View File

@@ -5,7 +5,7 @@ import (
"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/gateway/samechannel" "github.com/42wim/matterbridge/gateway/samechannel"
log "github.com/Sirupsen/logrus" //log "github.com/sirupsen/logrus"
// "github.com/davecgh/go-spew/spew" // "github.com/davecgh/go-spew/spew"
"time" "time"
) )
@@ -42,12 +42,13 @@ func NewRouter(cfg *config.Config) (*Router, error) {
func (r *Router) Start() error { func (r *Router) Start() error {
m := make(map[string]*bridge.Bridge) m := make(map[string]*bridge.Bridge)
for _, gw := range r.Gateways { for _, gw := range r.Gateways {
flog.Infof("Parsing gateway %s", gw.Name)
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
m[br.Account] = br m[br.Account] = br
} }
} }
for _, br := range m { for _, br := range m {
log.Infof("Starting bridge: %s ", br.Account) flog.Infof("Starting bridge: %s ", br.Account)
err := br.Connect() err := br.Connect()
if err != nil { if err != nil {
return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err) return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)

View File

@@ -5,22 +5,21 @@ import (
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway" "github.com/42wim/matterbridge/gateway"
log "github.com/Sirupsen/logrus"
"github.com/google/gops/agent" "github.com/google/gops/agent"
log "github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
"os" "os"
"strings" "strings"
) )
var ( var (
version = "1.7.1" version = "1.8.0"
githash string githash string
) )
func init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
}
func main() { func main() {
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: true})
flog := log.WithFields(log.Fields{"prefix": "main"})
flagConfig := flag.String("conf", "matterbridge.toml", "config file") flagConfig := flag.String("conf", "matterbridge.toml", "config file")
flagDebug := flag.Bool("debug", false, "enable debug") flagDebug := flag.Bool("debug", false, "enable debug")
flagVersion := flag.Bool("version", false, "show version") flagVersion := flag.Bool("version", false, "show version")
@@ -35,23 +34,24 @@ func main() {
return return
} }
if *flagDebug || os.Getenv("DEBUG") == "1" { if *flagDebug || os.Getenv("DEBUG") == "1" {
log.Info("Enabling debug") log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
flog.Info("Enabling debug")
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
} }
log.Printf("Running version %s %s", version, githash) flog.Printf("Running version %s %s", version, githash)
if strings.Contains(version, "-dev") { if strings.Contains(version, "-dev") {
log.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.") flog.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
} }
cfg := config.NewConfig(*flagConfig) cfg := config.NewConfig(*flagConfig)
cfg.General.Debug = *flagDebug cfg.General.Debug = *flagDebug
r, err := gateway.NewRouter(cfg) r, err := gateway.NewRouter(cfg)
if err != nil { if err != nil {
log.Fatalf("Starting gateway failed: %s", err) flog.Fatalf("Starting gateway failed: %s", err)
} }
err = r.Start() err = r.Start()
if err != nil { if err != nil {
log.Fatalf("Starting gateway failed: %s", err) flog.Fatalf("Starting gateway failed: %s", err)
} }
log.Printf("Gateway(s) started succesfully. Now relaying messages") flog.Printf("Gateway(s) started succesfully. Now relaying messages")
select {} select {}
} }

View File

@@ -117,16 +117,21 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information #The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -135,6 +140,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#XMPP section #XMPP section
################################################################### ###################################################################
@@ -197,15 +207,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#OPTIONAL (default empty) #OPTIONAL (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -214,6 +229,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#hipchat section #hipchat section
################################################################### ###################################################################
@@ -268,15 +288,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -285,6 +310,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#mattermost section #mattermost section
################################################################### ###################################################################
@@ -399,15 +429,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -416,6 +451,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#Gitter section #Gitter section
#Best to make a dedicated gitter account for the bot. #Best to make a dedicated gitter account for the bot.
@@ -460,15 +500,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -477,6 +522,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#slack section #slack section
################################################################### ###################################################################
@@ -512,6 +562,7 @@ WebhookBindAddress="0.0.0.0:9999"
#Icon that will be showed in slack #Icon that will be showed in slack
#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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL #OPTIONAL
IconURL="https://robohash.org/{NICK}.png?size=48x48" IconURL="https://robohash.org/{NICK}.png?size=48x48"
@@ -568,15 +619,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -585,6 +641,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#discord section #discord section
################################################################### ###################################################################
@@ -653,15 +714,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -670,6 +736,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#telegram section #telegram section
################################################################### ###################################################################
@@ -737,15 +808,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -754,6 +830,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#rocketchat section #rocketchat section
################################################################### ###################################################################
@@ -822,15 +903,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -839,6 +925,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#matrix section #matrix section
################################################################### ###################################################################
@@ -899,15 +990,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -916,6 +1012,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
#steam section #steam section
################################################################### ###################################################################
@@ -970,15 +1071,20 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] 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 #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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Only works hiding/show messages from irc and mattermost bridge for now #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@@ -987,6 +1093,11 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=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
################################################################### ###################################################################
@@ -1008,9 +1119,14 @@ Buffer=1000
#OPTIONAL (no authorization if token is empty) #OPTIONAL (no authorization if token is empty)
Token="mytoken" Token="mytoken"
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="{NICK}" RemoteNickFormat="{NICK}"
@@ -1025,6 +1141,7 @@ RemoteNickFormat="{NICK}"
#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
#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 #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
@@ -1053,7 +1170,7 @@ MediaServerDownload="https://youserver.com/download"
#eg downloading from slack to upload it to mattermost #eg downloading from slack to upload it to mattermost
# #
#It will only download from bridges that don't have public links available, which are for the moment #It will only download from bridges that don't have public links available, which are for the moment
#slack, telegram and matrix #slack, telegram, matrix and mattermost
# #
#Optional (default 1000000 (1 megabyte)) #Optional (default 1000000 (1 megabyte))
MediaDownloadSize=1000000 MediaDownloadSize=1000000

View File

@@ -13,7 +13,8 @@ import (
"sync" "sync"
"time" "time"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/hashicorp/golang-lru" "github.com/hashicorp/golang-lru"
@@ -73,12 +74,16 @@ type MMClient struct {
func New(login, pass, team, server string) *MMClient { func New(login, pass, team, server string) *MMClient {
cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server} cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server}
mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)} mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)}
mmclient.log = log.WithFields(log.Fields{"module": "matterclient"}) log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true})
log.SetFormatter(&log.TextFormatter{FullTimestamp: true}) mmclient.log = log.WithFields(log.Fields{"prefix": "matterclient"})
mmclient.lruCache, _ = lru.New(500) mmclient.lruCache, _ = lru.New(500)
return mmclient return mmclient
} }
func (m *MMClient) SetDebugLog() {
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
}
func (m *MMClient) SetLogLevel(level string) { func (m *MMClient) SetLogLevel(level string) {
l, err := log.ParseLevel(level) l, err := log.ParseLevel(level)
if err != nil { if err != nil {
@@ -585,9 +590,9 @@ func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
func (m *MMClient) UpdateLastViewed(channelId string) { func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId) m.log.Debugf("posting lastview %#v", channelId)
view := &model.ChannelView{ChannelId: channelId} view := &model.ChannelView{ChannelId: channelId}
res, _ := m.Client.ViewChannel(m.User.Id, view) _, resp := m.Client.ViewChannel(m.User.Id, view)
if !res { if resp.Error != nil {
m.log.Errorf("ChannelView update for %s failed", channelId) m.log.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
} }
} }

View File

@@ -1,30 +0,0 @@
package main
import (
"github.com/Sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.TextFormatter) // default
log.Hooks.Add(airbrake.NewHook(123, "xyz", "development"))
}
func main() {
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
}

View File

@@ -1,67 +0,0 @@
package test
import (
"io/ioutil"
"github.com/Sirupsen/logrus"
)
// test.Hook is a hook designed for dealing with logs in test scenarios.
type Hook struct {
Entries []*logrus.Entry
}
// Installs a test hook for the global logger.
func NewGlobal() *Hook {
hook := new(Hook)
logrus.AddHook(hook)
return hook
}
// Installs a test hook for a given local logger.
func NewLocal(logger *logrus.Logger) *Hook {
hook := new(Hook)
logger.Hooks.Add(hook)
return hook
}
// Creates a discarding logger and installs the test hook.
func NewNullLogger() (*logrus.Logger, *Hook) {
logger := logrus.New()
logger.Out = ioutil.Discard
return logger, NewLocal(logger)
}
func (t *Hook) Fire(e *logrus.Entry) error {
t.Entries = append(t.Entries, e)
return nil
}
func (t *Hook) Levels() []logrus.Level {
return logrus.AllLevels
}
// LastEntry returns the last entry that was logged or nil.
func (t *Hook) LastEntry() (l *logrus.Entry) {
if i := len(t.Entries) - 1; i < 0 {
return nil
} else {
return t.Entries[i]
}
}
// Reset removes all Entries from this test hook.
func (t *Hook) Reset() {
t.Entries = make([]*logrus.Entry, 0)
}

View File

@@ -1,10 +0,0 @@
// +build appengine
package logrus
import "io"
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal(f io.Writer) bool {
return true
}

View File

@@ -1,10 +0,0 @@
// +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine
package logrus
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
type Termios syscall.Termios

View File

@@ -1,28 +0,0 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux darwin freebsd openbsd netbsd dragonfly
// +build !appengine
package logrus
import (
"io"
"os"
"syscall"
"unsafe"
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal(f io.Writer) bool {
var termios Termios
switch v := f.(type) {
case *os.File:
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(v.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
default:
return false
}
}

View File

@@ -1,21 +0,0 @@
// +build solaris,!appengine
package logrus
import (
"io"
"os"
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal(f io.Writer) bool {
switch v := f.(type) {
case *os.File:
_, err := unix.IoctlGetTermios(int(v.Fd()), unix.TCGETA)
return err == nil
default:
return false
}
}

View File

@@ -1,33 +0,0 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows,!appengine
package logrus
import (
"io"
"os"
"syscall"
"unsafe"
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var (
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal(f io.Writer) bool {
switch v := f.(type) {
case *os.File:
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(v.Fd()), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
default:
return false
}
}

View File

@@ -21,7 +21,7 @@ import (
) )
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
const VERSION = "0.17.0" const VERSION = "0.18.0"
// ErrMFA will be risen by New when the user has 2FA. // ErrMFA will be risen by New when the user has 2FA.
var ErrMFA = errors.New("account has 2FA enabled") var ErrMFA = errors.New("account has 2FA enabled")
@@ -50,7 +50,7 @@ func New(args ...interface{}) (s *Session, err error) {
// Create an empty Session interface. // Create an empty Session interface.
s = &Session{ s = &Session{
State: NewState(), State: NewState(),
ratelimiter: NewRatelimiter(), Ratelimiter: NewRatelimiter(),
StateEnabled: true, StateEnabled: true,
Compress: true, Compress: true,
ShouldReconnectOnError: true, ShouldReconnectOnError: true,

View File

@@ -71,7 +71,6 @@ var (
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID }
EndpointGuild = func(gID string) string { return EndpointGuilds + gID } EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" }
EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" }
EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" } EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" }
EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID } EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID }
@@ -98,7 +97,7 @@ var (
EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" } EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" }
EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID } EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID }
EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" } EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" }
EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk_delete" } EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" }
EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" }
EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID }
@@ -122,6 +121,8 @@ var (
EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID } EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID }
EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" } EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" }
EndpointGuildCreate = EndpointAPI + "guilds"
EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID }
EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }

View File

@@ -6,7 +6,7 @@ type EventHandler interface {
Type() string Type() string
// Handle is called whenever an event of Type() happens. // Handle is called whenever an event of Type() happens.
// It is the recievers responsibility to type assert that the interface // It is the receivers responsibility to type assert that the interface
// is the expected struct. // is the expected struct.
Handle(*Session, interface{}) Handle(*Session, interface{})
} }

View File

@@ -79,7 +79,7 @@ func main() {
ap.Name = Name ap.Name = Name
ap, err = dg.ApplicationCreate(ap) ap, err = dg.ApplicationCreate(ap)
if err != nil { if err != nil {
fmt.Println("error creating new applicaiton,", err) fmt.Println("error creating new application,", err)
return return
} }

View File

@@ -23,7 +23,7 @@ const (
LogError int = iota LogError int = iota
// LogWarning level is used for very abnormal events and errors that are // LogWarning level is used for very abnormal events and errors that are
// also returend to a calling function. // also returned to a calling function.
LogWarning LogWarning
// LogInformational level is used for normal non-error activity // LogInformational level is used for normal non-error activity
@@ -34,14 +34,21 @@ const (
LogDebug LogDebug
) )
// Logger can be used to replace the standard logging for discordgo
var Logger func(msgL, caller int, format string, a ...interface{})
// msglog provides package wide logging consistancy for discordgo // msglog provides package wide logging consistancy for discordgo
// the format, a... portion this command follows that of fmt.Printf // the format, a... portion this command follows that of fmt.Printf
// msgL : LogLevel of the message // msgL : LogLevel of the message
// caller : 1 + the number of callers away from the message source // caller : 1 + the number of callers away from the message source
// format : Printf style message format // format : Printf style message format
// a ... : comma seperated list of values to pass // a ... : comma separated list of values to pass
func msglog(msgL, caller int, format string, a ...interface{}) { func msglog(msgL, caller int, format string, a ...interface{}) {
if Logger != nil {
Logger(msgL, caller, format, a...)
} else {
pc, file, line, _ := runtime.Caller(caller) pc, file, line, _ := runtime.Caller(caller)
files := strings.Split(file, "/") files := strings.Split(file, "/")
@@ -54,6 +61,7 @@ func msglog(msgL, caller int, format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg) log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg)
}
} }
// helper function that wraps msglog for the Session struct // helper function that wraps msglog for the Session struct

View File

@@ -237,7 +237,7 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e
continue continue
} }
content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1) content = strings.Replace(content, "<@&"+role.ID+">", "@"+role.Name, -1)
} }
content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {

View File

@@ -41,8 +41,8 @@ func NewRatelimiter() *RateLimiter {
} }
} }
// getBucket retrieves or creates a bucket // GetBucket retrieves or creates a bucket
func (r *RateLimiter) getBucket(key string) *Bucket { func (r *RateLimiter) GetBucket(key string) *Bucket {
r.Lock() r.Lock()
defer r.Unlock() defer r.Unlock()
@@ -51,7 +51,7 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
} }
b := &Bucket{ b := &Bucket{
remaining: 1, Remaining: 1,
Key: key, Key: key,
global: r.global, global: r.global,
} }
@@ -68,27 +68,37 @@ func (r *RateLimiter) getBucket(key string) *Bucket {
return b return b
} }
// LockBucket Locks until a request can be made // GetWaitTime returns the duration you should wait for a Bucket
func (r *RateLimiter) LockBucket(bucketID string) *Bucket { func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration {
b := r.getBucket(bucketID)
b.Lock()
// If we ran out of calls and the reset time is still ahead of us // If we ran out of calls and the reset time is still ahead of us
// then we need to take it easy and relax a little // then we need to take it easy and relax a little
if b.remaining < 1 && b.reset.After(time.Now()) { if b.Remaining < minRemaining && b.reset.After(time.Now()) {
time.Sleep(b.reset.Sub(time.Now())) return b.reset.Sub(time.Now())
} }
// Check for global ratelimits // Check for global ratelimits
sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) sleepTo := time.Unix(0, atomic.LoadInt64(r.global))
if now := time.Now(); now.Before(sleepTo) { if now := time.Now(); now.Before(sleepTo) {
time.Sleep(sleepTo.Sub(now)) return sleepTo.Sub(now)
} }
b.remaining-- return 0
}
// LockBucket Locks until a request can be made
func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
return r.LockBucketObject(r.GetBucket(bucketID))
}
// LockBucketObject Locks an already resolved bucket until a request can be made
func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket {
b.Lock()
if wait := r.GetWaitTime(b, 1); wait > 0 {
time.Sleep(wait)
}
b.Remaining--
return b return b
} }
@@ -96,13 +106,14 @@ func (r *RateLimiter) LockBucket(bucketID string) *Bucket {
type Bucket struct { type Bucket struct {
sync.Mutex sync.Mutex
Key string Key string
remaining int Remaining int
limit int limit int
reset time.Time reset time.Time
global *int64 global *int64
lastReset time.Time lastReset time.Time
customRateLimit *customRateLimit customRateLimit *customRateLimit
Userdata interface{}
} }
// Release unlocks the bucket and reads the headers to update the buckets ratelimit info // Release unlocks the bucket and reads the headers to update the buckets ratelimit info
@@ -113,10 +124,10 @@ func (b *Bucket) Release(headers http.Header) error {
// Check if the bucket uses a custom ratelimiter // Check if the bucket uses a custom ratelimiter
if rl := b.customRateLimit; rl != nil { if rl := b.customRateLimit; rl != nil {
if time.Now().Sub(b.lastReset) >= rl.reset { if time.Now().Sub(b.lastReset) >= rl.reset {
b.remaining = rl.requests - 1 b.Remaining = rl.requests - 1
b.lastReset = time.Now() b.lastReset = time.Now()
} }
if b.remaining < 1 { if b.Remaining < 1 {
b.reset = time.Now().Add(rl.reset) b.reset = time.Now().Add(rl.reset)
} }
return nil return nil
@@ -176,7 +187,7 @@ func (b *Bucket) Release(headers http.Header) error {
if err != nil { if err != nil {
return err return err
} }
b.remaining = int(parsedRemaining) b.Remaining = int(parsedRemaining)
} }
return nil return nil

View File

@@ -65,9 +65,11 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
if bucketID == "" { if bucketID == "" {
bucketID = strings.SplitN(urlStr, "?", 2)[0] bucketID = strings.SplitN(urlStr, "?", 2)[0]
} }
return s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucket(bucketID), sequence)
}
bucket := s.ratelimiter.LockBucket(bucketID) // RequestWithLockedBucket makes a request using a bucket that's already been locked
func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b []byte, bucket *Bucket, sequence int) (response []byte, err error) {
if s.Debug { if s.Debug {
log.Printf("API REQUEST %8s :: %s\n", method, urlStr) log.Printf("API REQUEST %8s :: %s\n", method, urlStr)
log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b)) log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b))
@@ -139,7 +141,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
if sequence < s.MaxRestRetries { if sequence < s.MaxRestRetries {
s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status) s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status)
response, err = s.request(method, urlStr, contentType, b, bucketID, sequence+1) response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence+1)
} else { } else {
err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response)
} }
@@ -158,7 +160,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID
// we can make the above smarter // we can make the above smarter
// this method can cause longer delays than required // this method can cause longer delays than required
response, err = s.request(method, urlStr, contentType, b, bucketID, sequence) response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence)
default: // Error condition default: // Error condition
err = newRestError(req, resp, response) err = newRestError(req, resp, response)
@@ -585,7 +587,7 @@ func (s *Session) GuildCreate(name string) (st *Guild, err error) {
Name string `json:"name"` Name string `json:"name"`
}{name} }{name}
body, err := s.RequestWithBucketID("POST", EndpointGuilds, data, EndpointGuilds) body, err := s.RequestWithBucketID("POST", EndpointGuildCreate, data, EndpointGuildCreate)
if err != nil { if err != nil {
return return
} }
@@ -907,7 +909,7 @@ func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err
// GuildInvites returns an array of Invite structures for the given guild // GuildInvites returns an array of Invite structures for the given guild
// guildID : The ID of a Guild. // guildID : The ID of a Guild.
func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) { func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) {
body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInivtes(guildID)) body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInvites(guildID))
if err != nil { if err != nil {
return return
} }
@@ -957,6 +959,7 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b
// Prevent sending a color int that is too big. // Prevent sending a color int that is too big.
if color > 0xFFFFFF { if color > 0xFFFFFF {
err = fmt.Errorf("color value cannot be larger than 0xFFFFFF") err = fmt.Errorf("color value cannot be larger than 0xFFFFFF")
return nil, err
} }
data := struct { data := struct {
@@ -1020,6 +1023,9 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er
uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days) uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days)
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID))
if err != nil {
return
}
err = unmarshal(body, &p) err = unmarshal(body, &p)
if err != nil { if err != nil {
@@ -1204,7 +1210,7 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string)
// Functions specific to Discord Channels // Functions specific to Discord Channels
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Channel returns a Channel strucutre of a specific Channel. // Channel returns a Channel structure of a specific Channel.
// channelID : The ID of the Channel you want returned. // channelID : The ID of the Channel you want returned.
func (s *Session) Channel(channelID string) (st *Channel, err error) { func (s *Session) Channel(channelID string) (st *Channel, err error) {
body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID)) body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID))
@@ -1219,12 +1225,16 @@ func (s *Session) Channel(channelID string) (st *Channel, err error) {
// ChannelEdit edits the given channel // ChannelEdit edits the given channel
// channelID : The ID of a Channel // channelID : The ID of a Channel
// name : The new name to assign the channel. // name : The new name to assign the channel.
func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) { func (s *Session) ChannelEdit(channelID, name string) (*Channel, error) {
return s.ChannelEditComplex(channelID, &ChannelEdit{
data := struct { Name: name,
Name string `json:"name"` })
}{name} }
// ChannelEditComplex edits an existing channel, replacing the parameters entirely with ChannelEdit struct
// channelID : The ID of a Channel
// data : The channel struct to send
func (s *Session) ChannelEditComplex(channelID string, data *ChannelEdit) (st *Channel, err error) {
body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID)) body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID))
if err != nil { if err != nil {
return return
@@ -1476,7 +1486,7 @@ func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error)
} }
// ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs. // ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs.
// If only one messageID is in the slice call channelMessageDelete funciton. // If only one messageID is in the slice call channelMessageDelete function.
// If the slice is empty do nothing. // If the slice is empty do nothing.
// channelID : The ID of the channel for the messages to delete. // channelID : The ID of the channel for the messages to delete.
// messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages. // messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages.
@@ -1569,16 +1579,14 @@ func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) {
// ChannelInviteCreate creates a new invite for the given channel. // ChannelInviteCreate creates a new invite for the given channel.
// channelID : The ID of a Channel // channelID : The ID of a Channel
// i : An Invite struct with the values MaxAge, MaxUses, Temporary, // i : An Invite struct with the values MaxAge, MaxUses and Temporary defined.
// and XkcdPass defined.
func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) { func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) {
data := struct { data := struct {
MaxAge int `json:"max_age"` MaxAge int `json:"max_age"`
MaxUses int `json:"max_uses"` MaxUses int `json:"max_uses"`
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
XKCDPass string `json:"xkcdpass"` }{i.MaxAge, i.MaxUses, i.Temporary}
}{i.MaxAge, i.MaxUses, i.Temporary, i.XkcdPass}
body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID))
if err != nil { if err != nil {
@@ -1618,7 +1626,7 @@ func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Invite returns an Invite structure of the given invite // Invite returns an Invite structure of the given invite
// inviteID : The invite code (or maybe xkcdpass?) // inviteID : The invite code
func (s *Session) Invite(inviteID string) (st *Invite, err error) { func (s *Session) Invite(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite(""))
@@ -1631,7 +1639,7 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) {
} }
// InviteDelete deletes an existing invite // InviteDelete deletes an existing invite
// inviteID : the code (or maybe xkcdpass?) of an invite // inviteID : the code of an invite
func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite(""))
@@ -1644,7 +1652,7 @@ func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
} }
// InviteAccept accepts an Invite to a Guild or Channel // InviteAccept accepts an Invite to a Guild or Channel
// inviteID : The invite code (or maybe xkcdpass?) // inviteID : The invite code
func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) { func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite("")) body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite(""))

View File

@@ -531,7 +531,7 @@ func (s *State) PrivateChannel(channelID string) (*Channel, error) {
return s.Channel(channelID) return s.Channel(channelID)
} }
// Channel gets a channel by ID, it will look in all guilds an private channels. // Channel gets a channel by ID, it will look in all guilds and private channels.
func (s *State) Channel(channelID string) (*Channel, error) { func (s *State) Channel(channelID string) (*Channel, error) {
if s == nil { if s == nil {
return nil, ErrNilState return nil, ErrNilState
@@ -816,6 +816,13 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
if s.TrackMembers { if s.TrackMembers {
err = s.MemberRemove(t.Member) err = s.MemberRemove(t.Member)
} }
case *GuildMembersChunk:
if s.TrackMembers {
for i := range t.Members {
t.Members[i].GuildID = t.GuildID
err = s.MemberAdd(t.Members[i])
}
}
case *GuildRoleCreate: case *GuildRoleCreate:
if s.TrackRoles { if s.TrackRoles {
err = s.RoleAdd(t.GuildID, t.Role) err = s.RoleAdd(t.GuildID, t.Role)

View File

@@ -14,7 +14,6 @@ package discordgo
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv"
"sync" "sync"
"time" "time"
@@ -85,6 +84,9 @@ type Session struct {
// Stores the last HeartbeatAck that was recieved (in UTC) // Stores the last HeartbeatAck that was recieved (in UTC)
LastHeartbeatAck time.Time LastHeartbeatAck time.Time
// used to deal with rate limits
Ratelimiter *RateLimiter
// Event handlers // Event handlers
handlersMu sync.RWMutex handlersMu sync.RWMutex
handlers map[string][]*eventHandlerInstance handlers map[string][]*eventHandlerInstance
@@ -96,9 +98,6 @@ type Session struct {
// When nil, the session is not listening. // When nil, the session is not listening.
listening chan interface{} listening chan interface{}
// used to deal with rate limits
ratelimiter *RateLimiter
// sequence tracks the current gateway api websocket sequence number // sequence tracks the current gateway api websocket sequence number
sequence *int64 sequence *int64
@@ -143,9 +142,9 @@ type Invite struct {
MaxAge int `json:"max_age"` MaxAge int `json:"max_age"`
Uses int `json:"uses"` Uses int `json:"uses"`
MaxUses int `json:"max_uses"` MaxUses int `json:"max_uses"`
XkcdPass string `json:"xkcdpass"`
Revoked bool `json:"revoked"` Revoked bool `json:"revoked"`
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
Unique bool `json:"unique"`
} }
// ChannelType is the type of a Channel // ChannelType is the type of a Channel
@@ -171,9 +170,22 @@ type Channel struct {
NSFW bool `json:"nsfw"` NSFW bool `json:"nsfw"`
Position int `json:"position"` Position int `json:"position"`
Bitrate int `json:"bitrate"` Bitrate int `json:"bitrate"`
Recipients []*User `json:"recipient"` Recipients []*User `json:"recipients"`
Messages []*Message `json:"-"` Messages []*Message `json:"-"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
ParentID string `json:"parent_id"`
}
// A ChannelEdit holds Channel Feild data for a channel edit.
type ChannelEdit struct {
Name string `json:"name,omitempty"`
Topic string `json:"topic,omitempty"`
NSFW bool `json:"nsfw,omitempty"`
Position int `json:"position"`
Bitrate int `json:"bitrate,omitempty"`
UserLimit int `json:"user_limit,omitempty"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
ParentID string `json:"parent_id,omitempty"`
} }
// A PermissionOverwrite holds permission overwrite data for a Channel // A PermissionOverwrite holds permission overwrite data for a Channel
@@ -191,6 +203,7 @@ type Emoji struct {
Roles []string `json:"roles"` Roles []string `json:"roles"`
Managed bool `json:"managed"` Managed bool `json:"managed"`
RequireColons bool `json:"require_colons"` RequireColons bool `json:"require_colons"`
Animated bool `json:"animated"`
} }
// APIName returns an correctly formatted API name for use in the MessageReactions endpoints. // APIName returns an correctly formatted API name for use in the MessageReactions endpoints.
@@ -204,7 +217,7 @@ func (e *Emoji) APIName() string {
return e.ID return e.ID
} }
// VerificationLevel type defination // VerificationLevel type definition
type VerificationLevel int type VerificationLevel int
// Constants for VerificationLevel levels from 0 to 3 inclusive // Constants for VerificationLevel levels from 0 to 3 inclusive
@@ -314,43 +327,56 @@ type Presence struct {
Since *int `json:"since"` Since *int `json:"since"`
} }
// GameType is the type of "game" (see GameType* consts) in the Game struct
type GameType int
// Valid GameType values
const (
GameTypeGame GameType = iota
GameTypeStreaming
)
// A Game struct holds the name of the "playing .." game for a user // A Game struct holds the name of the "playing .." game for a user
type Game struct { type Game struct {
Name string `json:"name"` Name string `json:"name"`
Type int `json:"type"` Type GameType `json:"type"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Details string `json:"details,omitempty"`
State string `json:"state,omitempty"`
TimeStamps TimeStamps `json:"timestamps,omitempty"`
Assets Assets `json:"assets,omitempty"`
ApplicationID string `json:"application_id,omitempty"`
Instance int8 `json:"instance,omitempty"`
// TODO: Party and Secrets (unknown structure)
} }
// UnmarshalJSON unmarshals json to Game struct // A TimeStamps struct contains start and end times used in the rich presence "playing .." Game
func (g *Game) UnmarshalJSON(bytes []byte) error { type TimeStamps struct {
temp := &struct { EndTimestamp int64 `json:"end,omitempty"`
Name json.Number `json:"name"` StartTimestamp int64 `json:"start,omitempty"`
Type json.RawMessage `json:"type"` }
URL string `json:"url"`
// UnmarshalJSON unmarshals JSON into TimeStamps struct
func (t *TimeStamps) UnmarshalJSON(b []byte) error {
temp := struct {
End float64 `json:"end,omitempty"`
Start float64 `json:"start,omitempty"`
}{} }{}
err := json.Unmarshal(bytes, temp) err := json.Unmarshal(b, &temp)
if err != nil { if err != nil {
return err return err
} }
g.URL = temp.URL t.EndTimestamp = int64(temp.End)
g.Name = temp.Name.String() t.StartTimestamp = int64(temp.Start)
if temp.Type != nil {
err = json.Unmarshal(temp.Type, &g.Type)
if err == nil {
return nil return nil
} }
s := "" // An Assets struct contains assets and labels used in the rich presence "playing .." Game
err = json.Unmarshal(temp.Type, &s) type Assets struct {
if err == nil { LargeImageID string `json:"large_image,omitempty"`
g.Type, err = strconv.Atoi(s) SmallImageID string `json:"small_image,omitempty"`
} LargeText string `json:"large_text,omitempty"`
SmallText string `json:"small_text,omitempty"`
return err
}
return nil
} }
// A Member stores user information for Guild members. // A Member stores user information for Guild members.
@@ -383,7 +409,7 @@ type Settings struct {
DeveloperMode bool `json:"developer_mode"` DeveloperMode bool `json:"developer_mode"`
} }
// Status type defination // Status type definition
type Status string type Status string
// Constants for Status with the different current available status // Constants for Status with the different current available status

View File

@@ -30,6 +30,8 @@ func (u *User) Mention() string {
// AvatarURL returns a URL to the user's avatar. // AvatarURL returns a URL to the user's avatar.
// size: The size of the user's avatar as a power of two // size: The size of the user's avatar as a power of two
// if size is an empty string, no size parameter will
// be added to the URL.
func (u *User) AvatarURL(size string) string { func (u *User) AvatarURL(size string) string {
var URL string var URL string
if strings.HasPrefix(u.Avatar, "a_") { if strings.HasPrefix(u.Avatar, "a_") {
@@ -38,5 +40,8 @@ func (u *User) AvatarURL(size string) string {
URL = EndpointUserAvatar(u.ID, u.Avatar) URL = EndpointUserAvatar(u.ID, u.Avatar)
} }
if size != "" {
return URL + "?size=" + size return URL + "?size=" + size
}
return URL
} }

View File

@@ -13,7 +13,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net" "net"
"strings" "strings"
"sync" "sync"
@@ -69,7 +68,7 @@ type VoiceConnection struct {
voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler
} }
// VoiceSpeakingUpdateHandler type provides a function defination for the // VoiceSpeakingUpdateHandler type provides a function definition for the
// VoiceSpeakingUpdate event // VoiceSpeakingUpdate event
type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate) type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate)
@@ -104,7 +103,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) {
defer v.Unlock() defer v.Unlock()
if err != nil { if err != nil {
v.speaking = false v.speaking = false
log.Println("Speaking() write json error:", err) v.log(LogError, "Speaking() write json error:", err)
return return
} }
@@ -181,7 +180,7 @@ func (v *VoiceConnection) Close() {
v.log(LogInformational, "closing udp") v.log(LogInformational, "closing udp")
err := v.udpConn.Close() err := v.udpConn.Close()
if err != nil { if err != nil {
log.Println("error closing udp connection: ", err) v.log(LogError, "error closing udp connection: ", err)
} }
v.udpConn = nil v.udpConn = nil
} }
@@ -247,7 +246,7 @@ type voiceOP2 struct {
} }
// WaitUntilConnected waits for the Voice Connection to // WaitUntilConnected waits for the Voice Connection to
// become ready, if it does not become ready it retuns an err // become ready, if it does not become ready it returns an err
func (v *VoiceConnection) waitUntilConnected() error { func (v *VoiceConnection) waitUntilConnected() error {
v.log(LogInformational, "called") v.log(LogInformational, "called")
@@ -858,7 +857,7 @@ func (v *VoiceConnection) reconnect() {
} }
if v.session.DataReady == false || v.session.wsConn == nil { if v.session.DataReady == false || v.session.wsConn == nil {
v.log(LogInformational, "cannot reconenct to channel %s with unready session", v.ChannelID) v.log(LogInformational, "cannot reconnect to channel %s with unready session", v.ChannelID)
continue continue
} }

View File

@@ -15,6 +15,7 @@ import (
"compress/zlib" "compress/zlib"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"runtime" "runtime"
@@ -45,19 +46,114 @@ type resumePacket struct {
} `json:"d"` } `json:"d"`
} }
// Open opens a websocket connection to Discord. // Open creates a websocket connection to Discord.
func (s *Session) Open() (err error) { // See: https://discordapp.com/developers/docs/topics/gateway#connecting
func (s *Session) Open() error {
s.log(LogInformational, "called") s.log(LogInformational, "called")
var err error
// Prevent Open or other major Session functions from
// being called while Open is still running.
s.Lock() s.Lock()
defer func() { defer s.Unlock()
// If the websock is already open, bail out here.
if s.wsConn != nil {
return ErrWSAlreadyOpen
}
// Get the gateway to use for the Websocket connection
if s.gateway == "" {
s.gateway, err = s.Gateway()
if err != nil { if err != nil {
s.Unlock() return err
}
// Add the version and encoding to the URL
s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
}
// Connect to the Gateway
s.log(LogInformational, "connecting to gateway %s", s.gateway)
header := http.Header{}
header.Add("accept-encoding", "zlib")
s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
if err != nil {
s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err)
s.gateway = "" // clear cached gateway
s.wsConn = nil // Just to be safe.
return err
}
defer func() {
// because of this, all code below must set err to the error
// when exiting with an error :) Maybe someone has a better
// way :)
if err != nil {
s.wsConn.Close()
s.wsConn = nil
} }
}() }()
// The first response from Discord should be an Op 10 (Hello) Packet.
// When processed by onEvent the heartbeat goroutine will be started.
mt, m, err := s.wsConn.ReadMessage()
if err != nil {
return err
}
e, err := s.onEvent(mt, m)
if err != nil {
return err
}
if e.Operation != 10 {
err = fmt.Errorf("expecting Op 10, got Op %d instead", e.Operation)
return err
}
s.log(LogInformational, "Op 10 Hello Packet received from Discord")
s.LastHeartbeatAck = time.Now().UTC()
var h helloOp
if err = json.Unmarshal(e.RawData, &h); err != nil {
err = fmt.Errorf("error unmarshalling helloOp, %s", err)
return err
}
// Now we send either an Op 2 Identity if this is a brand new
// connection or Op 6 Resume if we are resuming an existing connection.
sequence := atomic.LoadInt64(s.sequence)
if s.sessionID == "" && sequence == 0 {
// Send Op 2 Identity Packet
err = s.identify()
if err != nil {
err = fmt.Errorf("error sending identify packet to gateway, %s, %s", s.gateway, err)
return err
}
} else {
// Send Op 6 Resume Packet
p := resumePacket{}
p.Op = 6
p.Data.Token = s.Token
p.Data.SessionID = s.sessionID
p.Data.Sequence = sequence
s.log(LogInformational, "sending resume packet to gateway")
s.wsMutex.Lock()
err = s.wsConn.WriteJSON(p)
s.wsMutex.Unlock()
if err != nil {
err = fmt.Errorf("error sending gateway resume packet, %s, %s", s.gateway, err)
return err
}
}
// A basic state is a hard requirement for Voice. // A basic state is a hard requirement for Voice.
// We create it here so the below READY/RESUMED packet can populate
// the state :)
// XXX: Move to New() func?
if s.State == nil { if s.State == nil {
state := NewState() state := NewState()
state.TrackChannels = false state.TrackChannels = false
@@ -68,77 +164,42 @@ func (s *Session) Open() (err error) {
s.State = state s.State = state
} }
if s.wsConn != nil { // Now Discord should send us a READY or RESUMED packet.
err = ErrWSAlreadyOpen mt, m, err = s.wsConn.ReadMessage()
return if err != nil {
return err
} }
e, err = s.onEvent(mt, m)
if err != nil {
return err
}
if e.Type != `READY` && e.Type != `RESUMED` {
// This is not fatal, but it does not follow their API documentation.
s.log(LogWarning, "Expected READY/RESUMED, instead got:\n%#v\n", e)
}
s.log(LogInformational, "First Packet:\n%#v\n", e)
s.log(LogInformational, "We are now connected to Discord, emitting connect event")
s.handleEvent(connectEventType, &Connect{})
// A VoiceConnections map is a hard requirement for Voice.
// XXX: can this be moved to when opening a voice connection?
if s.VoiceConnections == nil { if s.VoiceConnections == nil {
s.log(LogInformational, "creating new VoiceConnections map") s.log(LogInformational, "creating new VoiceConnections map")
s.VoiceConnections = make(map[string]*VoiceConnection) s.VoiceConnections = make(map[string]*VoiceConnection)
} }
// Get the gateway to use for the Websocket connection // Create listening chan outside of listen, as it needs to happen inside the
if s.gateway == "" { // mutex lock and needs to exist before calling heartbeat and listen
s.gateway, err = s.Gateway() // go rountines.
if err != nil {
return
}
// Add the version and encoding to the URL
s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json"
}
header := http.Header{}
header.Add("accept-encoding", "zlib")
s.log(LogInformational, "connecting to gateway %s", s.gateway)
s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
if err != nil {
s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err)
s.gateway = "" // clear cached gateway
// TODO: should we add a retry block here?
return
}
sequence := atomic.LoadInt64(s.sequence)
if s.sessionID != "" && sequence > 0 {
p := resumePacket{}
p.Op = 6
p.Data.Token = s.Token
p.Data.SessionID = s.sessionID
p.Data.Sequence = sequence
s.log(LogInformational, "sending resume packet to gateway")
err = s.wsConn.WriteJSON(p)
if err != nil {
s.log(LogWarning, "error sending gateway resume packet, %s, %s", s.gateway, err)
return
}
} else {
err = s.identify()
if err != nil {
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
return
}
}
// Create listening outside of listen, as it needs to happen inside the mutex
// lock.
s.listening = make(chan interface{}) s.listening = make(chan interface{})
// Start sending heartbeats and reading messages from Discord.
go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval)
go s.listen(s.wsConn, s.listening) go s.listen(s.wsConn, s.listening)
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock()
s.log(LogInformational, "emit connect event")
s.handleEvent(connectEventType, &Connect{})
s.log(LogInformational, "exiting") s.log(LogInformational, "exiting")
return return nil
} }
// listen polls the websocket connection for events, it will stop when the // listen polls the websocket connection for events, it will stop when the
@@ -249,7 +310,8 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
} }
} }
type updateStatusData struct { // UpdateStatusData ia provided to UpdateStatusComplex()
type UpdateStatusData struct {
IdleSince *int `json:"since"` IdleSince *int `json:"since"`
Game *Game `json:"game"` Game *Game `json:"game"`
AFK bool `json:"afk"` AFK bool `json:"afk"`
@@ -258,7 +320,7 @@ type updateStatusData struct {
type updateStatusOp struct { type updateStatusOp struct {
Op int `json:"op"` Op int `json:"op"`
Data updateStatusData `json:"d"` Data UpdateStatusData `json:"d"`
} }
// UpdateStreamingStatus is used to update the user's streaming status. // UpdateStreamingStatus is used to update the user's streaming status.
@@ -270,13 +332,7 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
s.log(LogInformational, "called") s.log(LogInformational, "called")
s.RLock() usd := UpdateStatusData{
defer s.RUnlock()
if s.wsConn == nil {
return ErrWSNotFound
}
usd := updateStatusData{
Status: "online", Status: "online",
} }
@@ -285,9 +341,9 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
} }
if game != "" { if game != "" {
gameType := 0 gameType := GameTypeGame
if url != "" { if url != "" {
gameType = 1 gameType = GameTypeStreaming
} }
usd.Game = &Game{ usd.Game = &Game{
Name: game, Name: game,
@@ -296,6 +352,18 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
} }
} }
return s.UpdateStatusComplex(usd)
}
// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo.
func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
s.RLock()
defer s.RUnlock()
if s.wsConn == nil {
return ErrWSNotFound
}
s.wsMutex.Lock() s.wsMutex.Lock()
err = s.wsConn.WriteJSON(updateStatusOp{3, usd}) err = s.wsConn.WriteJSON(updateStatusOp{3, usd})
s.wsMutex.Unlock() s.wsMutex.Unlock()
@@ -357,9 +425,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err
// //
// If you use the AddHandler() function to register a handler for the // If you use the AddHandler() function to register a handler for the
// "OnEvent" event then all events will be passed to that handler. // "OnEvent" event then all events will be passed to that handler.
// func (s *Session) onEvent(messageType int, message []byte) (*Event, error) {
// TODO: You may also register a custom event handler entirely using...
func (s *Session) onEvent(messageType int, message []byte) {
var err error var err error
var reader io.Reader var reader io.Reader
@@ -371,7 +437,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
z, err2 := zlib.NewReader(reader) z, err2 := zlib.NewReader(reader)
if err2 != nil { if err2 != nil {
s.log(LogError, "error uncompressing websocket message, %s", err) s.log(LogError, "error uncompressing websocket message, %s", err)
return return nil, err2
} }
defer func() { defer func() {
@@ -389,7 +455,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
decoder := json.NewDecoder(reader) decoder := json.NewDecoder(reader)
if err = decoder.Decode(&e); err != nil { if err = decoder.Decode(&e); err != nil {
s.log(LogError, "error decoding websocket message, %s", err) s.log(LogError, "error decoding websocket message, %s", err)
return return e, err
} }
s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData)) s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData))
@@ -403,10 +469,10 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil { if err != nil {
s.log(LogError, "error sending heartbeat in response to Op1") s.log(LogError, "error sending heartbeat in response to Op1")
return return e, err
} }
return return e, nil
} }
// Reconnect // Reconnect
@@ -415,7 +481,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.log(LogInformational, "Closing and reconnecting in response to Op7") s.log(LogInformational, "Closing and reconnecting in response to Op7")
s.Close() s.Close()
s.reconnect() s.reconnect()
return return e, nil
} }
// Invalid Session // Invalid Session
@@ -427,20 +493,15 @@ func (s *Session) onEvent(messageType int, message []byte) {
err = s.identify() err = s.identify()
if err != nil { if err != nil {
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
return return e, err
} }
return return e, nil
} }
if e.Operation == 10 { if e.Operation == 10 {
var h helloOp // Op10 is handled by Open()
if err = json.Unmarshal(e.RawData, &h); err != nil { return e, nil
s.log(LogError, "error unmarshalling helloOp, %s", err)
} else {
go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval)
}
return
} }
if e.Operation == 11 { if e.Operation == 11 {
@@ -448,7 +509,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
s.LastHeartbeatAck = time.Now().UTC() s.LastHeartbeatAck = time.Now().UTC()
s.Unlock() s.Unlock()
s.log(LogInformational, "got heartbeat ACK") s.log(LogInformational, "got heartbeat ACK")
return return e, nil
} }
// Do not try to Dispatch a non-Dispatch Message // Do not try to Dispatch a non-Dispatch Message
@@ -456,7 +517,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
// But we probably should be doing something with them. // But we probably should be doing something with them.
// TEMP // TEMP
s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message))
return return e, nil
} }
// Store the message sequence // Store the message sequence
@@ -485,6 +546,8 @@ func (s *Session) onEvent(messageType int, message []byte) {
// For legacy reasons, we send the raw event also, this could be useful for handling unknown events. // For legacy reasons, we send the raw event also, this could be useful for handling unknown events.
s.handleEvent(eventEventType, e) s.handleEvent(eventEventType, e)
return e, nil
} }
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
@@ -610,7 +673,7 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) {
voice.GuildID = st.GuildID voice.GuildID = st.GuildID
voice.Unlock() voice.Unlock()
// Open a conenction to the voice server // Open a connection to the voice server
err := voice.open() err := voice.open()
if err != nil { if err != nil {
s.log(LogError, "onVoiceServerUpdate voice.open, %s", err) s.log(LogError, "onVoiceServerUpdate voice.open, %s", err)

77
vendor/github.com/gorilla/websocket/proxy.go generated vendored Normal file
View File

@@ -0,0 +1,77 @@
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"encoding/base64"
"errors"
"net"
"net/http"
"net/url"
"strings"
)
type netDialerFunc func(netowrk, addr string) (net.Conn, error)
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
return fn(network, addr)
}
func init() {
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil
})
}
type httpProxyDialer struct {
proxyURL *url.URL
fowardDial func(network, addr string) (net.Conn, error)
}
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
hostPort, _ := hostPortNoPort(hpd.proxyURL)
conn, err := hpd.fowardDial(network, hostPort)
if err != nil {
return nil, err
}
connectHeader := make(http.Header)
if user := hpd.proxyURL.User; user != nil {
proxyUser := user.Username()
if proxyPassword, passwordSet := user.Password(); passwordSet {
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
}
}
connectReq := &http.Request{
Method: "CONNECT",
URL: &url.URL{Opaque: addr},
Host: addr,
Header: connectHeader,
}
if err := connectReq.Write(conn); err != nil {
conn.Close()
return nil, err
}
// Read response. It's OK to use and discard buffered reader here becaue
// the remote server does not speak until spoken to.
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, connectReq)
if err != nil {
conn.Close()
return nil, err
}
if resp.StatusCode != 200 {
conn.Close()
f := strings.SplitN(resp.Status, " ", 2)
return nil, errors.New(f[1])
}
return conn, nil
}

473
vendor/github.com/gorilla/websocket/x_net_proxy.go generated vendored Normal file
View File

@@ -0,0 +1,473 @@
// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy
// Package proxy provides support for a variety of protocols to proxy network
// data.
//
package websocket
import (
"errors"
"io"
"net"
"net/url"
"os"
"strconv"
"strings"
"sync"
)
type proxy_direct struct{}
// Direct is a direct proxy: one that makes network connections directly.
var proxy_Direct = proxy_direct{}
func (proxy_direct) Dial(network, addr string) (net.Conn, error) {
return net.Dial(network, addr)
}
// A PerHost directs connections to a default Dialer unless the host name
// requested matches one of a number of exceptions.
type proxy_PerHost struct {
def, bypass proxy_Dialer
bypassNetworks []*net.IPNet
bypassIPs []net.IP
bypassZones []string
bypassHosts []string
}
// NewPerHost returns a PerHost Dialer that directs connections to either
// defaultDialer or bypass, depending on whether the connection matches one of
// the configured rules.
func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost {
return &proxy_PerHost{
def: defaultDialer,
bypass: bypass,
}
}
// Dial connects to the address addr on the given network through either
// defaultDialer or bypass.
func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
return p.dialerForRequest(host).Dial(network, addr)
}
func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer {
if ip := net.ParseIP(host); ip != nil {
for _, net := range p.bypassNetworks {
if net.Contains(ip) {
return p.bypass
}
}
for _, bypassIP := range p.bypassIPs {
if bypassIP.Equal(ip) {
return p.bypass
}
}
return p.def
}
for _, zone := range p.bypassZones {
if strings.HasSuffix(host, zone) {
return p.bypass
}
if host == zone[1:] {
// For a zone ".example.com", we match "example.com"
// too.
return p.bypass
}
}
for _, bypassHost := range p.bypassHosts {
if bypassHost == host {
return p.bypass
}
}
return p.def
}
// AddFromString parses a string that contains comma-separated values
// specifying hosts that should use the bypass proxy. Each value is either an
// IP address, a CIDR range, a zone (*.example.com) or a host name
// (localhost). A best effort is made to parse the string and errors are
// ignored.
func (p *proxy_PerHost) AddFromString(s string) {
hosts := strings.Split(s, ",")
for _, host := range hosts {
host = strings.TrimSpace(host)
if len(host) == 0 {
continue
}
if strings.Contains(host, "/") {
// We assume that it's a CIDR address like 127.0.0.0/8
if _, net, err := net.ParseCIDR(host); err == nil {
p.AddNetwork(net)
}
continue
}
if ip := net.ParseIP(host); ip != nil {
p.AddIP(ip)
continue
}
if strings.HasPrefix(host, "*.") {
p.AddZone(host[1:])
continue
}
p.AddHost(host)
}
}
// AddIP specifies an IP address that will use the bypass proxy. Note that
// this will only take effect if a literal IP address is dialed. A connection
// to a named host will never match an IP.
func (p *proxy_PerHost) AddIP(ip net.IP) {
p.bypassIPs = append(p.bypassIPs, ip)
}
// AddNetwork specifies an IP range that will use the bypass proxy. Note that
// this will only take effect if a literal IP address is dialed. A connection
// to a named host will never match.
func (p *proxy_PerHost) AddNetwork(net *net.IPNet) {
p.bypassNetworks = append(p.bypassNetworks, net)
}
// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of
// "example.com" matches "example.com" and all of its subdomains.
func (p *proxy_PerHost) AddZone(zone string) {
if strings.HasSuffix(zone, ".") {
zone = zone[:len(zone)-1]
}
if !strings.HasPrefix(zone, ".") {
zone = "." + zone
}
p.bypassZones = append(p.bypassZones, zone)
}
// AddHost specifies a host name that will use the bypass proxy.
func (p *proxy_PerHost) AddHost(host string) {
if strings.HasSuffix(host, ".") {
host = host[:len(host)-1]
}
p.bypassHosts = append(p.bypassHosts, host)
}
// A Dialer is a means to establish a connection.
type proxy_Dialer interface {
// Dial connects to the given address via the proxy.
Dial(network, addr string) (c net.Conn, err error)
}
// Auth contains authentication parameters that specific Dialers may require.
type proxy_Auth struct {
User, Password string
}
// FromEnvironment returns the dialer specified by the proxy related variables in
// the environment.
func proxy_FromEnvironment() proxy_Dialer {
allProxy := proxy_allProxyEnv.Get()
if len(allProxy) == 0 {
return proxy_Direct
}
proxyURL, err := url.Parse(allProxy)
if err != nil {
return proxy_Direct
}
proxy, err := proxy_FromURL(proxyURL, proxy_Direct)
if err != nil {
return proxy_Direct
}
noProxy := proxy_noProxyEnv.Get()
if len(noProxy) == 0 {
return proxy
}
perHost := proxy_NewPerHost(proxy, proxy_Direct)
perHost.AddFromString(noProxy)
return perHost
}
// proxySchemes is a map from URL schemes to a function that creates a Dialer
// from a URL with such a scheme.
var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)
// RegisterDialerType takes a URL scheme and a function to generate Dialers from
// a URL with that scheme and a forwarding Dialer. Registered schemes are used
// by FromURL.
func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) {
if proxy_proxySchemes == nil {
proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error))
}
proxy_proxySchemes[scheme] = f
}
// FromURL returns a Dialer given a URL specification and an underlying
// Dialer for it to make network requests.
func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) {
var auth *proxy_Auth
if u.User != nil {
auth = new(proxy_Auth)
auth.User = u.User.Username()
if p, ok := u.User.Password(); ok {
auth.Password = p
}
}
switch u.Scheme {
case "socks5":
return proxy_SOCKS5("tcp", u.Host, auth, forward)
}
// If the scheme doesn't match any of the built-in schemes, see if it
// was registered by another package.
if proxy_proxySchemes != nil {
if f, ok := proxy_proxySchemes[u.Scheme]; ok {
return f(u, forward)
}
}
return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
}
var (
proxy_allProxyEnv = &proxy_envOnce{
names: []string{"ALL_PROXY", "all_proxy"},
}
proxy_noProxyEnv = &proxy_envOnce{
names: []string{"NO_PROXY", "no_proxy"},
}
)
// envOnce looks up an environment variable (optionally by multiple
// names) once. It mitigates expensive lookups on some platforms
// (e.g. Windows).
// (Borrowed from net/http/transport.go)
type proxy_envOnce struct {
names []string
once sync.Once
val string
}
func (e *proxy_envOnce) Get() string {
e.once.Do(e.init)
return e.val
}
func (e *proxy_envOnce) init() {
for _, n := range e.names {
e.val = os.Getenv(n)
if e.val != "" {
return
}
}
}
// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
// with an optional username and password. See RFC 1928 and RFC 1929.
func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) {
s := &proxy_socks5{
network: network,
addr: addr,
forward: forward,
}
if auth != nil {
s.user = auth.User
s.password = auth.Password
}
return s, nil
}
type proxy_socks5 struct {
user, password string
network, addr string
forward proxy_Dialer
}
const proxy_socks5Version = 5
const (
proxy_socks5AuthNone = 0
proxy_socks5AuthPassword = 2
)
const proxy_socks5Connect = 1
const (
proxy_socks5IP4 = 1
proxy_socks5Domain = 3
proxy_socks5IP6 = 4
)
var proxy_socks5Errors = []string{
"",
"general failure",
"connection forbidden",
"network unreachable",
"host unreachable",
"connection refused",
"TTL expired",
"command not supported",
"address type not supported",
}
// Dial connects to the address addr on the given network via the SOCKS5 proxy.
func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network)
}
conn, err := s.forward.Dial(s.network, s.addr)
if err != nil {
return nil, err
}
if err := s.connect(conn, addr); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
// connect takes an existing connection to a socks5 proxy server,
// and commands the server to extend that connection to target,
// which must be a canonical address with a host and port.
func (s *proxy_socks5) connect(conn net.Conn, target string) error {
host, portStr, err := net.SplitHostPort(target)
if err != nil {
return err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return errors.New("proxy: failed to parse port number: " + portStr)
}
if port < 1 || port > 0xffff {
return errors.New("proxy: port number out of range: " + portStr)
}
// the size here is just an estimate
buf := make([]byte, 0, 6+len(host))
buf = append(buf, proxy_socks5Version)
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword)
} else {
buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone)
}
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[0] != 5 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
}
if buf[1] == 0xff {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
}
// See RFC 1929
if buf[1] == proxy_socks5AuthPassword {
buf = buf[:0]
buf = append(buf, 1 /* password protocol version */)
buf = append(buf, uint8(len(s.user)))
buf = append(buf, s.user...)
buf = append(buf, uint8(len(s.password)))
buf = append(buf, s.password...)
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[1] != 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
}
}
buf = buf[:0]
buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */)
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
buf = append(buf, proxy_socks5IP4)
ip = ip4
} else {
buf = append(buf, proxy_socks5IP6)
}
buf = append(buf, ip...)
} else {
if len(host) > 255 {
return errors.New("proxy: destination host name too long: " + host)
}
buf = append(buf, proxy_socks5Domain)
buf = append(buf, byte(len(host)))
buf = append(buf, host...)
}
buf = append(buf, byte(port>>8), byte(port))
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
failure := "unknown error"
if int(buf[1]) < len(proxy_socks5Errors) {
failure = proxy_socks5Errors[buf[1]]
}
if len(failure) > 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
}
bytesToDiscard := 0
switch buf[3] {
case proxy_socks5IP4:
bytesToDiscard = net.IPv4len
case proxy_socks5IP6:
bytesToDiscard = net.IPv6len
case proxy_socks5Domain:
_, err := io.ReadFull(conn, buf[:1])
if err != nil {
return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
bytesToDiscard = int(buf[0])
default:
return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
}
if cap(buf) < bytesToDiscard {
buf = make([]byte, bytesToDiscard)
} else {
buf = buf[:bytesToDiscard]
}
if _, err := io.ReadFull(conn, buf); err != nil {
return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
// Also need to discard the port number
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
return nil
}

View File

@@ -274,13 +274,6 @@ func (c *context) Param(name string) string {
if n == name { if n == name {
return c.pvalues[i] return c.pvalues[i]
} }
// Param name with aliases
for _, p := range strings.Split(n, ",") {
if p == name {
return c.pvalues[i]
}
}
} }
} }
return "" return ""

View File

@@ -76,6 +76,7 @@ type (
DisableHTTP2 bool DisableHTTP2 bool
Debug bool Debug bool
HideBanner bool HideBanner bool
HidePort bool
HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler
Binder Binder Binder Binder
Validator Validator Validator Validator
@@ -213,7 +214,7 @@ const (
) )
const ( const (
version = "3.2.5" version = "3.2.6"
website = "https://echo.labstack.com" website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo // http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = ` banner = `
@@ -414,9 +415,9 @@ func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
// Any registers a new route for all HTTP methods and path with matching handler // Any registers a new route for all HTTP methods and path with matching handler
// in the router with optional route-level middleware. // in the router with optional route-level middleware.
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route { func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
routes := make([]*Route, 0) routes := make([]*Route, len(methods))
for _, m := range methods { for i, m := range methods {
routes = append(routes, e.Add(m, path, handler, middleware...)) routes[i] = e.Add(m, path, handler, middleware...)
} }
return routes return routes
} }
@@ -424,9 +425,9 @@ func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFun
// Match registers a new route for multiple HTTP methods and path with matching // Match registers a new route for multiple HTTP methods and path with matching
// handler in the router with optional route-level middleware. // handler in the router with optional route-level middleware.
func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route { func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
routes := make([]*Route, 0) routes := make([]*Route, len(methods))
for _, m := range methods { for i, m := range methods {
routes = append(routes, e.Add(m, path, handler, middleware...)) routes[i] = e.Add(m, path, handler, middleware...)
} }
return routes return routes
} }
@@ -644,7 +645,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
return err return err
} }
} }
if !e.HideBanner { if !e.HidePort {
e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr())) e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr()))
} }
return s.Serve(e.Listener) return s.Serve(e.Listener)
@@ -656,7 +657,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
} }
e.TLSListener = tls.NewListener(l, s.TLSConfig) e.TLSListener = tls.NewListener(l, s.TLSConfig)
} }
if !e.HideBanner { if !e.HidePort {
e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr())) e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr()))
} }
return s.Serve(e.TLSListener) return s.Serve(e.TLSListener)

View File

@@ -20,9 +20,11 @@ func (g *Group) Use(middleware ...MiddlewareFunc) {
g.middleware = append(g.middleware, middleware...) g.middleware = append(g.middleware, middleware...)
// Allow all requests to reach the group as they might get dropped if router // Allow all requests to reach the group as they might get dropped if router
// doesn't find a match, making none of the group middleware process. // doesn't find a match, making none of the group middleware process.
g.echo.Any(path.Clean(g.prefix+"/*"), func(c Context) error { for _, p := range []string{"", "/*"} {
g.echo.Any(path.Clean(g.prefix+p), func(c Context) error {
return NotFoundHandler(c) return NotFoundHandler(c)
}, g.middleware...) }, g.middleware...)
}
} }
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group.
@@ -71,17 +73,21 @@ func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
} }
// Any implements `Echo#Any()` for sub-routes within the Group. // Any implements `Echo#Any()` for sub-routes within the Group.
func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
g.Add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = g.Add(m, path, handler, middleware...)
} }
return routes
} }
// Match implements `Echo#Match()` for sub-routes within the Group. // Match implements `Echo#Match()` for sub-routes within the Group.
func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
g.Add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = g.Add(m, path, handler, middleware...)
} }
return routes
} }
// Group creates a new sub-group with prefix and optional sub-group-level middleware. // Group creates a new sub-group with prefix and optional sub-group-level middleware.

View File

@@ -88,6 +88,7 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
} else if valid { } else if valid {
return next(c) return next(c)
} }
break
} }
} }
} }

View File

@@ -33,7 +33,7 @@ type (
) )
var ( var (
// DefaultBodyDumpConfig is the default Gzip middleware config. // DefaultBodyDumpConfig is the default BodyDump middleware config.
DefaultBodyDumpConfig = BodyDumpConfig{ DefaultBodyDumpConfig = BodyDumpConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
} }

View File

@@ -17,7 +17,7 @@ type (
// Maximum allowed size for a request body, it can be specified // Maximum allowed size for a request body, it can be specified
// as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P. // as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P.
Limit string `json:"limit"` Limit string `yaml:"limit"`
limit int64 limit int64
} }

View File

@@ -20,7 +20,7 @@ type (
// Gzip compression level. // Gzip compression level.
// Optional. Default value -1. // Optional. Default value -1.
Level int `json:"level"` Level int `yaml:"level"`
} }
gzipResponseWriter struct { gzipResponseWriter struct {

View File

@@ -16,34 +16,34 @@ type (
// AllowOrigin defines a list of origins that may access the resource. // AllowOrigin defines a list of origins that may access the resource.
// Optional. Default value []string{"*"}. // Optional. Default value []string{"*"}.
AllowOrigins []string `json:"allow_origins"` AllowOrigins []string `yaml:"allow_origins"`
// AllowMethods defines a list methods allowed when accessing the resource. // AllowMethods defines a list methods allowed when accessing the resource.
// This is used in response to a preflight request. // This is used in response to a preflight request.
// Optional. Default value DefaultCORSConfig.AllowMethods. // Optional. Default value DefaultCORSConfig.AllowMethods.
AllowMethods []string `json:"allow_methods"` AllowMethods []string `yaml:"allow_methods"`
// AllowHeaders defines a list of request headers that can be used when // AllowHeaders defines a list of request headers that can be used when
// making the actual request. This in response to a preflight request. // making the actual request. This in response to a preflight request.
// Optional. Default value []string{}. // Optional. Default value []string{}.
AllowHeaders []string `json:"allow_headers"` AllowHeaders []string `yaml:"allow_headers"`
// AllowCredentials indicates whether or not the response to the request // AllowCredentials indicates whether or not the response to the request
// can be exposed when the credentials flag is true. When used as part of // can be exposed when the credentials flag is true. When used as part of
// a response to a preflight request, this indicates whether or not the // a response to a preflight request, this indicates whether or not the
// actual request can be made using credentials. // actual request can be made using credentials.
// Optional. Default value false. // Optional. Default value false.
AllowCredentials bool `json:"allow_credentials"` AllowCredentials bool `yaml:"allow_credentials"`
// ExposeHeaders defines a whitelist headers that clients are allowed to // ExposeHeaders defines a whitelist headers that clients are allowed to
// access. // access.
// Optional. Default value []string{}. // Optional. Default value []string{}.
ExposeHeaders []string `json:"expose_headers"` ExposeHeaders []string `yaml:"expose_headers"`
// MaxAge indicates how long (in seconds) the results of a preflight request // MaxAge indicates how long (in seconds) the results of a preflight request
// can be cached. // can be cached.
// Optional. Default value 0. // Optional. Default value 0.
MaxAge int `json:"max_age"` MaxAge int `yaml:"max_age"`
} }
) )

View File

@@ -18,7 +18,7 @@ type (
Skipper Skipper Skipper Skipper
// TokenLength is the length of the generated token. // TokenLength is the length of the generated token.
TokenLength uint8 `json:"token_length"` TokenLength uint8 `yaml:"token_length"`
// Optional. Default value 32. // Optional. Default value 32.
// TokenLookup is a string in the form of "<source>:<key>" that is used // TokenLookup is a string in the form of "<source>:<key>" that is used
@@ -28,35 +28,35 @@ type (
// - "header:<name>" // - "header:<name>"
// - "form:<name>" // - "form:<name>"
// - "query:<name>" // - "query:<name>"
TokenLookup string `json:"token_lookup"` TokenLookup string `yaml:"token_lookup"`
// Context key to store generated CSRF token into context. // Context key to store generated CSRF token into context.
// Optional. Default value "csrf". // Optional. Default value "csrf".
ContextKey string `json:"context_key"` ContextKey string `yaml:"context_key"`
// Name of the CSRF cookie. This cookie will store CSRF token. // Name of the CSRF cookie. This cookie will store CSRF token.
// Optional. Default value "csrf". // Optional. Default value "csrf".
CookieName string `json:"cookie_name"` CookieName string `yaml:"cookie_name"`
// Domain of the CSRF cookie. // Domain of the CSRF cookie.
// Optional. Default value none. // Optional. Default value none.
CookieDomain string `json:"cookie_domain"` CookieDomain string `yaml:"cookie_domain"`
// Path of the CSRF cookie. // Path of the CSRF cookie.
// Optional. Default value none. // Optional. Default value none.
CookiePath string `json:"cookie_path"` CookiePath string `yaml:"cookie_path"`
// Max age (in seconds) of the CSRF cookie. // Max age (in seconds) of the CSRF cookie.
// Optional. Default value 86400 (24hr). // Optional. Default value 86400 (24hr).
CookieMaxAge int `json:"cookie_max_age"` CookieMaxAge int `yaml:"cookie_max_age"`
// Indicates if CSRF cookie is secure. // Indicates if CSRF cookie is secure.
// Optional. Default value false. // Optional. Default value false.
CookieSecure bool `json:"cookie_secure"` CookieSecure bool `yaml:"cookie_secure"`
// Indicates if CSRF cookie is HTTP only. // Indicates if CSRF cookie is HTTP only.
// Optional. Default value false. // Optional. Default value false.
CookieHTTPOnly bool `json:"cookie_http_only"` CookieHTTPOnly bool `yaml:"cookie_http_only"`
} }
// csrfTokenExtractor defines a function that takes `echo.Context` and returns // csrfTokenExtractor defines a function that takes `echo.Context` and returns

View File

@@ -20,7 +20,8 @@ type (
// Possible values: // Possible values:
// - "header:<name>" // - "header:<name>"
// - "query:<name>" // - "query:<name>"
KeyLookup string `json:"key_lookup"` // - "form:<name>"
KeyLookup string `yaml:"key_lookup"`
// AuthScheme to be used in the Authorization header. // AuthScheme to be used in the Authorization header.
// Optional. Default value "Bearer". // Optional. Default value "Bearer".
@@ -81,6 +82,8 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
switch parts[0] { switch parts[0] {
case "query": case "query":
extractor = keyFromQuery(parts[1]) extractor = keyFromQuery(parts[1])
case "form":
extractor = keyFromForm(parts[1])
} }
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -134,3 +137,14 @@ func keyFromQuery(param string) keyExtractor {
return key, nil return key, nil
} }
} }
// keyFromForm returns a `keyExtractor` that extracts key from the form.
func keyFromForm(param string) keyExtractor {
return func(c echo.Context) (string, error) {
key := c.FormValue(param)
if key == "" {
return "", errors.New("Missing key in the form")
}
return key, nil
}
}

View File

@@ -26,6 +26,7 @@ type (
// - time_unix_nano // - time_unix_nano
// - time_rfc3339 // - time_rfc3339
// - time_rfc3339_nano // - time_rfc3339_nano
// - time_custom
// - id (Request ID) // - id (Request ID)
// - remote_ip // - remote_ip
// - uri // - uri
@@ -46,7 +47,10 @@ type (
// Example "${remote_ip} ${status}" // Example "${remote_ip} ${status}"
// //
// Optional. Default value DefaultLoggerConfig.Format. // Optional. Default value DefaultLoggerConfig.Format.
Format string `json:"format"` Format string `yaml:"format"`
// Optional. Default value DefaultLoggerConfig.CustomTimeFormat.
CustomTimeFormat string `yaml:"custom_time_format"`
// Output is a writer where logs in JSON format are written. // Output is a writer where logs in JSON format are written.
// Optional. Default value os.Stdout. // Optional. Default value os.Stdout.
@@ -66,6 +70,7 @@ var (
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
`"latency_human":"${latency_human}","bytes_in":${bytes_in},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in},` +
`"bytes_out":${bytes_out}}` + "\n", `"bytes_out":${bytes_out}}` + "\n",
CustomTimeFormat:"2006-01-02 15:04:05.00000",
Output: os.Stdout, Output: os.Stdout,
colorer: color.New(), colorer: color.New(),
} }
@@ -126,6 +131,8 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return buf.WriteString(time.Now().Format(time.RFC3339)) return buf.WriteString(time.Now().Format(time.RFC3339))
case "time_rfc3339_nano": case "time_rfc3339_nano":
return buf.WriteString(time.Now().Format(time.RFC3339Nano)) return buf.WriteString(time.Now().Format(time.RFC3339Nano))
case "time_custom":
return buf.WriteString(time.Now().Format(config.CustomTimeFormat))
case "id": case "id":
id := req.Header.Get(echo.HeaderXRequestID) id := req.Header.Get(echo.HeaderXRequestID)
if id == "" { if id == "" {

View File

@@ -1,6 +1,12 @@
package middleware package middleware
import "github.com/labstack/echo" import (
"regexp"
"strconv"
"strings"
"github.com/labstack/echo"
)
type ( type (
// Skipper defines a function to skip middleware. Returning true skips processing // Skipper defines a function to skip middleware. Returning true skips processing
@@ -8,6 +14,21 @@ type (
Skipper func(c echo.Context) bool Skipper func(c echo.Context) bool
) )
func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {
groups := pattern.FindAllStringSubmatch(input, -1)
if groups == nil {
return nil
}
values := groups[0][1:]
replace := make([]string, 2*len(values))
for i, v := range values {
j := 2 * i
replace[j] = "$" + strconv.Itoa(i+1)
replace[j+1] = v
}
return strings.NewReplacer(replace...)
}
// DefaultSkipper returns false which processes the middleware. // DefaultSkipper returns false which processes the middleware.
func DefaultSkipper(echo.Context) bool { func DefaultSkipper(echo.Context) bool {
return false return false

View File

@@ -8,6 +8,9 @@ import (
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"regexp"
"strings"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -24,33 +27,49 @@ type (
// Balancer defines a load balancing technique. // Balancer defines a load balancing technique.
// Required. // Required.
// Possible values:
// - RandomBalancer
// - RoundRobinBalancer
Balancer ProxyBalancer Balancer ProxyBalancer
// Rewrite defines URL path rewrite rules. The values captured in asterisk can be
// retrieved by index e.g. $1, $2 and so on.
// Examples:
// "/old": "/new",
// "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2",
Rewrite map[string]string
rewriteRegex map[*regexp.Regexp]string
} }
// ProxyTarget defines the upstream target. // ProxyTarget defines the upstream target.
ProxyTarget struct { ProxyTarget struct {
Name string
URL *url.URL URL *url.URL
} }
// RandomBalancer implements a random load balancing technique.
RandomBalancer struct {
Targets []*ProxyTarget
random *rand.Rand
}
// RoundRobinBalancer implements a round-robin load balancing technique.
RoundRobinBalancer struct {
Targets []*ProxyTarget
i uint32
}
// ProxyBalancer defines an interface to implement a load balancing technique. // ProxyBalancer defines an interface to implement a load balancing technique.
ProxyBalancer interface { ProxyBalancer interface {
AddTarget(*ProxyTarget) bool
RemoveTarget(string) bool
Next() *ProxyTarget Next() *ProxyTarget
} }
commonBalancer struct {
targets []*ProxyTarget
mutex sync.RWMutex
}
// RandomBalancer implements a random load balancing technique.
randomBalancer struct {
*commonBalancer
random *rand.Rand
}
// RoundRobinBalancer implements a round-robin load balancing technique.
roundRobinBalancer struct {
*commonBalancer
i uint32
}
) )
var ( var (
@@ -104,19 +123,61 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
}) })
} }
// Next randomly returns an upstream target. // NewRandomBalancer returns a random proxy balancer.
func (r *RandomBalancer) Next() *ProxyTarget { func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {
if r.random == nil { b := &randomBalancer{commonBalancer: new(commonBalancer)}
r.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) b.targets = targets
return b
}
// NewRoundRobinBalancer returns a round-robin proxy balancer.
func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {
b := &roundRobinBalancer{commonBalancer: new(commonBalancer)}
b.targets = targets
return b
}
// AddTarget adds an upstream target to the list.
func (b *commonBalancer) AddTarget(target *ProxyTarget) bool {
for _, t := range b.targets {
if t.Name == target.Name {
return false
} }
return r.Targets[r.random.Intn(len(r.Targets))] }
b.mutex.Lock()
defer b.mutex.Unlock()
b.targets = append(b.targets, target)
return true
}
// RemoveTarget removes an upstream target from the list.
func (b *commonBalancer) RemoveTarget(name string) bool {
b.mutex.Lock()
defer b.mutex.Unlock()
for i, t := range b.targets {
if t.Name == name {
b.targets = append(b.targets[:i], b.targets[i+1:]...)
return true
}
}
return false
}
// Next randomly returns an upstream target.
func (b *randomBalancer) Next() *ProxyTarget {
if b.random == nil {
b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
}
b.mutex.RLock()
defer b.mutex.RUnlock()
return b.targets[b.random.Intn(len(b.targets))]
} }
// Next returns an upstream target using round-robin technique. // Next returns an upstream target using round-robin technique.
func (r *RoundRobinBalancer) Next() *ProxyTarget { func (b *roundRobinBalancer) Next() *ProxyTarget {
r.i = r.i % uint32(len(r.Targets)) b.i = b.i % uint32(len(b.targets))
t := r.Targets[r.i] t := b.targets[b.i]
atomic.AddUint32(&r.i, 1) atomic.AddUint32(&b.i, 1)
return t return t
} }
@@ -139,6 +200,13 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
if config.Balancer == nil { if config.Balancer == nil {
panic("echo: proxy middleware requires balancer") panic("echo: proxy middleware requires balancer")
} }
config.rewriteRegex = map[*regexp.Regexp]string{}
// Initialize
for k, v := range config.Rewrite {
k = strings.Replace(k, "*", "(\\S*)", -1)
config.rewriteRegex[regexp.MustCompile(k)] = v
}
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) { return func(c echo.Context) (err error) {
@@ -150,6 +218,14 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
res := c.Response() res := c.Response()
tgt := config.Balancer.Next() tgt := config.Balancer.Next()
// Rewrite
for k, v := range config.rewriteRegex {
replacer := captureTokens(k, req.URL.Path)
if replacer != nil {
req.URL.Path = replacer.Replace(v)
}
}
// Fix header // Fix header
if req.Header.Get(echo.HeaderXRealIP) == "" { if req.Header.Get(echo.HeaderXRealIP) == "" {
req.Header.Set(echo.HeaderXRealIP, c.RealIP()) req.Header.Set(echo.HeaderXRealIP, c.RealIP())

View File

@@ -15,16 +15,16 @@ type (
// Size of the stack to be printed. // Size of the stack to be printed.
// Optional. Default value 4KB. // Optional. Default value 4KB.
StackSize int `json:"stack_size"` StackSize int `yaml:"stack_size"`
// DisableStackAll disables formatting stack traces of all other goroutines // DisableStackAll disables formatting stack traces of all other goroutines
// into buffer after the trace for the current goroutine. // into buffer after the trace for the current goroutine.
// Optional. Default value false. // Optional. Default value false.
DisableStackAll bool `json:"disable_stack_all"` DisableStackAll bool `yaml:"disable_stack_all"`
// DisablePrintStack disables printing stack trace. // DisablePrintStack disables printing stack trace.
// Optional. Default value as false. // Optional. Default value as false.
DisablePrintStack bool `json:"disable_print_stack"` DisablePrintStack bool `yaml:"disable_print_stack"`
} }
) )

View File

@@ -6,29 +6,28 @@ import (
"github.com/labstack/echo" "github.com/labstack/echo"
) )
type ( // RedirectConfig defines the config for Redirect middleware.
// RedirectConfig defines the config for Redirect middleware. type RedirectConfig struct {
RedirectConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper
// Status code to be used when redirecting the request. // Status code to be used when redirecting the request.
// Optional. Default value http.StatusMovedPermanently. // Optional. Default value http.StatusMovedPermanently.
Code int `json:"code"` Code int `yaml:"code"`
} }
)
const ( // redirectLogic represents a function that given a scheme, host and uri
www = "www" // can both: 1) determine if redirect is needed (will set ok accordingly) and
) // 2) return the appropriate redirect url.
type redirectLogic func(scheme, host, uri string) (ok bool, url string)
var ( const www = "www"
// DefaultRedirectConfig is the default Redirect middleware config.
DefaultRedirectConfig = RedirectConfig{ // DefaultRedirectConfig is the default Redirect middleware config.
var DefaultRedirectConfig = RedirectConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
Code: http.StatusMovedPermanently, Code: http.StatusMovedPermanently,
} }
)
// HTTPSRedirect redirects http requests to https. // HTTPSRedirect redirects http requests to https.
// For example, http://labstack.com will be redirect to https://labstack.com. // For example, http://labstack.com will be redirect to https://labstack.com.
@@ -41,29 +40,12 @@ func HTTPSRedirect() echo.MiddlewareFunc {
// HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSRedirect()`. // See `HTTPSRedirect()`.
func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https"; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = "https://" + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() {
return c.Redirect(config.Code, "https://"+host+uri)
}
return next(c)
}
} }
return
})
} }
// HTTPSWWWRedirect redirects http requests to https www. // HTTPSWWWRedirect redirects http requests to https www.
@@ -77,29 +59,12 @@ func HTTPSWWWRedirect() echo.MiddlewareFunc {
// HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSWWWRedirect()`. // See `HTTPSWWWRedirect()`.
func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https" && host[:3] != www; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = "https://www." + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() && host[:3] != www {
return c.Redirect(config.Code, "https://www."+host+uri)
}
return next(c)
}
} }
return
})
} }
// HTTPSNonWWWRedirect redirects http requests to https non www. // HTTPSNonWWWRedirect redirects http requests to https non www.
@@ -113,32 +78,15 @@ func HTTPSNonWWWRedirect() echo.MiddlewareFunc {
// HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSNonWWWRedirect()`. // See `HTTPSNonWWWRedirect()`.
func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https"; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() {
if host[:3] == www { if host[:3] == www {
return c.Redirect(config.Code, "https://"+host[4:]+uri) host = host[4:]
}
return c.Redirect(config.Code, "https://"+host+uri)
}
return next(c)
} }
url = "https://" + host + uri
} }
return
})
} }
// WWWRedirect redirects non www requests to www. // WWWRedirect redirects non www requests to www.
@@ -152,30 +100,12 @@ func WWWRedirect() echo.MiddlewareFunc {
// WWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // WWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `WWWRedirect()`. // See `WWWRedirect()`.
func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = host[:3] != www; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = scheme + "://www." + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
scheme := c.Scheme()
host := req.Host
if host[:3] != www {
uri := req.RequestURI
return c.Redirect(config.Code, scheme+"://www."+host+uri)
}
return next(c)
}
} }
return
})
} }
// NonWWWRedirect redirects www requests to non www. // NonWWWRedirect redirects www requests to non www.
@@ -189,6 +119,15 @@ func NonWWWRedirect() echo.MiddlewareFunc {
// NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `NonWWWRedirect()`. // See `NonWWWRedirect()`.
func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if ok = host[:3] == www; ok {
url = scheme + "://" + host[4:] + uri
}
return
})
}
func redirect(config RedirectConfig, cb redirectLogic) echo.MiddlewareFunc {
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultTrailingSlashConfig.Skipper config.Skipper = DefaultTrailingSlashConfig.Skipper
} }
@@ -202,13 +141,12 @@ func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
return next(c) return next(c)
} }
req := c.Request() req, scheme := c.Request(), c.Scheme()
scheme := c.Scheme()
host := req.Host host := req.Host
if host[:3] == www { if ok, url := cb(scheme, host, req.RequestURI); ok {
uri := req.RequestURI return c.Redirect(config.Code, url)
return c.Redirect(config.Code, scheme+"://"+host[4:]+uri)
} }
return next(c) return next(c)
} }
} }

83
vendor/github.com/labstack/echo/middleware/rewrite.go generated vendored Normal file
View File

@@ -0,0 +1,83 @@
package middleware
import (
"regexp"
"strings"
"github.com/labstack/echo"
)
type (
// RewriteConfig defines the config for Rewrite middleware.
RewriteConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Rules defines the URL path rewrite rules. The values captured in asterisk can be
// retrieved by index e.g. $1, $2 and so on.
// Example:
// "/old": "/new",
// "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2",
// Required.
Rules map[string]string `yaml:"rules"`
rulesRegex map[*regexp.Regexp]string
}
)
var (
// DefaultRewriteConfig is the default Rewrite middleware config.
DefaultRewriteConfig = RewriteConfig{
Skipper: DefaultSkipper,
}
)
// Rewrite returns a Rewrite middleware.
//
// Rewrite middleware rewrites the URL path based on the provided rules.
func Rewrite(rules map[string]string) echo.MiddlewareFunc {
c := DefaultRewriteConfig
c.Rules = rules
return RewriteWithConfig(c)
}
// RewriteWithConfig returns a Rewrite middleware with config.
// See: `Rewrite()`.
func RewriteWithConfig(config RewriteConfig) echo.MiddlewareFunc {
// Defaults
if config.Rules == nil {
panic("echo: rewrite middleware requires url path rewrite rules")
}
if config.Skipper == nil {
config.Skipper = DefaultBodyDumpConfig.Skipper
}
config.rulesRegex = map[*regexp.Regexp]string{}
// Initialize
for k, v := range config.Rules {
k = strings.Replace(k, "*", "(\\S*)", -1)
config.rulesRegex[regexp.MustCompile(k)] = v
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
// Rewrite
for k, v := range config.rulesRegex {
replacer := captureTokens(k, req.URL.Path)
if replacer != nil {
req.URL.Path = replacer.Replace(v)
}
}
return next(c)
}
}
}

View File

@@ -15,12 +15,12 @@ type (
// XSSProtection provides protection against cross-site scripting attack (XSS) // XSSProtection provides protection against cross-site scripting attack (XSS)
// by setting the `X-XSS-Protection` header. // by setting the `X-XSS-Protection` header.
// Optional. Default value "1; mode=block". // Optional. Default value "1; mode=block".
XSSProtection string `json:"xss_protection"` XSSProtection string `yaml:"xss_protection"`
// ContentTypeNosniff provides protection against overriding Content-Type // ContentTypeNosniff provides protection against overriding Content-Type
// header by setting the `X-Content-Type-Options` header. // header by setting the `X-Content-Type-Options` header.
// Optional. Default value "nosniff". // Optional. Default value "nosniff".
ContentTypeNosniff string `json:"content_type_nosniff"` ContentTypeNosniff string `yaml:"content_type_nosniff"`
// XFrameOptions can be used to indicate whether or not a browser should // XFrameOptions can be used to indicate whether or not a browser should
// be allowed to render a page in a <frame>, <iframe> or <object> . // be allowed to render a page in a <frame>, <iframe> or <object> .
@@ -32,27 +32,27 @@ type (
// - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself. // - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself.
// - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so. // - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so.
// - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin. // - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin.
XFrameOptions string `json:"x_frame_options"` XFrameOptions string `yaml:"x_frame_options"`
// HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how // HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how
// long (in seconds) browsers should remember that this site is only to // long (in seconds) browsers should remember that this site is only to
// be accessed using HTTPS. This reduces your exposure to some SSL-stripping // be accessed using HTTPS. This reduces your exposure to some SSL-stripping
// man-in-the-middle (MITM) attacks. // man-in-the-middle (MITM) attacks.
// Optional. Default value 0. // Optional. Default value 0.
HSTSMaxAge int `json:"hsts_max_age"` HSTSMaxAge int `yaml:"hsts_max_age"`
// HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security` // HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security`
// header, excluding all subdomains from security policy. It has no effect // header, excluding all subdomains from security policy. It has no effect
// unless HSTSMaxAge is set to a non-zero value. // unless HSTSMaxAge is set to a non-zero value.
// Optional. Default value false. // Optional. Default value false.
HSTSExcludeSubdomains bool `json:"hsts_exclude_subdomains"` HSTSExcludeSubdomains bool `yaml:"hsts_exclude_subdomains"`
// ContentSecurityPolicy sets the `Content-Security-Policy` header providing // ContentSecurityPolicy sets the `Content-Security-Policy` header providing
// security against cross-site scripting (XSS), clickjacking and other code // security against cross-site scripting (XSS), clickjacking and other code
// injection attacks resulting from execution of malicious content in the // injection attacks resulting from execution of malicious content in the
// trusted web page context. // trusted web page context.
// Optional. Default value "". // Optional. Default value "".
ContentSecurityPolicy string `json:"content_security_policy"` ContentSecurityPolicy string `yaml:"content_security_policy"`
} }
) )

View File

@@ -12,7 +12,7 @@ type (
// Status code to be used when redirecting the request. // Status code to be used when redirecting the request.
// Optional, but when provided the request is redirected using this code. // Optional, but when provided the request is redirected using this code.
RedirectCode int `json:"redirect_code"` RedirectCode int `yaml:"redirect_code"`
} }
) )

View File

@@ -19,20 +19,20 @@ type (
// Root directory from where the static content is served. // Root directory from where the static content is served.
// Required. // Required.
Root string `json:"root"` Root string `yaml:"root"`
// Index file for serving a directory. // Index file for serving a directory.
// Optional. Default value "index.html". // Optional. Default value "index.html".
Index string `json:"index"` Index string `yaml:"index"`
// Enable HTML5 mode by forwarding all not-found requests to root so that // Enable HTML5 mode by forwarding all not-found requests to root so that
// SPA (single-page application) can handle the routing. // SPA (single-page application) can handle the routing.
// Optional. Default value false. // Optional. Default value false.
HTML5 bool `json:"html5"` HTML5 bool `yaml:"html5"`
// Enable directory browsing. // Enable directory browsing.
// Optional. Default value false. // Optional. Default value false.
Browse bool `json:"browse"` Browse bool `yaml:"browse"`
} }
) )

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"net" "net"
"net/http" "net/http"
"strconv"
) )
type ( type (
@@ -12,7 +13,9 @@ type (
// See: https://golang.org/pkg/net/http/#ResponseWriter // See: https://golang.org/pkg/net/http/#ResponseWriter
Response struct { Response struct {
echo *Echo echo *Echo
contentLength int64
beforeFuncs []func() beforeFuncs []func()
afterFuncs []func()
Writer http.ResponseWriter Writer http.ResponseWriter
Status int Status int
Size int64 Size int64
@@ -40,6 +43,12 @@ func (r *Response) Before(fn func()) {
r.beforeFuncs = append(r.beforeFuncs, fn) r.beforeFuncs = append(r.beforeFuncs, fn)
} }
// After registers a function which is called just after the response is written.
// If the `Content-Length` is unknown, none of the after function is executed.
func (r *Response) After(fn func()) {
r.afterFuncs = append(r.afterFuncs, fn)
}
// WriteHeader sends an HTTP response header with status code. If WriteHeader is // WriteHeader sends an HTTP response header with status code. If WriteHeader is
// not called explicitly, the first call to Write will trigger an implicit // not called explicitly, the first call to Write will trigger an implicit
// WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly // WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly
@@ -55,6 +64,7 @@ func (r *Response) WriteHeader(code int) {
r.Status = code r.Status = code
r.Writer.WriteHeader(code) r.Writer.WriteHeader(code)
r.Committed = true r.Committed = true
r.contentLength, _ = strconv.ParseInt(r.Header().Get(HeaderContentLength), 10, 0)
} }
// Write writes the data to the connection as part of an HTTP reply. // Write writes the data to the connection as part of an HTTP reply.
@@ -64,6 +74,11 @@ func (r *Response) Write(b []byte) (n int, err error) {
} }
n, err = r.Writer.Write(b) n, err = r.Writer.Write(b)
r.Size += int64(n) r.Size += int64(n)
if r.Size == r.contentLength {
for _, fn := range r.afterFuncs {
fn()
}
}
return return
} }
@@ -91,6 +106,9 @@ func (r *Response) CloseNotify() <-chan bool {
} }
func (r *Response) reset(w http.ResponseWriter) { func (r *Response) reset(w http.ResponseWriter) {
r.contentLength = 0
r.beforeFuncs = nil
r.afterFuncs = nil
r.Writer = w r.Writer = w
r.Size = 0 r.Size = 0
r.Status = http.StatusOK r.Status = http.StatusOK

View File

@@ -1,7 +1,5 @@
package echo package echo
import "strings"
type ( type (
// Router is the registry of all registered routes for an `Echo` instance for // Router is the registry of all registered routes for an `Echo` instance for
// request matching and URL path parameter parsing. // request matching and URL path parameter parsing.
@@ -175,12 +173,6 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
if len(cn.pnames) == 0 { // Issue #729 if len(cn.pnames) == 0 { // Issue #729
cn.pnames = pnames cn.pnames = pnames
} }
for i, n := range pnames {
// Param name aliases
if i < len(cn.pnames) && !strings.Contains(cn.pnames[i], n) {
cn.pnames[i] += "," + n
}
}
} }
} }
return return

View File

@@ -0,0 +1,10 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import "github.com/mattermost/mattermost-server/model"
type AccountMigrationInterface interface {
MigrateToLdap(fromAuthService string, forignUserFieldNameToMatch string, force bool) *model.AppError
}

View File

@@ -4,7 +4,7 @@
package einterfaces package einterfaces
import ( import (
"github.com/mattermost/platform/model" "github.com/mattermost/mattermost-server/model"
"mime/multipart" "mime/multipart"
) )
@@ -12,13 +12,3 @@ type BrandInterface interface {
SaveBrandImage(*multipart.FileHeader) *model.AppError SaveBrandImage(*multipart.FileHeader) *model.AppError
GetBrandImage() ([]byte, *model.AppError) GetBrandImage() ([]byte, *model.AppError)
} }
var theBrandInterface BrandInterface
func RegisterBrandInterface(newInterface BrandInterface) {
theBrandInterface = newInterface
}
func GetBrandInterface() BrandInterface {
return theBrandInterface
}

View File

@@ -4,7 +4,7 @@
package einterfaces package einterfaces
import ( import (
"github.com/mattermost/platform/model" "github.com/mattermost/mattermost-server/model"
) )
type ClusterMessageHandler func(msg *model.ClusterMessage) type ClusterMessageHandler func(msg *model.ClusterMessage)
@@ -14,6 +14,8 @@ type ClusterInterface interface {
StopInterNodeCommunication() StopInterNodeCommunication()
RegisterClusterMessageHandler(event string, crm ClusterMessageHandler) RegisterClusterMessageHandler(event string, crm ClusterMessageHandler)
GetClusterId() string GetClusterId() string
IsLeader() bool
GetMyClusterInfo() *model.ClusterInfo
GetClusterInfos() []*model.ClusterInfo GetClusterInfos() []*model.ClusterInfo
SendClusterMessage(cluster *model.ClusterMessage) SendClusterMessage(cluster *model.ClusterMessage)
NotifyMsg(buf []byte) NotifyMsg(buf []byte)
@@ -21,13 +23,3 @@ type ClusterInterface interface {
GetLogs(page, perPage int) ([]string, *model.AppError) GetLogs(page, perPage int) ([]string, *model.AppError)
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
} }
var theClusterInterface ClusterInterface
func RegisterClusterInterface(newInterface ClusterInterface) {
theClusterInterface = newInterface
}
func GetClusterInterface() ClusterInterface {
return theClusterInterface
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/mattermost-server/model"
)
type ComplianceInterface interface {
StartComplianceDailyJob()
RunComplianceJob(job *model.Compliance) *model.AppError
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/mattermost-server/model"
)
type DataRetentionInterface interface {
GetPolicy() (*model.DataRetentionPolicy, *model.AppError)
}

View File

@@ -3,7 +3,11 @@
package einterfaces package einterfaces
import "github.com/mattermost/platform/model" import (
"time"
"github.com/mattermost/mattermost-server/model"
)
type ElasticsearchInterface interface { type ElasticsearchInterface interface {
Start() *model.AppError Start() *model.AppError
@@ -12,14 +16,5 @@ type ElasticsearchInterface interface {
DeletePost(post *model.Post) *model.AppError DeletePost(post *model.Post) *model.AppError
TestConfig(cfg *model.Config) *model.AppError TestConfig(cfg *model.Config) *model.AppError
PurgeIndexes() *model.AppError PurgeIndexes() *model.AppError
} DataRetentionDeleteIndexes(cutoff time.Time) *model.AppError
var theElasticsearchInterface ElasticsearchInterface
func RegisterElasticsearchInterface(newInterface ElasticsearchInterface) {
theElasticsearchInterface = newInterface
}
func GetElasticsearchInterface() ElasticsearchInterface {
return theElasticsearchInterface
} }

View File

@@ -0,0 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/mattermost-server/model"
)
type EmojiInterface interface {
CanUserCreateEmoji(string, []*model.TeamMember) bool
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package jobs
import (
"github.com/mattermost/mattermost-server/model"
)
type DataRetentionJobInterface interface {
MakeWorker() model.Worker
MakeScheduler() model.Scheduler
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package jobs
import (
"github.com/mattermost/mattermost-server/model"
)
type ElasticsearchIndexerInterface interface {
MakeWorker() model.Worker
}
type ElasticsearchAggregatorInterface interface {
MakeWorker() model.Worker
MakeScheduler() model.Scheduler
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package jobs
import (
"github.com/mattermost/mattermost-server/model"
)
type LdapSyncInterface interface {
MakeWorker() model.Worker
MakeScheduler() model.Scheduler
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package jobs
import (
"github.com/mattermost/mattermost-server/model"
)
type MessageExportJobInterface interface {
MakeWorker() model.Worker
MakeScheduler() model.Scheduler
}

View File

@@ -4,28 +4,22 @@
package einterfaces package einterfaces
import ( import (
"github.com/mattermost/platform/model" "github.com/go-ldap/ldap"
"github.com/mattermost/mattermost-server/model"
) )
type LdapInterface interface { type LdapInterface interface {
DoLogin(id string, password string) (*model.User, *model.AppError) DoLogin(id string, password string) (*model.User, *model.AppError)
GetUser(id string) (*model.User, *model.AppError) GetUser(id string) (*model.User, *model.AppError)
GetUserAttributes(id string, attributes []string) (map[string]string, *model.AppError)
CheckPassword(id string, password string) *model.AppError CheckPassword(id string, password string) *model.AppError
SwitchToLdap(userId, ldapId, ldapPassword string) *model.AppError SwitchToLdap(userId, ldapId, ldapPassword string) *model.AppError
ValidateFilter(filter string) *model.AppError ValidateFilter(filter string) *model.AppError
Syncronize() *model.AppError StartSynchronizeJob(waitForJobToFinish bool) (*model.Job, *model.AppError)
StartLdapSyncJob()
SyncNow()
RunTest() *model.AppError RunTest() *model.AppError
GetAllLdapUsers() ([]*model.User, *model.AppError) GetAllLdapUsers() ([]*model.User, *model.AppError)
} UserFromLdapUser(ldapUser *ldap.Entry) *model.User
UserHasUpdateFromLdap(existingUser *model.User, currentLdapUser *model.User) bool
var theLdapInterface LdapInterface UpdateLocalLdapUser(existingUser *model.User, currentLdapUser *model.User) *model.User
func RegisterLdapInterface(newInterface LdapInterface) {
theLdapInterface = newInterface
}
func GetLdapInterface() LdapInterface {
return theLdapInterface
} }

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"context"
"github.com/mattermost/mattermost-server/model"
)
type MessageExportInterface interface {
StartSynchronizeJob(ctx context.Context, exportFromTimestamp int64) (*model.Job, *model.AppError)
}

View File

@@ -37,14 +37,7 @@ type MetricsInterface interface {
AddMemCacheHitCounter(cacheName string, amount float64) AddMemCacheHitCounter(cacheName string, amount float64)
AddMemCacheMissCounter(cacheName string, amount float64) AddMemCacheMissCounter(cacheName string, amount float64)
}
var theMetricsInterface MetricsInterface IncrementPostsSearchCounter()
ObservePostsSearchDuration(elapsed float64)
func RegisterMetricsInterface(newInterface MetricsInterface) {
theMetricsInterface = newInterface
}
func GetMetricsInterface() MetricsInterface {
return theMetricsInterface
} }

View File

@@ -4,7 +4,7 @@
package einterfaces package einterfaces
import ( import (
"github.com/mattermost/platform/model" "github.com/mattermost/mattermost-server/model"
) )
type MfaInterface interface { type MfaInterface interface {
@@ -13,13 +13,3 @@ type MfaInterface interface {
Deactivate(userId string) *model.AppError Deactivate(userId string) *model.AppError
ValidateToken(secret, token string) (bool, *model.AppError) ValidateToken(secret, token string) (bool, *model.AppError)
} }
var theMfaInterface MfaInterface
func RegisterMfaInterface(newInterface MfaInterface) {
theMfaInterface = newInterface
}
func GetMfaInterface() MfaInterface {
return theMfaInterface
}

View File

@@ -4,7 +4,7 @@
package einterfaces package einterfaces
import ( import (
"github.com/mattermost/platform/model" "github.com/mattermost/mattermost-server/model"
"io" "io"
) )

View File

@@ -4,7 +4,7 @@
package einterfaces package einterfaces
import ( import (
"github.com/mattermost/platform/model" "github.com/mattermost/mattermost-server/model"
) )
type SamlInterface interface { type SamlInterface interface {
@@ -13,13 +13,3 @@ type SamlInterface interface {
DoLogin(encodedXML string, relayState map[string]string) (*model.User, *model.AppError) DoLogin(encodedXML string, relayState map[string]string) (*model.User, *model.AppError)
GetMetadata() (string, *model.AppError) GetMetadata() (string, *model.AppError)
} }
var theSamlInterface SamlInterface
func RegisterSamlInterface(newInterface SamlInterface) {
theSamlInterface = newInterface
}
func GetSamlInterface() SamlInterface {
return theSamlInterface
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
"net/http"
)
const (
ACCESS_TOKEN_GRANT_TYPE = "authorization_code"
ACCESS_TOKEN_TYPE = "bearer"
REFRESH_TOKEN_GRANT_TYPE = "refresh_token"
)
type AccessData struct {
ClientId string `json:"client_id"`
UserId string `json:"user_id"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
RedirectUri string `json:"redirect_uri"`
ExpiresAt int64 `json:"expires_at"`
Scope string `json:"scope"`
}
type AccessResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int32 `json:"expires_in"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
}
// IsValid validates the AccessData and returns an error if it isn't configured
// correctly.
func (ad *AccessData) IsValid() *AppError {
if len(ad.ClientId) == 0 || len(ad.ClientId) > 26 {
return NewAppError("AccessData.IsValid", "model.access.is_valid.client_id.app_error", nil, "", http.StatusBadRequest)
}
if len(ad.UserId) == 0 || len(ad.UserId) > 26 {
return NewAppError("AccessData.IsValid", "model.access.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if len(ad.Token) != 26 {
return NewAppError("AccessData.IsValid", "model.access.is_valid.access_token.app_error", nil, "", http.StatusBadRequest)
}
if len(ad.RefreshToken) > 26 {
return NewAppError("AccessData.IsValid", "model.access.is_valid.refresh_token.app_error", nil, "", http.StatusBadRequest)
}
if len(ad.RedirectUri) == 0 || len(ad.RedirectUri) > 256 || !IsValidHttpUrl(ad.RedirectUri) {
return NewAppError("AccessData.IsValid", "model.access.is_valid.redirect_uri.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (me *AccessData) IsExpired() bool {
if me.ExpiresAt <= 0 {
return false
}
if GetMillis() > me.ExpiresAt {
return true
}
return false
}
func (ad *AccessData) ToJson() string {
b, _ := json.Marshal(ad)
return string(b)
}
func AccessDataFromJson(data io.Reader) *AccessData {
var ad *AccessData
json.NewDecoder(data).Decode(&ad)
return ad
}
func (ar *AccessResponse) ToJson() string {
b, _ := json.Marshal(ar)
return string(b)
}
func AccessResponseFromJson(data io.Reader) *AccessResponse {
var ar *AccessResponse
json.NewDecoder(data).Decode(&ar)
return ar
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type AnalyticsRow struct {
Name string `json:"name"`
Value float64 `json:"value"`
}
type AnalyticsRows []*AnalyticsRow
func (me *AnalyticsRow) ToJson() string {
b, _ := json.Marshal(me)
return string(b)
}
func AnalyticsRowFromJson(data io.Reader) *AnalyticsRow {
var me *AnalyticsRow
json.NewDecoder(data).Decode(&me)
return me
}
func (me AnalyticsRows) ToJson() string {
if b, err := json.Marshal(me); err != nil {
return "[]"
} else {
return string(b)
}
}
func AnalyticsRowsFromJson(data io.Reader) AnalyticsRows {
var me AnalyticsRows
json.NewDecoder(data).Decode(&me)
return me
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type Audit struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UserId string `json:"user_id"`
Action string `json:"action"`
ExtraInfo string `json:"extra_info"`
IpAddress string `json:"ip_address"`
SessionId string `json:"session_id"`
}
func (o *Audit) ToJson() string {
b, _ := json.Marshal(o)
return string(b)
}
func AuditFromJson(data io.Reader) *Audit {
var o *Audit
json.NewDecoder(data).Decode(&o)
return o
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type Audits []Audit
func (o Audits) Etag() string {
if len(o) > 0 {
// the first in the list is always the most current
return Etag(o[0].CreateAt)
} else {
return ""
}
}
func (o Audits) ToJson() string {
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
}
func AuditsFromJson(data io.Reader) Audits {
var o Audits
json.NewDecoder(data).Decode(&o)
return o
}

View File

@@ -0,0 +1,522 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
type Permission struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
type Role struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
}
var PERMISSION_INVITE_USER *Permission
var PERMISSION_ADD_USER_TO_TEAM *Permission
var PERMISSION_USE_SLASH_COMMANDS *Permission
var PERMISSION_MANAGE_SLASH_COMMANDS *Permission
var PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS *Permission
var PERMISSION_CREATE_PUBLIC_CHANNEL *Permission
var PERMISSION_CREATE_PRIVATE_CHANNEL *Permission
var PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS *Permission
var PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS *Permission
var PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE *Permission
var PERMISSION_MANAGE_ROLES *Permission
var PERMISSION_MANAGE_TEAM_ROLES *Permission
var PERMISSION_MANAGE_CHANNEL_ROLES *Permission
var PERMISSION_CREATE_DIRECT_CHANNEL *Permission
var PERMISSION_CREATE_GROUP_CHANNEL *Permission
var PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES *Permission
var PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES *Permission
var PERMISSION_LIST_TEAM_CHANNELS *Permission
var PERMISSION_JOIN_PUBLIC_CHANNELS *Permission
var PERMISSION_DELETE_PUBLIC_CHANNEL *Permission
var PERMISSION_DELETE_PRIVATE_CHANNEL *Permission
var PERMISSION_EDIT_OTHER_USERS *Permission
var PERMISSION_READ_CHANNEL *Permission
var PERMISSION_READ_PUBLIC_CHANNEL *Permission
var PERMISSION_PERMANENT_DELETE_USER *Permission
var PERMISSION_UPLOAD_FILE *Permission
var PERMISSION_GET_PUBLIC_LINK *Permission
var PERMISSION_MANAGE_WEBHOOKS *Permission
var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission
var PERMISSION_MANAGE_OAUTH *Permission
var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
var PERMISSION_CREATE_POST *Permission
var PERMISSION_CREATE_POST_PUBLIC *Permission
var PERMISSION_EDIT_POST *Permission
var PERMISSION_EDIT_OTHERS_POSTS *Permission
var PERMISSION_DELETE_POST *Permission
var PERMISSION_DELETE_OTHERS_POSTS *Permission
var PERMISSION_REMOVE_USER_FROM_TEAM *Permission
var PERMISSION_CREATE_TEAM *Permission
var PERMISSION_MANAGE_TEAM *Permission
var PERMISSION_IMPORT_TEAM *Permission
var PERMISSION_VIEW_TEAM *Permission
var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission
var PERMISSION_MANAGE_JOBS *Permission
var PERMISSION_CREATE_USER_ACCESS_TOKEN *Permission
var PERMISSION_READ_USER_ACCESS_TOKEN *Permission
var PERMISSION_REVOKE_USER_ACCESS_TOKEN *Permission
// General permission that encompases all system admin functions
// in the future this could be broken up to allow access to some
// admin functions but not others
var PERMISSION_MANAGE_SYSTEM *Permission
const (
SYSTEM_USER_ROLE_ID = "system_user"
SYSTEM_ADMIN_ROLE_ID = "system_admin"
SYSTEM_POST_ALL_ROLE_ID = "system_post_all"
SYSTEM_POST_ALL_PUBLIC_ROLE_ID = "system_post_all_public"
SYSTEM_USER_ACCESS_TOKEN_ROLE_ID = "system_user_access_token"
TEAM_USER_ROLE_ID = "team_user"
TEAM_ADMIN_ROLE_ID = "team_admin"
TEAM_POST_ALL_ROLE_ID = "team_post_all"
TEAM_POST_ALL_PUBLIC_ROLE_ID = "team_post_all_public"
CHANNEL_USER_ROLE_ID = "channel_user"
CHANNEL_ADMIN_ROLE_ID = "channel_admin"
CHANNEL_GUEST_ROLE_ID = "guest"
)
func initializePermissions() {
PERMISSION_INVITE_USER = &Permission{
"invite_user",
"authentication.permissions.team_invite_user.name",
"authentication.permissions.team_invite_user.description",
}
PERMISSION_ADD_USER_TO_TEAM = &Permission{
"add_user_to_team",
"authentication.permissions.add_user_to_team.name",
"authentication.permissions.add_user_to_team.description",
}
PERMISSION_USE_SLASH_COMMANDS = &Permission{
"use_slash_commands",
"authentication.permissions.team_use_slash_commands.name",
"authentication.permissions.team_use_slash_commands.description",
}
PERMISSION_MANAGE_SLASH_COMMANDS = &Permission{
"manage_slash_commands",
"authentication.permissions.manage_slash_commands.name",
"authentication.permissions.manage_slash_commands.description",
}
PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS = &Permission{
"manage_others_slash_commands",
"authentication.permissions.manage_others_slash_commands.name",
"authentication.permissions.manage_others_slash_commands.description",
}
PERMISSION_CREATE_PUBLIC_CHANNEL = &Permission{
"create_public_channel",
"authentication.permissions.create_public_channel.name",
"authentication.permissions.create_public_channel.description",
}
PERMISSION_CREATE_PRIVATE_CHANNEL = &Permission{
"create_private_channel",
"authentication.permissions.create_private_channel.name",
"authentication.permissions.create_private_channel.description",
}
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS = &Permission{
"manage_public_channel_members",
"authentication.permissions.manage_public_channel_members.name",
"authentication.permissions.manage_public_channel_members.description",
}
PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS = &Permission{
"manage_private_channel_members",
"authentication.permissions.manage_private_channel_members.name",
"authentication.permissions.manage_private_channel_members.description",
}
PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE = &Permission{
"assign_system_admin_role",
"authentication.permissions.assign_system_admin_role.name",
"authentication.permissions.assign_system_admin_role.description",
}
PERMISSION_MANAGE_ROLES = &Permission{
"manage_roles",
"authentication.permissions.manage_roles.name",
"authentication.permissions.manage_roles.description",
}
PERMISSION_MANAGE_TEAM_ROLES = &Permission{
"manage_team_roles",
"authentication.permissions.manage_team_roles.name",
"authentication.permissions.manage_team_roles.description",
}
PERMISSION_MANAGE_CHANNEL_ROLES = &Permission{
"manage_channel_roles",
"authentication.permissions.manage_channel_roles.name",
"authentication.permissions.manage_channel_roles.description",
}
PERMISSION_MANAGE_SYSTEM = &Permission{
"manage_system",
"authentication.permissions.manage_system.name",
"authentication.permissions.manage_system.description",
}
PERMISSION_CREATE_DIRECT_CHANNEL = &Permission{
"create_direct_channel",
"authentication.permissions.create_direct_channel.name",
"authentication.permissions.create_direct_channel.description",
}
PERMISSION_CREATE_GROUP_CHANNEL = &Permission{
"create_group_channel",
"authentication.permissions.create_group_channel.name",
"authentication.permissions.create_group_channel.description",
}
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = &Permission{
"manage__publicchannel_properties",
"authentication.permissions.manage_public_channel_properties.name",
"authentication.permissions.manage_public_channel_properties.description",
}
PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES = &Permission{
"manage_private_channel_properties",
"authentication.permissions.manage_private_channel_properties.name",
"authentication.permissions.manage_private_channel_properties.description",
}
PERMISSION_LIST_TEAM_CHANNELS = &Permission{
"list_team_channels",
"authentication.permissions.list_team_channels.name",
"authentication.permissions.list_team_channels.description",
}
PERMISSION_JOIN_PUBLIC_CHANNELS = &Permission{
"join_public_channels",
"authentication.permissions.join_public_channels.name",
"authentication.permissions.join_public_channels.description",
}
PERMISSION_DELETE_PUBLIC_CHANNEL = &Permission{
"delete_public_channel",
"authentication.permissions.delete_public_channel.name",
"authentication.permissions.delete_public_channel.description",
}
PERMISSION_DELETE_PRIVATE_CHANNEL = &Permission{
"delete_private_channel",
"authentication.permissions.delete_private_channel.name",
"authentication.permissions.delete_private_channel.description",
}
PERMISSION_EDIT_OTHER_USERS = &Permission{
"edit_other_users",
"authentication.permissions.edit_other_users.name",
"authentication.permissions.edit_other_users.description",
}
PERMISSION_READ_CHANNEL = &Permission{
"read_channel",
"authentication.permissions.read_channel.name",
"authentication.permissions.read_channel.description",
}
PERMISSION_READ_PUBLIC_CHANNEL = &Permission{
"read_public_channel",
"authentication.permissions.read_public_channel.name",
"authentication.permissions.read_public_channel.description",
}
PERMISSION_PERMANENT_DELETE_USER = &Permission{
"permanent_delete_user",
"authentication.permissions.permanent_delete_user.name",
"authentication.permissions.permanent_delete_user.description",
}
PERMISSION_UPLOAD_FILE = &Permission{
"upload_file",
"authentication.permissions.upload_file.name",
"authentication.permissions.upload_file.description",
}
PERMISSION_GET_PUBLIC_LINK = &Permission{
"get_public_link",
"authentication.permissions.get_public_link.name",
"authentication.permissions.get_public_link.description",
}
PERMISSION_MANAGE_WEBHOOKS = &Permission{
"manage_webhooks",
"authentication.permissions.manage_webhooks.name",
"authentication.permissions.manage_webhooks.description",
}
PERMISSION_MANAGE_OTHERS_WEBHOOKS = &Permission{
"manage_others_webhooks",
"authentication.permissions.manage_others_webhooks.name",
"authentication.permissions.manage_others_webhooks.description",
}
PERMISSION_MANAGE_OAUTH = &Permission{
"manage_oauth",
"authentication.permissions.manage_oauth.name",
"authentication.permissions.manage_oauth.description",
}
PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH = &Permission{
"manage_sytem_wide_oauth",
"authentication.permissions.manage_sytem_wide_oauth.name",
"authentication.permissions.manage_sytem_wide_oauth.description",
}
PERMISSION_CREATE_POST = &Permission{
"create_post",
"authentication.permissions.create_post.name",
"authentication.permissions.create_post.description",
}
PERMISSION_CREATE_POST_PUBLIC = &Permission{
"create_post_public",
"authentication.permissions.create_post_public.name",
"authentication.permissions.create_post_public.description",
}
PERMISSION_EDIT_POST = &Permission{
"edit_post",
"authentication.permissions.edit_post.name",
"authentication.permissions.edit_post.description",
}
PERMISSION_EDIT_OTHERS_POSTS = &Permission{
"edit_others_posts",
"authentication.permissions.edit_others_posts.name",
"authentication.permissions.edit_others_posts.description",
}
PERMISSION_DELETE_POST = &Permission{
"delete_post",
"authentication.permissions.delete_post.name",
"authentication.permissions.delete_post.description",
}
PERMISSION_DELETE_OTHERS_POSTS = &Permission{
"delete_others_posts",
"authentication.permissions.delete_others_posts.name",
"authentication.permissions.delete_others_posts.description",
}
PERMISSION_REMOVE_USER_FROM_TEAM = &Permission{
"remove_user_from_team",
"authentication.permissions.remove_user_from_team.name",
"authentication.permissions.remove_user_from_team.description",
}
PERMISSION_CREATE_TEAM = &Permission{
"create_team",
"authentication.permissions.create_team.name",
"authentication.permissions.create_team.description",
}
PERMISSION_MANAGE_TEAM = &Permission{
"manage_team",
"authentication.permissions.manage_team.name",
"authentication.permissions.manage_team.description",
}
PERMISSION_IMPORT_TEAM = &Permission{
"import_team",
"authentication.permissions.import_team.name",
"authentication.permissions.import_team.description",
}
PERMISSION_VIEW_TEAM = &Permission{
"view_team",
"authentication.permissions.view_team.name",
"authentication.permissions.view_team.description",
}
PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{
"list_users_without_team",
"authentication.permissions.list_users_without_team.name",
"authentication.permissions.list_users_without_team.description",
}
PERMISSION_CREATE_USER_ACCESS_TOKEN = &Permission{
"create_user_access_token",
"authentication.permissions.create_user_access_token.name",
"authentication.permissions.create_user_access_token.description",
}
PERMISSION_READ_USER_ACCESS_TOKEN = &Permission{
"read_user_access_token",
"authentication.permissions.read_user_access_token.name",
"authentication.permissions.read_user_access_token.description",
}
PERMISSION_REVOKE_USER_ACCESS_TOKEN = &Permission{
"revoke_user_access_token",
"authentication.permissions.revoke_user_access_token.name",
"authentication.permissions.revoke_user_access_token.description",
}
PERMISSION_MANAGE_JOBS = &Permission{
"manage_jobs",
"authentication.permisssions.manage_jobs.name",
"authentication.permisssions.manage_jobs.description",
}
}
var DefaultRoles map[string]*Role
func initializeDefaultRoles() {
DefaultRoles = make(map[string]*Role)
DefaultRoles[CHANNEL_USER_ROLE_ID] = &Role{
"channel_user",
"authentication.roles.channel_user.name",
"authentication.roles.channel_user.description",
[]string{
PERMISSION_READ_CHANNEL.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id,
PERMISSION_UPLOAD_FILE.Id,
PERMISSION_GET_PUBLIC_LINK.Id,
PERMISSION_CREATE_POST.Id,
PERMISSION_EDIT_POST.Id,
PERMISSION_USE_SLASH_COMMANDS.Id,
},
}
DefaultRoles[CHANNEL_ADMIN_ROLE_ID] = &Role{
"channel_admin",
"authentication.roles.channel_admin.name",
"authentication.roles.channel_admin.description",
[]string{
PERMISSION_MANAGE_CHANNEL_ROLES.Id,
},
}
DefaultRoles[CHANNEL_GUEST_ROLE_ID] = &Role{
"guest",
"authentication.roles.global_guest.name",
"authentication.roles.global_guest.description",
[]string{},
}
DefaultRoles[TEAM_USER_ROLE_ID] = &Role{
"team_user",
"authentication.roles.team_user.name",
"authentication.roles.team_user.description",
[]string{
PERMISSION_LIST_TEAM_CHANNELS.Id,
PERMISSION_JOIN_PUBLIC_CHANNELS.Id,
PERMISSION_READ_PUBLIC_CHANNEL.Id,
PERMISSION_VIEW_TEAM.Id,
},
}
DefaultRoles[TEAM_POST_ALL_ROLE_ID] = &Role{
"team_post_all",
"authentication.roles.team_post_all.name",
"authentication.roles.team_post_all.description",
[]string{
PERMISSION_CREATE_POST.Id,
},
}
DefaultRoles[TEAM_POST_ALL_PUBLIC_ROLE_ID] = &Role{
"team_post_all_public",
"authentication.roles.team_post_all_public.name",
"authentication.roles.team_post_all_public.description",
[]string{
PERMISSION_CREATE_POST_PUBLIC.Id,
},
}
DefaultRoles[TEAM_ADMIN_ROLE_ID] = &Role{
"team_admin",
"authentication.roles.team_admin.name",
"authentication.roles.team_admin.description",
[]string{
PERMISSION_EDIT_OTHERS_POSTS.Id,
PERMISSION_REMOVE_USER_FROM_TEAM.Id,
PERMISSION_MANAGE_TEAM.Id,
PERMISSION_IMPORT_TEAM.Id,
PERMISSION_MANAGE_TEAM_ROLES.Id,
PERMISSION_MANAGE_CHANNEL_ROLES.Id,
PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
PERMISSION_MANAGE_SLASH_COMMANDS.Id,
PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id,
PERMISSION_MANAGE_WEBHOOKS.Id,
},
}
DefaultRoles[SYSTEM_USER_ROLE_ID] = &Role{
"system_user",
"authentication.roles.global_user.name",
"authentication.roles.global_user.description",
[]string{
PERMISSION_CREATE_DIRECT_CHANNEL.Id,
PERMISSION_CREATE_GROUP_CHANNEL.Id,
PERMISSION_PERMANENT_DELETE_USER.Id,
},
}
DefaultRoles[SYSTEM_POST_ALL_ROLE_ID] = &Role{
"system_post_all",
"authentication.roles.system_post_all.name",
"authentication.roles.system_post_all.description",
[]string{
PERMISSION_CREATE_POST.Id,
},
}
DefaultRoles[SYSTEM_POST_ALL_PUBLIC_ROLE_ID] = &Role{
"system_post_all_public",
"authentication.roles.system_post_all_public.name",
"authentication.roles.system_post_all_public.description",
[]string{
PERMISSION_CREATE_POST_PUBLIC.Id,
},
}
DefaultRoles[SYSTEM_USER_ACCESS_TOKEN_ROLE_ID] = &Role{
"system_user_access_token",
"authentication.roles.system_user_access_token.name",
"authentication.roles.system_user_access_token.description",
[]string{
PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
},
}
DefaultRoles[SYSTEM_ADMIN_ROLE_ID] = &Role{
"system_admin",
"authentication.roles.global_admin.name",
"authentication.roles.global_admin.description",
// System admins can do anything channel and team admins can do
// plus everything members of teams and channels can do to all teams
// and channels on the system
append(
append(
append(
append(
[]string{
PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id,
PERMISSION_MANAGE_SYSTEM.Id,
PERMISSION_MANAGE_ROLES.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id,
PERMISSION_DELETE_PUBLIC_CHANNEL.Id,
PERMISSION_CREATE_PUBLIC_CHANNEL.Id,
PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id,
PERMISSION_DELETE_PRIVATE_CHANNEL.Id,
PERMISSION_CREATE_PRIVATE_CHANNEL.Id,
PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id,
PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
PERMISSION_EDIT_OTHER_USERS.Id,
PERMISSION_MANAGE_OAUTH.Id,
PERMISSION_INVITE_USER.Id,
PERMISSION_DELETE_POST.Id,
PERMISSION_DELETE_OTHERS_POSTS.Id,
PERMISSION_CREATE_TEAM.Id,
PERMISSION_ADD_USER_TO_TEAM.Id,
PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
PERMISSION_MANAGE_JOBS.Id,
PERMISSION_CREATE_POST_PUBLIC.Id,
PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
},
DefaultRoles[TEAM_USER_ROLE_ID].Permissions...,
),
DefaultRoles[CHANNEL_USER_ROLE_ID].Permissions...,
),
DefaultRoles[TEAM_ADMIN_ROLE_ID].Permissions...,
),
DefaultRoles[CHANNEL_ADMIN_ROLE_ID].Permissions...,
),
}
}
func RoleIdsToString(roles []string) string {
output := ""
for _, role := range roles {
output += role + ", "
}
if output == "" {
return "[<NO ROLES>]"
}
return output[:len(output)-1]
}
func init() {
initializePermissions()
initializeDefaultRoles()
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
"net/http"
)
const (
AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes
AUTHCODE_RESPONSE_TYPE = "code"
DEFAULT_SCOPE = "user"
)
type AuthData struct {
ClientId string `json:"client_id"`
UserId string `json:"user_id"`
Code string `json:"code"`
ExpiresIn int32 `json:"expires_in"`
CreateAt int64 `json:"create_at"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
Scope string `json:"scope"`
}
type AuthorizeRequest struct {
ResponseType string `json:"response_type"`
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
Scope string `json:"scope"`
State string `json:"state"`
}
// IsValid validates the AuthData and returns an error if it isn't configured
// correctly.
func (ad *AuthData) IsValid() *AppError {
if len(ad.ClientId) != 26 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.client_id.app_error", nil, "", http.StatusBadRequest)
}
if len(ad.UserId) != 26 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
if len(ad.Code) == 0 || len(ad.Code) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.auth_code.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
if ad.ExpiresIn == 0 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.expires.app_error", nil, "", http.StatusBadRequest)
}
if ad.CreateAt <= 0 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.create_at.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
if len(ad.RedirectUri) == 0 || len(ad.RedirectUri) > 256 || !IsValidHttpUrl(ad.RedirectUri) {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
if len(ad.State) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
if len(ad.Scope) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.scope.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest)
}
return nil
}
// IsValid validates the AuthorizeRequest and returns an error if it isn't configured
// correctly.
func (ar *AuthorizeRequest) IsValid() *AppError {
if len(ar.ClientId) != 26 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.client_id.app_error", nil, "", http.StatusBadRequest)
}
if len(ar.ResponseType) == 0 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.response_type.app_error", nil, "", http.StatusBadRequest)
}
if len(ar.RedirectUri) == 0 || len(ar.RedirectUri) > 256 || !IsValidHttpUrl(ar.RedirectUri) {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
if len(ar.State) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
if len(ar.Scope) > 128 {
return NewAppError("AuthData.IsValid", "model.authorize.is_valid.scope.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest)
}
return nil
}
func (ad *AuthData) PreSave() {
if ad.ExpiresIn == 0 {
ad.ExpiresIn = AUTHCODE_EXPIRE_TIME
}
if ad.CreateAt == 0 {
ad.CreateAt = GetMillis()
}
if len(ad.Scope) == 0 {
ad.Scope = DEFAULT_SCOPE
}
}
func (ad *AuthData) ToJson() string {
b, _ := json.Marshal(ad)
return string(b)
}
func AuthDataFromJson(data io.Reader) *AuthData {
var ad *AuthData
json.NewDecoder(data).Decode(&ad)
return ad
}
func (ar *AuthorizeRequest) ToJson() string {
b, _ := json.Marshal(ar)
return string(b)
}
func AuthorizeRequestFromJson(data io.Reader) *AuthorizeRequest {
var ar *AuthorizeRequest
json.NewDecoder(data).Decode(&ar)
return ar
}
func (ad *AuthData) IsExpired() bool {
return GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000)
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
func NewBool(b bool) *bool { return &b }
func NewInt(n int) *int { return &n }
func NewInt64(n int64) *int64 { return &n }
func NewString(s string) *string { return &s }

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
type BundleInfo struct {
Path string
Manifest *Manifest
ManifestPath string
ManifestError error
}
// Returns bundle info for the given path. The return value is never nil.
func BundleInfoForPath(path string) *BundleInfo {
m, mpath, err := FindManifest(path)
return &BundleInfo{
Path: path,
Manifest: m,
ManifestPath: mpath,
ManifestError: err,
}
}

View File

@@ -0,0 +1,208 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"sort"
"strings"
"unicode/utf8"
)
const (
CHANNEL_OPEN = "O"
CHANNEL_PRIVATE = "P"
CHANNEL_DIRECT = "D"
CHANNEL_GROUP = "G"
CHANNEL_GROUP_MAX_USERS = 8
CHANNEL_GROUP_MIN_USERS = 3
DEFAULT_CHANNEL = "town-square"
CHANNEL_DISPLAY_NAME_MAX_RUNES = 64
CHANNEL_NAME_MIN_LENGTH = 2
CHANNEL_NAME_MAX_LENGTH = 64
CHANNEL_NAME_UI_MAX_LENGTH = 22
CHANNEL_HEADER_MAX_RUNES = 1024
CHANNEL_PURPOSE_MAX_RUNES = 250
CHANNEL_CACHE_SIZE = 25000
)
type Channel struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
LastPostAt int64 `json:"last_post_at"`
TotalMsgCount int64 `json:"total_msg_count"`
ExtraUpdateAt int64 `json:"extra_update_at"`
CreatorId string `json:"creator_id"`
}
type ChannelPatch struct {
DisplayName *string `json:"display_name"`
Name *string `json:"name"`
Header *string `json:"header"`
Purpose *string `json:"purpose"`
}
func (o *Channel) DeepCopy() *Channel {
copy := *o
return &copy
}
func (o *Channel) ToJson() string {
b, _ := json.Marshal(o)
return string(b)
}
func (o *ChannelPatch) ToJson() string {
b, _ := json.Marshal(o)
return string(b)
}
func ChannelFromJson(data io.Reader) *Channel {
var o *Channel
json.NewDecoder(data).Decode(&o)
return o
}
func ChannelPatchFromJson(data io.Reader) *ChannelPatch {
var o *ChannelPatch
json.NewDecoder(data).Decode(&o)
return o
}
func (o *Channel) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
func (o *Channel) StatsEtag() string {
return Etag(o.Id, o.ExtraUpdateAt)
}
func (o *Channel) IsValid() *AppError {
if len(o.Id) != 26 {
return NewAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if o.CreateAt == 0 {
return NewAppError("Channel.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if o.UpdateAt == 0 {
return NewAppError("Channel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.DisplayName) > CHANNEL_DISPLAY_NAME_MAX_RUNES {
return NewAppError("Channel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !IsValidChannelIdentifier(o.Name) {
return NewAppError("Channel.IsValid", "model.channel.is_valid.2_or_more.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if !(o.Type == CHANNEL_OPEN || o.Type == CHANNEL_PRIVATE || o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP) {
return NewAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Header) > CHANNEL_HEADER_MAX_RUNES {
return NewAppError("Channel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Purpose) > CHANNEL_PURPOSE_MAX_RUNES {
return NewAppError("Channel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+o.Id, http.StatusBadRequest)
}
if len(o.CreatorId) > 26 {
return NewAppError("Channel.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "", http.StatusBadRequest)
}
return nil
}
func (o *Channel) PreSave() {
if o.Id == "" {
o.Id = NewId()
}
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
o.ExtraUpdateAt = o.CreateAt
}
func (o *Channel) PreUpdate() {
o.UpdateAt = GetMillis()
}
func (o *Channel) ExtraUpdated() {
o.ExtraUpdateAt = GetMillis()
}
func (o *Channel) IsGroupOrDirect() bool {
return o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP
}
func (o *Channel) Patch(patch *ChannelPatch) {
if patch.DisplayName != nil {
o.DisplayName = *patch.DisplayName
}
if patch.Name != nil {
o.Name = *patch.Name
}
if patch.Header != nil {
o.Header = *patch.Header
}
if patch.Purpose != nil {
o.Purpose = *patch.Purpose
}
}
func GetDMNameFromIds(userId1, userId2 string) string {
if userId1 > userId2 {
return userId2 + "__" + userId1
} else {
return userId1 + "__" + userId2
}
}
func GetGroupDisplayNameFromUsers(users []*User, truncate bool) string {
usernames := make([]string, len(users))
for index, user := range users {
usernames[index] = user.Username
}
sort.Strings(usernames)
name := strings.Join(usernames, ", ")
if truncate && len(name) > CHANNEL_NAME_MAX_LENGTH {
name = name[:CHANNEL_NAME_MAX_LENGTH]
}
return name
}
func GetGroupNameFromUserIds(userIds []string) string {
sort.Strings(userIds)
h := sha1.New()
for _, id := range userIds {
io.WriteString(h, id)
}
return hex.EncodeToString(h.Sum(nil))
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"crypto/md5"
"encoding/json"
"fmt"
"io"
"sort"
"strconv"
)
type ChannelCounts struct {
Counts map[string]int64 `json:"counts"`
UpdateTimes map[string]int64 `json:"update_times"`
}
func (o *ChannelCounts) Etag() string {
ids := []string{}
for id := range o.Counts {
ids = append(ids, id)
}
sort.Strings(ids)
str := ""
for _, id := range ids {
str += id + strconv.FormatInt(o.Counts[id], 10)
}
md5Counts := fmt.Sprintf("%x", md5.Sum([]byte(str)))
var update int64 = 0
for _, u := range o.UpdateTimes {
if u > update {
update = u
}
}
return Etag(md5Counts, update)
}
func (o *ChannelCounts) ToJson() string {
b, _ := json.Marshal(o)
return string(b)
}
func ChannelCountsFromJson(data io.Reader) *ChannelCounts {
var o *ChannelCounts
json.NewDecoder(data).Decode(&o)
return o
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ChannelData struct {
Channel *Channel `json:"channel"`
Member *ChannelMember `json:"member"`
}
func (o *ChannelData) Etag() string {
var mt int64 = 0
if o.Member != nil {
mt = o.Member.LastUpdateAt
}
return Etag(o.Channel.Id, o.Channel.UpdateAt, o.Channel.LastPostAt, mt)
}
func (o *ChannelData) ToJson() string {
b, _ := json.Marshal(o)
return string(b)
}
func ChannelDataFromJson(data io.Reader) *ChannelData {
var o *ChannelData
json.NewDecoder(data).Decode(&o)
return o
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ChannelList []*Channel
func (o *ChannelList) ToJson() string {
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
}
func (o *ChannelList) Etag() string {
id := "0"
var t int64 = 0
var delta int64 = 0
for _, v := range *o {
if v.LastPostAt > t {
t = v.LastPostAt
id = v.Id
}
if v.UpdateAt > t {
t = v.UpdateAt
id = v.Id
}
}
return Etag(id, t, delta, len(*o))
}
func ChannelListFromJson(data io.Reader) *ChannelList {
var o *ChannelList
json.NewDecoder(data).Decode(&o)
return o
}
func ChannelSliceFromJson(data io.Reader) []*Channel {
var o []*Channel
json.NewDecoder(data).Decode(&o)
return o
}

View File

@@ -0,0 +1,148 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
"net/http"
"strings"
)
const (
CHANNEL_NOTIFY_DEFAULT = "default"
CHANNEL_NOTIFY_ALL = "all"
CHANNEL_NOTIFY_MENTION = "mention"
CHANNEL_NOTIFY_NONE = "none"
CHANNEL_MARK_UNREAD_ALL = "all"
CHANNEL_MARK_UNREAD_MENTION = "mention"
)
type ChannelUnread struct {
TeamId string `json:"team_id"`
ChannelId string `json:"channel_id"`
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
NotifyProps StringMap `json:"-"`
}
type ChannelMember struct {
ChannelId string `json:"channel_id"`
UserId string `json:"user_id"`
Roles string `json:"roles"`
LastViewedAt int64 `json:"last_viewed_at"`
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
NotifyProps StringMap `json:"notify_props"`
LastUpdateAt int64 `json:"last_update_at"`
}
type ChannelMembers []ChannelMember
func (o *ChannelMembers) ToJson() string {
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
}
func (o *ChannelUnread) ToJson() string {
b, _ := json.Marshal(o)
return string(b)
}
func ChannelMembersFromJson(data io.Reader) *ChannelMembers {
var o *ChannelMembers
json.NewDecoder(data).Decode(&o)
return o
}
func ChannelUnreadFromJson(data io.Reader) *ChannelUnread {
var o *ChannelUnread
json.NewDecoder(data).Decode(&o)
return o
}
func (o *ChannelMember) ToJson() string {
b, _ := json.Marshal(o)
return string(b)
}
func ChannelMemberFromJson(data io.Reader) *ChannelMember {
var o *ChannelMember
json.NewDecoder(data).Decode(&o)
return o
}
func (o *ChannelMember) IsValid() *AppError {
if len(o.ChannelId) != 26 {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
}
if len(o.UserId) != 26 {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
}
notifyLevel := o.NotifyProps[DESKTOP_NOTIFY_PROP]
if len(notifyLevel) > 20 || !IsChannelNotifyLevelValid(notifyLevel) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.notify_level.app_error", nil, "notify_level="+notifyLevel, http.StatusBadRequest)
}
markUnreadLevel := o.NotifyProps[MARK_UNREAD_NOTIFY_PROP]
if len(markUnreadLevel) > 20 || !IsChannelMarkUnreadLevelValid(markUnreadLevel) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.unread_level.app_error", nil, "mark_unread_level="+markUnreadLevel, http.StatusBadRequest)
}
if pushLevel, ok := o.NotifyProps[PUSH_NOTIFY_PROP]; ok {
if len(pushLevel) > 20 || !IsChannelNotifyLevelValid(pushLevel) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.push_level.app_error", nil, "push_notification_level="+pushLevel, http.StatusBadRequest)
}
}
if sendEmail, ok := o.NotifyProps[EMAIL_NOTIFY_PROP]; ok {
if len(sendEmail) > 20 || !IsSendEmailValid(sendEmail) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.email_value.app_error", nil, "push_notification_level="+sendEmail, http.StatusBadRequest)
}
}
return nil
}
func (o *ChannelMember) PreSave() {
o.LastUpdateAt = GetMillis()
}
func (o *ChannelMember) PreUpdate() {
o.LastUpdateAt = GetMillis()
}
func (o *ChannelMember) GetRoles() []string {
return strings.Fields(o.Roles)
}
func IsChannelNotifyLevelValid(notifyLevel string) bool {
return notifyLevel == CHANNEL_NOTIFY_DEFAULT ||
notifyLevel == CHANNEL_NOTIFY_ALL ||
notifyLevel == CHANNEL_NOTIFY_MENTION ||
notifyLevel == CHANNEL_NOTIFY_NONE
}
func IsChannelMarkUnreadLevelValid(markUnreadLevel string) bool {
return markUnreadLevel == CHANNEL_MARK_UNREAD_ALL || markUnreadLevel == CHANNEL_MARK_UNREAD_MENTION
}
func IsSendEmailValid(sendEmail string) bool {
return sendEmail == CHANNEL_NOTIFY_DEFAULT || sendEmail == "true" || sendEmail == "false"
}
func GetDefaultChannelNotifyProps() StringMap {
return StringMap{
DESKTOP_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT,
MARK_UNREAD_NOTIFY_PROP: CHANNEL_MARK_UNREAD_ALL,
PUSH_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT,
EMAIL_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT,
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
type ChannelMemberHistory struct {
ChannelId string
UserId string
JoinTime int64
LeaveTime *int64
// these two fields are never set in the database - when we SELECT, we join on Users to get them
UserEmail string `db:"Email"`
Username string
}

Some files were not shown because too many files have changed in this diff Show More