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

Compare commits

...

50 Commits

Author SHA1 Message Date
Wim
50ac0fdd5d Release v1.6.3 2018-01-09 00:21:51 +01:00
Wim
2dfc1fada8 Update for 1.6.3 2018-01-09 00:21:50 +01:00
Wim
979c7dde01 Use upstream again (slack) 2018-01-09 00:21:49 +01:00
Wim
be898b44c3 Update vendor (slack) 2018-01-09 00:07:55 +01:00
4828c43443 Convert received IRC channel names to lowercase. Fixes #329 (#330) 2018-01-09 00:03:32 +01:00
Wim
a5d0197349 Log ConnectionErrorEvent (slack) 2018-01-09 00:03:31 +01:00
Wim
f1ed2ab403 Increase debug logging (slack) 2018-01-09 00:03:30 +01:00
Wim
f8329d8c77 Use a better check to join channel (slack) 2018-01-09 00:03:29 +01:00
Wim
ecf5669e80 Release v1.6.2 2018-01-01 15:10:41 +01:00
Wim
f825636c4f Update for 1.6.2 2018-01-01 15:10:00 +01:00
Wim
ef8fbe1756 Fix regression in mattermost bridge (mattermost). Closes #327 2018-01-01 15:08:20 +01:00
Wim
612acfddff Release v1.6.1 2017-12-26 19:20:44 +01:00
Wim
932b80d4f7 Fix regression. Closes #323 2017-12-26 19:14:36 +01:00
Wim
fac5f69ad2 Release v1.6.0 2017-12-23 00:28:01 +01:00
Wim
97c944bb63 Add RejoinDelay option. Delay to rejoin after channel kick (irc). Closes #322 2017-12-23 00:11:30 +01:00
Wim
d0c4fe78ee Allow specifying maximum download size of media using MediaDownloadSize (slack,telegram,matrix) 2017-12-19 23:44:13 +01:00
Wim
265457b451 Refactor and add MediaDownloadSize to General 2017-12-19 23:15:03 +01:00
Wim
4a4a29c9f6 Fix panic (matrix). Closes #316 2017-12-11 12:25:28 +01:00
Wim
0a91b9e1c9 Fix incorrect forward from text line (telegram) 2017-12-11 12:15:26 +01:00
Wim
f56163295c Remove unreachable code (api) 2017-12-10 15:20:17 +01:00
Wim
d1c87c068b Also use HTML in edited messages (telegram). Closes #315 2017-12-10 15:16:17 +01:00
Wim
fa20761110 Add support for Audio/Voice files (telegram). Closes #314 2017-12-10 15:08:23 +01:00
Wim
e4a0e0a0e9 Add support for forwarded messages. Closes #313 2017-12-10 14:52:29 +01:00
Wim
d30ae19e2a Add (simple, one listener) long-polling support (api). Closes #307 2017-12-07 23:48:44 +01:00
Wim
5c919e6bff Update vendor labstack/echo 2017-12-07 23:00:56 +01:00
Wim
434393d1c3 Update README 2017-12-07 22:30:17 +01:00
Wim
af9aa5d7cb Update changelog 2017-12-07 22:27:17 +01:00
Wim
05eb75442a Split on UTF-8 for MessageSplit (irc). Closes #308 2017-12-07 22:21:54 +01:00
Wim
3496ed0c7e Fix irc ACTION regression (irc). Closes #306 2017-12-07 22:07:45 +01:00
Wim
1b89604c7a Bump version 2017-12-03 01:29:54 +01:00
Wim
67a9d133e9 Add quick & dirty sshchat support (https://github.com/shazow/ssh-chat) 2017-12-03 01:29:25 +01:00
Wim
ed9118b346 Add sshchat dependencies in vendor 2017-12-03 01:24:05 +01:00
Wim
59e55cfbd5 Release v1.5.0 2017-12-03 00:01:05 +01:00
Wim
788d3b32ac Update vendor lrstanley/girc and readme 2017-12-02 23:58:02 +01:00
Wim
1d414cf2fd Allow ^ in nick (irc). Closes #305 2017-11-30 00:28:17 +01:00
Wim
cc3c168162 Update vendor lrstanley/girc 2017-11-30 00:27:31 +01:00
Wim
1ee6837f0e Update changelog 2017-11-24 23:56:22 +01:00
Wim
27dcea7c5b Update documentation about ReplaceMessages and ReplaceNicks 2017-11-24 23:45:00 +01:00
Wim
dcda7f7b8c Add documentation about MediaServerUpload and MediaServerDownload 2017-11-24 23:35:25 +01:00
Wim
e0cbb69a4f Add MessageSplit option to split messages on MessageLength (irc). Closes #281 2017-11-24 23:29:00 +01:00
Wim
7ec95f786d Use mediaserver urls for irc,gitter and xmpp 2017-11-24 22:55:24 +01:00
Wim
1efe40add5 Add initial support for an external mediaserver. #278
Add 2 extra options `MediaServerUpload` and `MediaServerDownload`, where
the URL for upload and download can be specified.

See https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
for an example with caddy
2017-11-24 22:36:19 +01:00
Wim
cbd73ee313 Add support for uploaded images/video/files (matrix) 2017-11-22 00:28:40 +01:00
Wim
34227a7a39 Add support for uploading images/video (matrix). Closes #302 2017-11-21 23:50:27 +01:00
Wim
71cb9b2d1d Update vendor github.com/matrix-org/gomatrix 2017-11-21 23:48:39 +01:00
Wim
cd4c9b194f Add support for ReplaceNicks using regexp to replace nicks. Closes #269 2017-11-20 23:27:27 +01:00
Wim
98762a0235 Add webp extension to stickers if necessary (telegram) 2017-11-20 22:12:51 +01:00
Wim
2fd1fd9573 Break when re-login fails (mattermost) 2017-11-16 20:19:52 +01:00
Wim
aff3964078 Add support for ReplaceMessages using regexp to replace messages. #269 2017-11-15 23:33:00 +01:00
Wim
2778580397 Bump version 2017-11-13 20:13:32 +01:00
399 changed files with 75927 additions and 26401 deletions

View File

@ -54,7 +54,7 @@ See https://github.com/42wim/matterbridge/wiki
# Installing # Installing
## Binaries ## Binaries
* Latest stable release [v1.4.1](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v1.6.3](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
@ -175,7 +175,7 @@ Matterbridge wouldn't exist without these libraries:
* echo - https://github.com/labstack/echo * echo - https://github.com/labstack/echo
* gitter - https://github.com/sromku/go-gitter * gitter - https://github.com/sromku/go-gitter
* gops - https://github.com/google/gops * gops - https://github.com/google/gops
* irc - https://github.com/thoj/go-ircevent * irc - https://github.com/lrstanley/girc
* mattermost - https://github.com/mattermost/platform * mattermost - https://github.com/mattermost/platform
* matrix - https://github.com/matrix-org/gomatrix * matrix - https://github.com/matrix-org/gomatrix
* slack - https://github.com/nlopes/slack * slack - https://github.com/nlopes/slack

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"encoding/json"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/labstack/echo" "github.com/labstack/echo"
@ -8,14 +9,13 @@ import (
"github.com/zfjagann/golang-ring" "github.com/zfjagann/golang-ring"
"net/http" "net/http"
"sync" "sync"
"time"
) )
type Api struct { type Api struct {
Config *config.Protocol
Remote chan config.Message
Account string
Messages ring.Ring Messages ring.Ring
sync.RWMutex sync.RWMutex
*config.BridgeConfig
} }
type ApiMessage struct { type ApiMessage struct {
@ -33,23 +33,21 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Api { func New(cfg *config.BridgeConfig) *Api {
b := &Api{} b := &Api{BridgeConfig: cfg}
e := echo.New() e := echo.New()
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
b.Messages.SetCapacity(cfg.Buffer) b.Messages.SetCapacity(b.Config.Buffer)
b.Config = &cfg
b.Account = account
b.Remote = c
if b.Config.Token != "" { if b.Config.Token != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return key == b.Config.Token, nil return key == b.Config.Token, nil
})) }))
} }
e.GET("/api/messages", b.handleMessages) e.GET("/api/messages", b.handleMessages)
e.GET("/api/stream", b.handleStream)
e.POST("/api/message", b.handlePostMessage) e.POST("/api/message", b.handlePostMessage)
go func() { go func() {
flog.Fatal(e.Start(cfg.BindAddress)) flog.Fatal(e.Start(b.Config.BindAddress))
}() }()
return b return b
} }
@ -103,3 +101,24 @@ func (b *Api) handleMessages(c echo.Context) error {
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
return nil return nil
} }
func (b *Api) handleStream(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
closeNotifier := c.Response().CloseNotify()
for {
select {
case <-closeNotifier:
return nil
default:
msg := b.Messages.Dequeue()
if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
return err
}
c.Response().Flush()
}
time.Sleep(200 * time.Millisecond)
}
}
}

View File

@ -10,6 +10,7 @@ import (
"github.com/42wim/matterbridge/bridge/mattermost" "github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/rocketchat" "github.com/42wim/matterbridge/bridge/rocketchat"
"github.com/42wim/matterbridge/bridge/slack" "github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/sshchat"
"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"
@ -45,44 +46,49 @@ func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Brid
b.Protocol = protocol b.Protocol = protocol
b.Account = bridge.Account b.Account = bridge.Account
b.Joined = make(map[string]bool) b.Joined = make(map[string]bool)
bridgeConfig := &config.BridgeConfig{General: &cfg.General, Account: bridge.Account, Remote: c}
// override config from environment // override config from environment
config.OverrideCfgFromEnv(cfg, protocol, name) config.OverrideCfgFromEnv(cfg, protocol, name)
switch protocol { switch protocol {
case "mattermost": case "mattermost":
b.Config = cfg.Mattermost[name] bridgeConfig.Config = cfg.Mattermost[name]
b.Bridger = bmattermost.New(cfg.Mattermost[name], bridge.Account, c) b.Bridger = bmattermost.New(bridgeConfig)
case "irc": case "irc":
b.Config = cfg.IRC[name] bridgeConfig.Config = cfg.IRC[name]
b.Bridger = birc.New(cfg.IRC[name], bridge.Account, c) b.Bridger = birc.New(bridgeConfig)
case "gitter": case "gitter":
b.Config = cfg.Gitter[name] bridgeConfig.Config = cfg.Gitter[name]
b.Bridger = bgitter.New(cfg.Gitter[name], bridge.Account, c) b.Bridger = bgitter.New(bridgeConfig)
case "slack": case "slack":
b.Config = cfg.Slack[name] bridgeConfig.Config = cfg.Slack[name]
b.Bridger = bslack.New(cfg.Slack[name], bridge.Account, c) b.Bridger = bslack.New(bridgeConfig)
case "xmpp": case "xmpp":
b.Config = cfg.Xmpp[name] bridgeConfig.Config = cfg.Xmpp[name]
b.Bridger = bxmpp.New(cfg.Xmpp[name], bridge.Account, c) b.Bridger = bxmpp.New(bridgeConfig)
case "discord": case "discord":
b.Config = cfg.Discord[name] bridgeConfig.Config = cfg.Discord[name]
b.Bridger = bdiscord.New(cfg.Discord[name], bridge.Account, c) b.Bridger = bdiscord.New(bridgeConfig)
case "telegram": case "telegram":
b.Config = cfg.Telegram[name] bridgeConfig.Config = cfg.Telegram[name]
b.Bridger = btelegram.New(cfg.Telegram[name], bridge.Account, c) b.Bridger = btelegram.New(bridgeConfig)
case "rocketchat": case "rocketchat":
b.Config = cfg.Rocketchat[name] bridgeConfig.Config = cfg.Rocketchat[name]
b.Bridger = brocketchat.New(cfg.Rocketchat[name], bridge.Account, c) b.Bridger = brocketchat.New(bridgeConfig)
case "matrix": case "matrix":
b.Config = cfg.Matrix[name] bridgeConfig.Config = cfg.Matrix[name]
b.Bridger = bmatrix.New(cfg.Matrix[name], bridge.Account, c) b.Bridger = bmatrix.New(bridgeConfig)
case "steam": case "steam":
b.Config = cfg.Steam[name] bridgeConfig.Config = cfg.Steam[name]
b.Bridger = bsteam.New(cfg.Steam[name], bridge.Account, c) b.Bridger = bsteam.New(bridgeConfig)
case "sshchat":
bridgeConfig.Config = cfg.Sshchat[name]
b.Bridger = bsshchat.New(bridgeConfig)
case "api": case "api":
b.Config = cfg.Api[name] bridgeConfig.Config = cfg.Api[name]
b.Bridger = api.New(cfg.Api[name], bridge.Account, c) b.Bridger = api.New(bridgeConfig)
} }
b.Config = bridgeConfig.Config
return b return b
} }

View File

@ -36,6 +36,7 @@ type FileInfo struct {
Name string Name string
Data *[]byte Data *[]byte
Comment string Comment string
URL string
} }
type ChannelInfo struct { type ChannelInfo struct {
@ -59,6 +60,14 @@ type Protocol struct {
IgnoreMessages string // all protocols IgnoreMessages string // all protocols
Jid string // xmpp Jid string // xmpp
Login string // mattermost, matrix Login string // mattermost, matrix
MediaDownloadSize int // all protocols
MediaServerDownload string
MediaServerUpload string
MessageDelay int // IRC, time in millisecond to wait between messages
MessageFormat string // telegram
MessageLength int // IRC, max length of a message allowed
MessageQueue int // IRC, size of message queue for flood control
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
Muc string // xmpp Muc string // xmpp
Name string // all protocols Name string // all protocols
Nick string // all protocols Nick string // all protocols
@ -71,11 +80,10 @@ type Protocol struct {
NoTLS bool // mattermost NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP,matrix Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack PrefixMessagesWithNick bool // mattemost, slack
Protocol string //all protocols Protocol string // all protocols
MessageQueue int // IRC, size of message queue for flood control RejoinDelay int // IRC
MessageDelay int // IRC, time in millisecond to wait between messages ReplaceMessages [][]string // all protocols
MessageLength int // IRC, max length of a message allowed ReplaceNicks [][]string // all protocols
MessageFormat string // telegram
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
@ -135,11 +143,19 @@ type Config struct {
Discord map[string]Protocol Discord map[string]Protocol
Telegram map[string]Protocol Telegram map[string]Protocol
Rocketchat map[string]Protocol Rocketchat map[string]Protocol
Sshchat map[string]Protocol
General Protocol General Protocol
Gateway []Gateway Gateway []Gateway
SameChannelGateway []SameChannelGateway SameChannelGateway []SameChannelGateway
} }
type BridgeConfig struct {
Config Protocol
General *Protocol
Account string
Remote chan Message
}
func NewConfig(cfgfile string) *Config { func NewConfig(cfgfile string) *Config {
var cfg Config var cfg Config
if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil { if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil {
@ -167,6 +183,9 @@ func NewConfig(cfgfile string) *Config {
if fail { if fail {
log.Fatalf("Fix your config. Please see changelog for more information") log.Fatalf("Fix your config. Please see changelog for more information")
} }
if cfg.General.MediaDownloadSize == 0 {
cfg.General.MediaDownloadSize = 1000000
}
return &cfg return &cfg
} }

View File

@ -12,9 +12,6 @@ import (
type bdiscord struct { type bdiscord struct {
c *discordgo.Session c *discordgo.Session
Config *config.Protocol
Remote chan config.Message
Account string
Channels []*discordgo.Channel Channels []*discordgo.Channel
Nick string Nick string
UseChannelID bool UseChannelID bool
@ -24,6 +21,7 @@ type bdiscord struct {
webhookToken string webhookToken string
channelInfoMap map[string]*config.ChannelInfo channelInfoMap map[string]*config.ChannelInfo
sync.RWMutex sync.RWMutex
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
@ -33,11 +31,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *bdiscord { func New(cfg *config.BridgeConfig) *bdiscord {
b := &bdiscord{} b := &bdiscord{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
b.userMemberMap = make(map[string]*discordgo.Member) b.userMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo) b.channelInfoMap = make(map[string]*config.ChannelInfo)
if b.Config.WebhookURL != "" { if b.Config.WebhookURL != "" {

View File

@ -10,12 +10,10 @@ import (
type Bgitter struct { type Bgitter struct {
c *gitter.Gitter c *gitter.Gitter
Config *config.Protocol
Remote chan config.Message
Account string
User *gitter.User User *gitter.User
Users []gitter.User Users []gitter.User
Rooms []gitter.Room Rooms []gitter.Room
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
@ -25,12 +23,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bgitter { func New(cfg *config.BridgeConfig) *Bgitter {
b := &Bgitter{} return &Bgitter{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
} }
func (b *Bgitter) Connect() error { func (b *Bgitter) Connect() error {
@ -125,6 +119,23 @@ func (b *Bgitter) Send(msg config.Message) (string, error) {
} }
return "", nil return "", nil
} }
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text = fi.URL
}
_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil {
return "", err
}
}
return "", nil
}
}
resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text) resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -26,3 +26,15 @@ func DownloadFile(url string) (*[]byte, error) {
resp.Body.Close() resp.Body.Close()
return &data, nil return &data, nil
} }
func SplitStringLength(input string, length int) string {
a := []rune(input)
str := ""
for i, r := range a {
str = str + string(r)
if i > 0 && (i+1)%length == 0 {
str += "\n"
}
}
return str
}

View File

@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus" 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"
@ -24,12 +25,11 @@ type Birc struct {
i *girc.Client i *girc.Client
Nick string Nick string
names map[string][]string names map[string][]string
Config *config.Protocol
Remote chan config.Message
connected chan struct{} connected chan struct{}
Local chan config.Message // local queue for flood control Local chan config.Message // local queue for flood control
Account string
FirstConnection bool FirstConnection bool
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
@ -39,13 +39,11 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Birc { func New(cfg *config.BridgeConfig) *Birc {
b := &Birc{} b := &Birc{}
b.Config = &cfg b.BridgeConfig = cfg
b.Nick = b.Config.Nick b.Nick = b.Config.Nick
b.Remote = c
b.names = make(map[string][]string) b.names = make(map[string][]string)
b.Account = account
b.connected = make(chan struct{}) b.connected = make(chan struct{})
if b.Config.MessageDelay == 0 { if b.Config.MessageDelay == 0 {
b.Config.MessageDelay = 1300 b.Config.MessageDelay = 1300
@ -178,9 +176,27 @@ func (b *Birc) Send(msg config.Message) (string, error) {
msg.Text = buf.String() msg.Text = buf.String()
} }
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text = fi.URL
}
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
}
return "", nil
}
}
// split long messages on messageLength, to avoid clipped messages #281
if b.Config.MessageSplit {
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 = text[:b.Config.MessageLength] + " <message clipped>" text = string(input[:b.Config.MessageLength]) + " <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 {
@ -251,6 +267,7 @@ func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
channel := event.Params[0] channel := event.Params[0]
if event.Command == "KICK" { if event.Command == "KICK" {
flog.Infof("Got kicked from %s by %s", channel, event.Source.Name) flog.Infof("Got kicked from %s by %s", channel, event.Source.Name)
time.Sleep(time.Duration(b.Config.RejoinDelay) * time.Second)
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS} b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
return return
} }
@ -306,14 +323,13 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if event.Source.Name == b.Nick { if event.Source.Name == b.Nick {
return return
} }
rmsg := config.Message{Username: event.Source.Name, Channel: event.Params[0], Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host} rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
flog.Debugf("handlePrivMsg() %s %s %#v", event.Source.Name, event.Trailing, event) flog.Debugf("handlePrivMsg() %s %s %#v", event.Source.Name, event.Trailing, event)
msg := "" msg := ""
if event.Command == "CTCP_ACTION" { if event.IsAction() {
// msg = event.Source.Name + " "
rmsg.Event = config.EVENT_USER_ACTION rmsg.Event = config.EVENT_USER_ACTION
} }
msg += event.Trailing msg += event.StripAction()
// strip IRC colors // strip IRC colors
re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`) re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`)
msg = re.ReplaceAllString(msg, "") msg = re.ReplaceAllString(msg, "")

View File

@ -1,22 +1,24 @@
package bmatrix package bmatrix
import ( import (
"bytes"
"mime"
"regexp" "regexp"
"strings"
"sync" "sync"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
matrix "github.com/matrix-org/gomatrix" matrix "github.com/matrix-org/gomatrix"
) )
type Bmatrix struct { type Bmatrix struct {
mc *matrix.Client mc *matrix.Client
Config *config.Protocol
Remote chan config.Message
Account string
UserID string UserID string
RoomMap map[string]string RoomMap map[string]string
sync.RWMutex sync.RWMutex
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
@ -26,12 +28,9 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bmatrix { func New(cfg *config.BridgeConfig) *Bmatrix {
b := &Bmatrix{} b := &Bmatrix{BridgeConfig: cfg}
b.RoomMap = make(map[string]string) b.RoomMap = make(map[string]string)
b.Config = &cfg
b.Account = account
b.Remote = c
return b return b
} }
@ -87,6 +86,44 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
matrix.TextMessage{"m.emote", msg.Username + msg.Text}) matrix.TextMessage{"m.emote", msg.Username + msg.Text})
return "", nil return "", nil
} }
if msg.Extra != nil {
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
content := bytes.NewReader(*fi.Data)
sp := strings.Split(fi.Name, ".")
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
if strings.Contains(mtype, "image") ||
strings.Contains(mtype, "video") {
flog.Debugf("uploading file: %s %s", fi.Name, mtype)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
if err != nil {
flog.Errorf("file upload failed: %#v", err)
continue
}
if strings.Contains(mtype, "video") {
flog.Debugf("sendVideo %s", res.ContentURI)
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
if err != nil {
flog.Errorf("sendVideo failed: %#v", err)
}
}
if strings.Contains(mtype, "image") {
flog.Debugf("sendImage %s", res.ContentURI)
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
if err != nil {
flog.Errorf("sendImage failed: %#v", err)
}
}
flog.Debugf("result: %#v", res)
}
}
return "", nil
}
}
b.mc.SendText(channel, msg.Username+msg.Text) b.mc.SendText(channel, msg.Username+msg.Text)
return "", nil return "", nil
} }
@ -104,7 +141,13 @@ func (b *Bmatrix) getRoomID(channel string) string {
func (b *Bmatrix) handlematrix() error { func (b *Bmatrix) handlematrix() error {
syncer := b.mc.Syncer.(*matrix.DefaultSyncer) syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
syncer.OnEventType("m.room.message", func(ev *matrix.Event) { syncer.OnEventType("m.room.message", func(ev *matrix.Event) {
if (ev.Content["msgtype"].(string) == "m.text" || ev.Content["msgtype"].(string) == "m.notice" || ev.Content["msgtype"].(string) == "m.emote") && ev.Sender != b.UserID { flog.Debugf("Received: %#v", ev)
if (ev.Content["msgtype"].(string) == "m.text" ||
ev.Content["msgtype"].(string) == "m.notice" ||
ev.Content["msgtype"].(string) == "m.emote" ||
ev.Content["msgtype"].(string) == "m.file" ||
ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video") && ev.Sender != b.UserID {
b.RLock() b.RLock()
channel, ok := b.RoomMap[ev.RoomID] channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock() b.RUnlock()
@ -121,10 +164,31 @@ func (b *Bmatrix) handlematrix() error {
if ev.Content["msgtype"].(string) == "m.emote" { if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EVENT_USER_ACTION rmsg.Event = config.EVENT_USER_ACTION
} }
if ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video" ||
ev.Content["msgtype"].(string) == "m.file" {
flog.Debugf("ev: %#v", ev)
rmsg.Extra = make(map[string][]interface{})
url := ev.Content["url"].(string)
url = strings.Replace(url, "mxc://", b.Config.Server+"/_matrix/media/v1/download/", -1)
info := ev.Content["info"].(map[string]interface{})
size := info["size"].(float64)
name := ev.Content["body"].(string)
flog.Debugf("trying to download %#v with size %#v", name, size)
if size <= float64(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))
rmsg.Extra["file"] = append(rmsg.Extra["file"], config.FileInfo{Name: name, Data: data})
}
}
rmsg.Text = ""
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account) flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg b.Remote <- rmsg
} }
flog.Debugf("Received: %#v", ev)
}) })
go func() { go func() {
for { for {

View File

@ -32,10 +32,8 @@ type MMMessage struct {
type Bmattermost struct { type Bmattermost struct {
MMhook MMhook
MMapi MMapi
Config *config.Protocol
Remote chan config.Message
TeamId string TeamId string
Account string *config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
@ -45,11 +43,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bmattermost { func New(cfg *config.BridgeConfig) *Bmattermost {
b := &Bmattermost{} b := &Bmattermost{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
b.mmMap = make(map[string]string) b.mmMap = make(map[string]string)
return b return b
} }

View File

@ -14,9 +14,7 @@ type MMhook struct {
type Brocketchat struct { type Brocketchat struct {
MMhook MMhook
Config *config.Protocol *config.BridgeConfig
Remote chan config.Message
Account string
} }
var flog *log.Entry var flog *log.Entry
@ -26,12 +24,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Brocketchat { func New(cfg *config.BridgeConfig) *Brocketchat {
b := &Brocketchat{} return &Brocketchat{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
} }
func (b *Brocketchat) Command(cmd string) string { func (b *Brocketchat) Command(cmd string) string {

View File

@ -7,7 +7,7 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/matterbridge/slack" "github.com/nlopes/slack"
"html" "html"
"io" "io"
"net/http" "net/http"
@ -27,14 +27,12 @@ type MMMessage struct {
type Bslack struct { type Bslack struct {
mh *matterhook.Client mh *matterhook.Client
sc *slack.Client sc *slack.Client
Config *config.Protocol
rtm *slack.RTM rtm *slack.RTM
Plus bool Plus bool
Remote chan config.Message
Users []slack.User Users []slack.User
Account string
si *slack.Info si *slack.Info
channels []slack.Channel channels []slack.Channel
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
@ -44,12 +42,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bslack { func New(cfg *config.BridgeConfig) *Bslack {
b := &Bslack{} return &Bslack{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
} }
func (b *Bslack) Command(cmd string) string { func (b *Bslack) Command(cmd string) string {
@ -113,7 +107,7 @@ func (b *Bslack) Disconnect() error {
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// we can only join channels using the API // we can only join channels using the API
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" { if b.sc != nil {
if strings.HasPrefix(b.Config.Token, "xoxb") { if strings.HasPrefix(b.Config.Token, "xoxb") {
// TODO check if bot has already joined channel // TODO check if bot has already joined channel
return nil return nil
@ -161,7 +155,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
np.AsUser = true np.AsUser = true
} }
np.Username = nick np.Username = nick
np.IconURL = config.GetIconURL(&msg, b.Config) np.IconURL = config.GetIconURL(&msg, &b.Config)
if msg.Avatar != "" { if msg.Avatar != "" {
np.IconURL = msg.Avatar np.IconURL = msg.Avatar
} }
@ -294,7 +288,7 @@ func (b *Bslack) handleSlack() {
// 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 <= 1000000 { if message.Raw.File.Size <= b.General.MediaDownloadSize {
comment := "" comment := ""
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload) data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
if err != nil { if err != nil {
@ -315,9 +309,11 @@ func (b *Bslack) handleSlack() {
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) { func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
for msg := range b.rtm.IncomingEvents { for msg := range b.rtm.IncomingEvents {
if msg.Type != "user_typing" && msg.Type != "latency_report" {
flog.Debugf("Receiving from slackclient %#v", msg.Data)
}
switch ev := msg.Data.(type) { switch ev := msg.Data.(type) {
case *slack.MessageEvent: case *slack.MessageEvent:
flog.Debugf("Receiving from slackclient %#v", ev)
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" {
@ -400,6 +396,8 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
} }
case *slack.InvalidAuthEvent: case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev) flog.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
flog.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
default: default:
} }
} }

132
bridge/sshchat/sshchat.go Normal file
View File

@ -0,0 +1,132 @@
package bsshchat
import (
"bufio"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/shazow/ssh-chat/sshd"
"io"
"strings"
)
type Bsshchat struct {
r *bufio.Scanner
w io.WriteCloser
*config.BridgeConfig
}
var flog *log.Entry
var protocol = "sshchat"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bsshchat {
return &Bsshchat{BridgeConfig: cfg}
}
func (b *Bsshchat) Connect() error {
var err error
flog.Infof("Connecting %s", b.Config.Server)
go func() {
err = sshd.ConnectShell(b.Config.Server, b.Config.Nick, func(r io.Reader, w io.WriteCloser) error {
b.r = bufio.NewScanner(r)
b.w = w
b.r.Scan()
w.Write([]byte("/theme mono\r\n"))
b.handleSshChat()
return nil
})
}()
if err != nil {
flog.Debugf("%#v", err)
return err
}
flog.Info("Connection succeeded")
return nil
}
func (b *Bsshchat) Disconnect() error {
return nil
}
func (b *Bsshchat) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bsshchat) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text = fi.URL
}
b.w.Write([]byte(msg.Username + msg.Text))
}
return "", nil
}
}
b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
return "", nil
}
/*
func (b *Bsshchat) sshchatKeepAlive() chan bool {
done := make(chan bool)
go func() {
ticker := time.NewTicker(90 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flog.Debugf("PING")
err := b.xc.PingC2S("", "")
if err != nil {
flog.Debugf("PING failed %#v", err)
}
case <-done:
return
}
}
}()
return done
}
*/
func stripPrompt(s string) string {
pos := strings.LastIndex(s, "\033[K")
if pos < 0 {
return s
}
return s[pos+3:]
}
func (b *Bsshchat) handleSshChat() error {
/*
done := b.sshchatKeepAlive()
defer close(done)
*/
wait := true
for {
if b.r.Scan() {
res := strings.Split(stripPrompt(b.r.Text()), ":")
if res[0] == "-> Set theme" {
wait = false
log.Debugf("mono found, allowing")
continue
}
if !wait {
flog.Debugf("message %#v", res)
rmsg := config.Message{Username: res[0], Text: strings.Join(res[1:], ":"), Channel: "sshchat", Account: b.Account, UserID: "nick"}
b.Remote <- rmsg
}
}
}
}

View File

@ -16,11 +16,9 @@ import (
type Bsteam struct { type Bsteam struct {
c *steam.Client c *steam.Client
connected chan struct{} connected chan struct{}
Config *config.Protocol
Remote chan config.Message
Account string
userMap map[steamid.SteamId]string userMap map[steamid.SteamId]string
sync.RWMutex sync.RWMutex
*config.BridgeConfig
} }
var flog *log.Entry var flog *log.Entry
@ -30,11 +28,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bsteam { func New(cfg *config.BridgeConfig) *Bsteam {
b := &Bsteam{} b := &Bsteam{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
b.userMap = make(map[steamid.SteamId]string) b.userMap = make(map[steamid.SteamId]string)
b.connected = make(chan struct{}) b.connected = make(chan struct{})
return b return b

View File

@ -13,9 +13,7 @@ import (
type Btelegram struct { type Btelegram struct {
c *tgbotapi.BotAPI c *tgbotapi.BotAPI
Config *config.Protocol *config.BridgeConfig
Remote chan config.Message
Account string
} }
var flog *log.Entry var flog *log.Entry
@ -25,12 +23,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Btelegram { func New(cfg *config.BridgeConfig) *Btelegram {
b := &Btelegram{} return &Btelegram{BridgeConfig: cfg}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
} }
func (b *Btelegram) Connect() error { func (b *Btelegram) Connect() error {
@ -90,6 +84,9 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
return "", err return "", err
} }
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text) m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML
}
_, err = b.c.Send(m) _, err = b.c.Send(m)
if err != nil { if err != nil {
return "", err return "", err
@ -179,6 +176,29 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
if message.Document != nil { if message.Document != nil {
b.handleDownload(message.Document, &fmsg) b.handleDownload(message.Document, &fmsg)
} }
if message.Voice != nil {
b.handleDownload(message.Voice, &fmsg)
}
if message.Audio != nil {
b.handleDownload(message.Audio, &fmsg)
}
if message.ForwardFrom != nil {
usernameForward := ""
if b.Config.UseFirstName {
usernameForward = message.ForwardFrom.FirstName
}
if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName
if usernameForward == "" {
usernameForward = message.ForwardFrom.FirstName
}
}
if usernameForward == "" {
usernameForward = "unknown"
}
text = "Forwarded from " + usernameForward + ": " + text
}
// quote the previous message // quote the previous message
if message.ReplyToMessage != nil { if message.ReplyToMessage != nil {
@ -224,11 +244,31 @@ func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) {
text := "" text := ""
fileid := "" fileid := ""
switch v := file.(type) { switch v := file.(type) {
case *tgbotapi.Audio:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
fileid = v.FileID
case *tgbotapi.Voice:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
if !strings.HasSuffix(name, ".ogg") {
name = name + ".ogg"
}
fileid = v.FileID
case *tgbotapi.Sticker: case *tgbotapi.Sticker:
size = v.FileSize size = v.FileSize
url = b.getFileDirectURL(v.FileID) url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/") urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1] name = urlPart[len(urlPart)-1]
if !strings.HasSuffix(name, ".webp") {
name = name + ".webp"
}
text = " " + url text = " " + url
fileid = v.FileID fileid = v.FileID
case *tgbotapi.Video: case *tgbotapi.Video:
@ -259,7 +299,7 @@ func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) {
// 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 // 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 <= 1000000 { if size <= b.General.MediaDownloadSize {
data, err := helper.DownloadFile(url) data, err := helper.DownloadFile(url)
if err != nil { if err != nil {
flog.Errorf("download %s failed %#v", url, err) flog.Errorf("download %s failed %#v", url, err)

View File

@ -14,9 +14,7 @@ import (
type Bxmpp struct { type Bxmpp struct {
xc *xmpp.Client xc *xmpp.Client
xmppMap map[string]string xmppMap map[string]string
Config *config.Protocol *config.BridgeConfig
Remote chan config.Message
Account string
} }
var flog *log.Entry var flog *log.Entry
@ -26,12 +24,9 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol}) flog = log.WithFields(log.Fields{"module": protocol})
} }
func New(cfg config.Protocol, account string, c chan config.Message) *Bxmpp { func New(cfg *config.BridgeConfig) *Bxmpp {
b := &Bxmpp{} b := &Bxmpp{BridgeConfig: cfg}
b.xmppMap = make(map[string]string) b.xmppMap = make(map[string]string)
b.Config = &cfg
b.Account = account
b.Remote = c
return b return b
} }
@ -85,6 +80,19 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
return "", nil return "", nil
} }
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text = fi.URL
}
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
}
return "", nil
}
}
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})
return "", nil return "", nil
} }

View File

@ -1,3 +1,50 @@
# v1.6.3
## Bugfix
* slack: Fix connection issues
* slack: Add more debug messages
* irc: Convert received IRC channel names to lowercase. Fixes #329 (#330)
# v1.6.2
## Bugfix
* mattermost: Crashes while connecting to Mattermost (regression). Closes #327
# v1.6.1
## Bugfix
* general: Display of nicks not longer working (regression). Closes #323
# v1.6.0
## New features
* sshchat: New protocol support added (https://github.com/shazow/ssh-chat)
* general: Allow specifying maximum download size of media using MediaDownloadSize (slack,telegram,matrix)
* api: Add (simple, one listener) long-polling support (api). Closes #307
* telegram: Add support for forwarded messages. Closes #313
* telegram: Add support for Audio/Voice files (telegram). Closes #314
* irc: Add RejoinDelay option. Delay to rejoin after channel kick (irc). Closes #322
## Bugfix
* telegram: Also use HTML in edited messages (telegram). Closes #315
* matrix: Fix panic (matrix). Closes #316
# v1.5.1
## Bugfix
* irc: Fix irc ACTION regression (irc). Closes #306
* irc: Split on UTF-8 for MessageSplit (irc). Closes #308
# v1.5.0
## New features
* general: remote mediaserver support. See MediaServerDownload and MediaServerUpload in matterbridge.toml.sample
more information on https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
* general: Add support for ReplaceNicks using regexp to replace nicks. Closes #269 (see matterbridge.toml.sample)
* general: Add support for ReplaceMessages using regexp to replace messages. #269 (see matterbridge.toml.sample)
* irc: Add MessageSplit option to split messages on MessageLength (irc). Closes #281
* matrix: Add support for uploading images/video (matrix). Closes #302
* matrix: Add support for uploaded images/video (matrix)
## Bugfix
* telegram: Add webp extension to stickers if necessary (telegram)
* mattermost: Break when re-login fails (mattermost)
# v1.4.1 # v1.4.1
## Bugfix ## Bugfix
* telegram: fix issue with uploading for images/documents/stickers * telegram: fix issue with uploading for images/documents/stickers

View File

@ -1,13 +1,16 @@
package gateway package gateway
import ( import (
"bytes"
"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"
"github.com/hashicorp/golang-lru" "github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib" "github.com/peterhellberg/emojilib"
"net/http"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -155,7 +158,8 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
if dest.Protocol != "discord" && if dest.Protocol != "discord" &&
dest.Protocol != "slack" && dest.Protocol != "slack" &&
dest.Protocol != "mattermost" && dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" { dest.Protocol != "telegram" &&
dest.Protocol != "matrix" {
if msg.Text == "" { if msg.Text == "" {
return brMsgIDs return brMsgIDs
} }
@ -254,6 +258,20 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
if nick == "" { if nick == "" {
nick = gw.Config.General.RemoteNickFormat nick = gw.Config.General.RemoteNickFormat
} }
// loop to replace nicks
for _, outer := range br.Config.ReplaceNicks {
search := outer[0]
replace := outer[1]
// TODO move compile to bridge init somewhere
re, err := regexp.Compile(search)
if err != nil {
log.Errorf("regexp in %s failed: %s", msg.Account, err)
break
}
msg.Username = re.ReplaceAllString(msg.Username, replace)
}
if len(msg.Username) > 0 { if len(msg.Username) > 0 {
// fix utf-8 issue #193 // fix utf-8 issue #193
i := 0 i := 0
@ -287,9 +305,50 @@ func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string
func (gw *Gateway) modifyMessage(msg *config.Message) { func (gw *Gateway) modifyMessage(msg *config.Message) {
// replace :emoji: to unicode // replace :emoji: to unicode
msg.Text = emojilib.Replace(msg.Text) msg.Text = emojilib.Replace(msg.Text)
br := gw.Bridges[msg.Account]
// loop to replace messages
for _, outer := range br.Config.ReplaceMessages {
search := outer[0]
replace := outer[1]
// TODO move compile to bridge init somewhere
re, err := regexp.Compile(search)
if err != nil {
log.Errorf("regexp in %s failed: %s", msg.Account, err)
break
}
msg.Text = re.ReplaceAllString(msg.Text, replace)
}
msg.Gateway = gw.Name msg.Gateway = gw.Name
} }
func (gw *Gateway) handleFiles(msg *config.Message) {
if msg.Extra == nil || gw.Config.General.MediaServerUpload == "" {
return
}
if len(msg.Extra["file"]) > 0 {
client := &http.Client{
Timeout: time.Second * 5,
}
for i, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))
reader := bytes.NewReader(*fi.Data)
url := gw.Config.General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
durl := gw.Config.General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
extra := msg.Extra["file"][i].(config.FileInfo)
extra.URL = durl
msg.Extra["file"][i] = extra
req, _ := http.NewRequest("PUT", url, reader)
req.Header.Set("Content-Type", "binary/octet-stream")
_, err := client.Do(req)
if err != nil {
log.Errorf("mediaserver upload failed: %#v", err)
}
log.Debugf("mediaserver download URL = %s", durl)
}
}
}
func getChannelID(msg config.Message) string { func getChannelID(msg config.Message) string {
return msg.Channel + msg.Account return msg.Channel + msg.Account
} }

View File

@ -99,6 +99,7 @@ func (r *Router) handleReceive() {
if !gw.ignoreMessage(&msg) { if !gw.ignoreMessage(&msg) {
msg.Timestamp = time.Now() msg.Timestamp = time.Now()
gw.modifyMessage(&msg) gw.modifyMessage(&msg)
gw.handleFiles(&msg)
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
msgIDs = append(msgIDs, gw.handleMessage(msg, br)...) msgIDs = append(msgIDs, gw.handleMessage(msg, br)...)
} }

View File

@ -12,7 +12,7 @@ import (
) )
var ( var (
version = "1.4.1" version = "1.6.3"
githash string githash string
) )

View File

@ -80,6 +80,15 @@ MessageQueue=30
#OPTIONAL (default 400) #OPTIONAL (default 400)
MessageLength=400 MessageLength=400
#Split messages on MessageLength instead of showing the <message clipped>
#WARNING: this could lead to flooding
#OPTIONAL (default false)
MessageSplit=false
#Delay in seconds to rejoin a channel when kicked
#OPTIONAL (default 0)
RejoinDelay=0
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
@ -91,6 +100,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -154,6 +180,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#Messages you want to replace.
#It replaces outgoing messages from the bridge.
#So you need to place it by the sending bridge definition.
#Regular expressions supported
#Some examples:
#This replaces cat => dog and sleep => awake
#ReplaceMessages=[ ["cat","dog"], ["sleep","awake"] ]
#This Replaces every number with number. 123 => numbernumbernumber
#ReplaceMessages=[ ["[0-9]","number"] ]
#OPTIONAL (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#Nicks you want to replace.
#See ReplaceMessages for syntaxA
#OPTIONAL (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -208,6 +251,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -322,6 +382,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -366,6 +443,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -457,6 +551,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -525,6 +636,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -592,6 +720,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -660,6 +805,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -720,6 +882,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -774,6 +953,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword" IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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
@ -838,6 +1034,30 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#MediaServerUpload and MediaServerDownload are used for uploading images/files/video to
#a remote "mediaserver" (a webserver like caddy for example).
#When configured images/files uploaded on bridges like mattermost,slack, telegram will be downloaded
#and uploaded again to MediaServerUpload URL
#The MediaServerDownload will be used so that bridges without native uploading support:
#gitter, irc and xmpp will be shown links to the files on MediaServerDownload
#
#More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
#OPTIONAL (default empty)
MediaServerUpload="https://user:pass@yourserver.com/upload"
#OPTIONAL (default empty)
MediaServerDownload="https://youserver.com/download"
#MediaDownloadSize is the maximum size of attachments, videos, images
#matterbridge will download and upload this file to bridges that also support uploading files.
#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
#slack, telegram and matrix
#
#Optional (default 1000000 (1 megabyte))
MediaDownloadSize=1000000
################################################################### ###################################################################
#Gateway configuration #Gateway configuration
################################################################### ###################################################################

View File

@ -817,9 +817,14 @@ func (m *MMClient) StatusLoop() {
backoff = time.Second * 60 backoff = time.Second * 60
case <-time.After(time.Second * 5): case <-time.After(time.Second * 5):
if retries > 3 { if retries > 3 {
m.log.Debug("StatusLoop() timeout")
m.Logout() m.Logout()
m.WsQuit = false m.WsQuit = false
m.Login() err := m.Login()
if err != nil {
log.Errorf("Login failed: %#v", err)
break
}
if m.OnWsConnect != nil { if m.OnWsConnect != nil {
m.OnWsConnect() m.OnWsConnect()
} }

View File

@ -494,14 +494,9 @@ func (c *context) Stream(code int, contentType string, r io.Reader) (err error)
} }
func (c *context) File(file string) (err error) { func (c *context) File(file string) (err error) {
file, err = url.QueryUnescape(file) // Issue #839
if err != nil {
return
}
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return ErrNotFound return NotFoundHandler(c)
} }
defer f.Close() defer f.Close()
@ -510,7 +505,7 @@ func (c *context) File(file string) (err error) {
file = filepath.Join(file, indexPage) file = filepath.Join(file, indexPage)
f, err = os.Open(file) f, err = os.Open(file)
if err != nil { if err != nil {
return ErrNotFound return NotFoundHandler(c)
} }
defer f.Close() defer f.Close()
if fi, err = f.Stat(); err != nil { if fi, err = f.Stat(); err != nil {
@ -530,7 +525,7 @@ func (c *context) Inline(file, name string) (err error) {
} }
func (c *context) contentDisposition(file, name, dispositionType string) (err error) { func (c *context) contentDisposition(file, name, dispositionType string) (err error) {
c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%s", dispositionType, name)) c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name))
c.File(file) c.File(file)
return return
} }

View File

@ -72,6 +72,7 @@ type (
TLSServer *http.Server TLSServer *http.Server
Listener net.Listener Listener net.Listener
TLSListener net.Listener TLSListener net.Listener
AutoTLSManager autocert.Manager
DisableHTTP2 bool DisableHTTP2 bool
Debug bool Debug bool
HideBanner bool HideBanner bool
@ -79,7 +80,6 @@ type (
Binder Binder Binder Binder
Validator Validator Validator Validator
Renderer Renderer Renderer Renderer
AutoTLSManager autocert.Manager
// Mutex sync.RWMutex // Mutex sync.RWMutex
Logger Logger Logger Logger
} }
@ -88,13 +88,14 @@ type (
Route struct { Route struct {
Method string `json:"method"` Method string `json:"method"`
Path string `json:"path"` Path string `json:"path"`
Handler string `json:"handler"` Name string `json:"name"`
} }
// HTTPError represents an error that occurred while handling a request. // HTTPError represents an error that occurred while handling a request.
HTTPError struct { HTTPError struct {
Code int Code int
Message interface{} Message interface{}
Inner error // Stores the error returned by an external dependency
} }
// MiddlewareFunc defines a function to process middleware. // MiddlewareFunc defines a function to process middleware.
@ -121,7 +122,7 @@ type (
// i is the interface for Echo and Group. // i is the interface for Echo and Group.
i interface { i interface {
GET(string, HandlerFunc, ...MiddlewareFunc) GET(string, HandlerFunc, ...MiddlewareFunc) *Route
} }
) )
@ -212,7 +213,7 @@ const (
) )
const ( const (
version = "3.1.0" version = "3.2.5"
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 = `
@ -282,7 +283,7 @@ func New() (e *Echo) {
e.TLSServer.Handler = e e.TLSServer.Handler = e
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
e.Binder = &DefaultBinder{} e.Binder = &DefaultBinder{}
e.Logger.SetLevel(log.OFF) e.Logger.SetLevel(log.ERROR)
e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
e.pool.New = func() interface{} { e.pool.New = func() interface{} {
return e.NewContext(nil, nil) return e.NewContext(nil, nil)
@ -319,6 +320,9 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
if he, ok := err.(*HTTPError); ok { if he, ok := err.(*HTTPError); ok {
code = he.Code code = he.Code
msg = he.Message msg = he.Message
if he.Inner != nil {
msg = fmt.Sprintf("%v, %v", err, he.Inner)
}
} else if e.Debug { } else if e.Debug {
msg = err.Error() msg = err.Error()
} else { } else {
@ -328,19 +332,19 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
msg = Map{"message": msg} msg = Map{"message": msg}
} }
e.Logger.Error(err)
// Send response
if !c.Response().Committed { if !c.Response().Committed {
if c.Request().Method == HEAD { // Issue #608 if c.Request().Method == HEAD { // Issue #608
if err := c.NoContent(code); err != nil { err = c.NoContent(code)
goto ERROR
}
} else { } else {
if err := c.JSON(code, msg); err != nil { err = c.JSON(code, msg)
goto ERROR
} }
} if err != nil {
}
ERROR:
e.Logger.Error(err) e.Logger.Error(err)
}
}
} }
// Pre adds middleware to the chain which is run before router. // Pre adds middleware to the chain which is run before router.
@ -355,104 +359,114 @@ func (e *Echo) Use(middleware ...MiddlewareFunc) {
// CONNECT registers a new CONNECT route for a path with matching handler in the // CONNECT registers a new CONNECT route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(CONNECT, path, h, m...) return e.Add(CONNECT, path, h, m...)
} }
// DELETE registers a new DELETE route for a path with matching handler in the router // DELETE registers a new DELETE route for a path with matching handler in the router
// with optional route-level middleware. // with optional route-level middleware.
func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(DELETE, path, h, m...) return e.Add(DELETE, path, h, m...)
} }
// GET registers a new GET route for a path with matching handler in the router // GET registers a new GET route for a path with matching handler in the router
// with optional route-level middleware. // with optional route-level middleware.
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(GET, path, h, m...) return e.Add(GET, path, h, m...)
} }
// HEAD registers a new HEAD route for a path with matching handler in the // HEAD registers a new HEAD route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(HEAD, path, h, m...) return e.Add(HEAD, path, h, m...)
} }
// OPTIONS registers a new OPTIONS route for a path with matching handler in the // OPTIONS registers a new OPTIONS route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(OPTIONS, path, h, m...) return e.Add(OPTIONS, path, h, m...)
} }
// PATCH registers a new PATCH route for a path with matching handler in the // PATCH registers a new PATCH route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(PATCH, path, h, m...) return e.Add(PATCH, path, h, m...)
} }
// POST registers a new POST route for a path with matching handler in the // POST registers a new POST route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(POST, path, h, m...) return e.Add(POST, path, h, m...)
} }
// PUT registers a new PUT route for a path with matching handler in the // PUT registers a new PUT route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(PUT, path, h, m...) return e.Add(PUT, path, h, m...)
} }
// TRACE registers a new TRACE route for a path with matching handler in the // TRACE registers a new TRACE route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(TRACE, path, h, m...) return e.Add(TRACE, path, h, m...)
} }
// 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) { func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
routes := make([]*Route, 0)
for _, m := range methods { for _, m := range methods {
e.add(m, path, handler, middleware...) routes = append(routes, e.Add(m, path, handler, middleware...))
} }
return routes
} }
// 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) { func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
routes := make([]*Route, 0)
for _, m := range methods { for _, m := range methods {
e.add(m, path, handler, middleware...) routes = append(routes, e.Add(m, path, handler, middleware...))
} }
return routes
} }
// Static registers a new route with path prefix to serve static files from the // Static registers a new route with path prefix to serve static files from the
// provided root directory. // provided root directory.
func (e *Echo) Static(prefix, root string) { func (e *Echo) Static(prefix, root string) *Route {
if root == "" { if root == "" {
root = "." // For security we want to restrict to CWD. root = "." // For security we want to restrict to CWD.
} }
static(e, prefix, root) return static(e, prefix, root)
} }
func static(i i, prefix, root string) { func static(i i, prefix, root string) *Route {
h := func(c Context) error { h := func(c Context) error {
name := filepath.Join(root, path.Clean("/"+c.Param("*"))) // "/"+ for security p, err := PathUnescape(c.Param("*"))
if err != nil {
return err
}
name := filepath.Join(root, path.Clean("/"+p)) // "/"+ for security
return c.File(name) return c.File(name)
} }
i.GET(prefix, h) i.GET(prefix, h)
if prefix == "/" { if prefix == "/" {
i.GET(prefix+"*", h) return i.GET(prefix+"*", h)
} else {
i.GET(prefix+"/*", h)
} }
return i.GET(prefix+"/*", h)
} }
// File registers a new route with path to serve a static file. // File registers a new route with path to serve a static file.
func (e *Echo) File(path, file string) { func (e *Echo) File(path, file string) *Route {
e.GET(path, func(c Context) error { return e.GET(path, func(c Context) error {
return c.File(file) return c.File(file)
}) })
} }
func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { // Add registers a new route for an HTTP method and path with matching handler
// in the router with optional route-level middleware.
func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
name := handlerName(handler) name := handlerName(handler)
e.router.Add(method, path, func(c Context) error { e.router.Add(method, path, func(c Context) error {
h := handler h := handler
@ -465,9 +479,10 @@ func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...Middl
r := &Route{ r := &Route{
Method: method, Method: method,
Path: path, Path: path,
Handler: name, Name: name,
} }
e.router.routes[method+path] = r e.router.routes[method+path] = r
return r
} }
// Group creates a new router group with prefix and optional group-level middleware. // Group creates a new router group with prefix and optional group-level middleware.
@ -479,12 +494,22 @@ func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
// URI generates a URI from handler. // URI generates a URI from handler.
func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string { func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
name := handlerName(handler)
return e.Reverse(name, params...)
}
// URL is an alias for `URI` function.
func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
return e.URI(h, params...)
}
// Reverse generates an URL from route name and provided parameters.
func (e *Echo) Reverse(name string, params ...interface{}) string {
uri := new(bytes.Buffer) uri := new(bytes.Buffer)
ln := len(params) ln := len(params)
n := 0 n := 0
name := handlerName(handler)
for _, r := range e.router.routes { for _, r := range e.router.routes {
if r.Handler == name { if r.Name == name {
for i, l := 0, len(r.Path); i < l; i++ { for i, l := 0, len(r.Path); i < l; i++ {
if r.Path[i] == ':' && n < ln { if r.Path[i] == ':' && n < ln {
for ; i < l && r.Path[i] != '/'; i++ { for ; i < l && r.Path[i] != '/'; i++ {
@ -502,11 +527,6 @@ func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
return uri.String() return uri.String()
} }
// URL is an alias for `URI` function.
func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
return e.URI(h, params...)
}
// Routes returns the registered routes. // Routes returns the registered routes.
func (e *Echo) Routes() []*Route { func (e *Echo) Routes() []*Route {
routes := []*Route{} routes := []*Route{}
@ -653,7 +673,7 @@ func NewHTTPError(code int, message ...interface{}) *HTTPError {
// Error makes it compatible with `error` interface. // Error makes it compatible with `error` interface.
func (he *HTTPError) Error() string { func (he *HTTPError) Error() string {
return fmt.Sprintf("code=%d, message=%s", he.Code, he.Message) return fmt.Sprintf("code=%d, message=%v", he.Code, he.Message)
} }
// WrapHandler wraps `http.Handler` into `echo.HandlerFunc`. // WrapHandler wraps `http.Handler` into `echo.HandlerFunc`.

View File

@ -21,66 +21,66 @@ func (g *Group) Use(middleware ...MiddlewareFunc) {
// 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 { g.echo.Any(path.Clean(g.prefix+"/*"), func(c Context) error {
return ErrNotFound 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.
func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(CONNECT, path, h, m...) return g.Add(CONNECT, path, h, m...)
} }
// DELETE implements `Echo#DELETE()` for sub-routes within the Group. // DELETE implements `Echo#DELETE()` for sub-routes within the Group.
func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(DELETE, path, h, m...) return g.Add(DELETE, path, h, m...)
} }
// GET implements `Echo#GET()` for sub-routes within the Group. // GET implements `Echo#GET()` for sub-routes within the Group.
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(GET, path, h, m...) return g.Add(GET, path, h, m...)
} }
// HEAD implements `Echo#HEAD()` for sub-routes within the Group. // HEAD implements `Echo#HEAD()` for sub-routes within the Group.
func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(HEAD, path, h, m...) return g.Add(HEAD, path, h, m...)
} }
// OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group. // OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group.
func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(OPTIONS, path, h, m...) return g.Add(OPTIONS, path, h, m...)
} }
// PATCH implements `Echo#PATCH()` for sub-routes within the Group. // PATCH implements `Echo#PATCH()` for sub-routes within the Group.
func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(PATCH, path, h, m...) return g.Add(PATCH, path, h, m...)
} }
// POST implements `Echo#POST()` for sub-routes within the Group. // POST implements `Echo#POST()` for sub-routes within the Group.
func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(POST, path, h, m...) return g.Add(POST, path, h, m...)
} }
// PUT implements `Echo#PUT()` for sub-routes within the Group. // PUT implements `Echo#PUT()` for sub-routes within the Group.
func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(PUT, path, h, m...) return g.Add(PUT, path, h, m...)
} }
// TRACE implements `Echo#TRACE()` for sub-routes within the Group. // TRACE implements `Echo#TRACE()` for sub-routes within the Group.
func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(TRACE, path, h, m...) return g.Add(TRACE, path, h, m...)
} }
// 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) {
for _, m := range methods { for _, m := range methods {
g.add(m, path, handler, middleware...) g.Add(m, path, handler, middleware...)
} }
} }
// 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) {
for _, m := range methods { for _, m := range methods {
g.add(m, path, handler, middleware...) g.Add(m, path, handler, middleware...)
} }
} }
@ -102,12 +102,13 @@ func (g *Group) File(path, file string) {
g.echo.File(g.prefix+path, file) g.echo.File(g.prefix+path, file)
} }
func (g *Group) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { // Add implements `Echo#Add()` for sub-routes within the Group.
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
// Combine into a new slice to avoid accidentally passing the same slice for // Combine into a new slice to avoid accidentally passing the same slice for
// multiple routes, which would lead to later add() calls overwriting the // multiple routes, which would lead to later add() calls overwriting the
// middleware from earlier calls. // middleware from earlier calls.
m := []MiddlewareFunc{} m := []MiddlewareFunc{}
m = append(m, g.middleware...) m = append(m, g.middleware...)
m = append(m, middleware...) m = append(m, middleware...)
g.echo.add(method, g.prefix+path, handler, m...) return g.echo.Add(method, g.prefix+path, handler, m...)
} }

View File

@ -3,6 +3,7 @@ package middleware
import ( import (
"encoding/base64" "encoding/base64"
"strconv" "strconv"
"strings"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
@ -27,7 +28,7 @@ type (
) )
const ( const (
basic = "Basic" basic = "basic"
defaultRealm = "Restricted" defaultRealm = "Restricted"
) )
@ -54,7 +55,7 @@ func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc {
func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc { func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
// Defaults // Defaults
if config.Validator == nil { if config.Validator == nil {
panic("basic-auth middleware requires a validator function") panic("echo: basic-auth middleware requires a validator function")
} }
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultBasicAuthConfig.Skipper config.Skipper = DefaultBasicAuthConfig.Skipper
@ -72,7 +73,7 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
auth := c.Request().Header.Get(echo.HeaderAuthorization) auth := c.Request().Header.Get(echo.HeaderAuthorization)
l := len(basic) l := len(basic)
if len(auth) > l+1 && auth[:l] == basic { if len(auth) > l+1 && strings.ToLower(auth[:l]) == basic {
b, err := base64.StdEncoding.DecodeString(auth[l+1:]) b, err := base64.StdEncoding.DecodeString(auth[l+1:])
if err != nil { if err != nil {
return err return err

112
vendor/github.com/labstack/echo/middleware/body_dump.go generated vendored Normal file
View File

@ -0,0 +1,112 @@
package middleware
import (
"bufio"
"bytes"
"io/ioutil"
"net"
"net/http"
"io"
"github.com/labstack/echo"
)
type (
// BodyDumpConfig defines the config for BodyDump middleware.
BodyDumpConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Handler receives request and response payload.
// Required.
Handler BodyDumpHandler
}
// BodyDumpHandler receives the request and response payload.
BodyDumpHandler func(echo.Context, []byte, []byte)
bodyDumpResponseWriter struct {
io.Writer
http.ResponseWriter
}
)
var (
// DefaultBodyDumpConfig is the default Gzip middleware config.
DefaultBodyDumpConfig = BodyDumpConfig{
Skipper: DefaultSkipper,
}
)
// BodyDump returns a BodyDump middleware.
//
// BodyLimit middleware captures the request and response payload and calls the
// registered handler.
func BodyDump(handler BodyDumpHandler) echo.MiddlewareFunc {
c := DefaultBodyDumpConfig
c.Handler = handler
return BodyDumpWithConfig(c)
}
// BodyDumpWithConfig returns a BodyDump middleware with config.
// See: `BodyDump()`.
func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc {
// Defaults
if config.Handler == nil {
panic("echo: body-dump middleware requires a handler function")
}
if config.Skipper == nil {
config.Skipper = DefaultBodyDumpConfig.Skipper
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
// Request
reqBody := []byte{}
if c.Request().Body != nil { // Read
reqBody, _ = ioutil.ReadAll(c.Request().Body)
}
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // Reset
// Response
resBody := new(bytes.Buffer)
mw := io.MultiWriter(c.Response().Writer, resBody)
writer := &bodyDumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer}
c.Response().Writer = writer
if err = next(c); err != nil {
c.Error(err)
}
// Callback
config.Handler(c, reqBody, resBody.Bytes())
return
}
}
}
func (w *bodyDumpResponseWriter) WriteHeader(code int) {
w.ResponseWriter.WriteHeader(code)
}
func (w *bodyDumpResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func (w *bodyDumpResponseWriter) Flush() {
w.ResponseWriter.(http.Flusher).Flush()
}
func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.ResponseWriter.(http.Hijacker).Hijack()
}
func (w *bodyDumpResponseWriter) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

View File

@ -30,7 +30,7 @@ type (
) )
var ( var (
// DefaultBodyLimitConfig is the default Gzip middleware config. // DefaultBodyLimitConfig is the default BodyLimit middleware config.
DefaultBodyLimitConfig = BodyLimitConfig{ DefaultBodyLimitConfig = BodyLimitConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
} }
@ -60,7 +60,7 @@ func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {
limit, err := bytes.Parse(config.Limit) limit, err := bytes.Parse(config.Limit)
if err != nil { if err != nil {
panic(fmt.Errorf("invalid body-limit=%s", config.Limit)) panic(fmt.Errorf("echo: invalid body-limit=%s", config.Limit))
} }
config.limit = limit config.limit = limit
pool := limitedReaderPool(config) pool := limitedReaderPool(config)

View File

@ -67,7 +67,7 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
res := c.Response() res := c.Response()
res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding) res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) { if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) {
res.Header().Add(echo.HeaderContentEncoding, gzipScheme) // Issue #806 res.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
rw := res.Writer rw := res.Writer
w, err := gzip.NewWriterLevel(rw, config.Level) w, err := gzip.NewWriterLevel(rw, config.Level)
if err != nil { if err != nil {
@ -98,6 +98,7 @@ func (w *gzipResponseWriter) WriteHeader(code int) {
if code == http.StatusNoContent { // Issue #489 if code == http.StatusNoContent { // Issue #489
w.ResponseWriter.Header().Del(echo.HeaderContentEncoding) w.ResponseWriter.Header().Del(echo.HeaderContentEncoding)
} }
w.Header().Del(echo.HeaderContentLength) // Issue #444
w.ResponseWriter.WriteHeader(code) w.ResponseWriter.WriteHeader(code)
} }

View File

@ -1,7 +1,6 @@
package middleware package middleware
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@ -57,6 +56,12 @@ const (
AlgorithmHS256 = "HS256" AlgorithmHS256 = "HS256"
) )
// Errors
var (
ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "Missing or malformed jwt")
ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired jwt")
)
var ( var (
// DefaultJWTConfig is the default JWT auth middleware config. // DefaultJWTConfig is the default JWT auth middleware config.
DefaultJWTConfig = JWTConfig{ DefaultJWTConfig = JWTConfig{
@ -77,7 +82,7 @@ var (
// //
// See: https://jwt.io/introduction // See: https://jwt.io/introduction
// See `JWTConfig.TokenLookup` // See `JWTConfig.TokenLookup`
func JWT(key []byte) echo.MiddlewareFunc { func JWT(key interface{}) echo.MiddlewareFunc {
c := DefaultJWTConfig c := DefaultJWTConfig
c.SigningKey = key c.SigningKey = key
return JWTWithConfig(c) return JWTWithConfig(c)
@ -134,14 +139,15 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
auth, err := extractor(c) auth, err := extractor(c)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()) return err
} }
token := new(jwt.Token) token := new(jwt.Token)
// Issue #647, #656 // Issue #647, #656
if _, ok := config.Claims.(jwt.MapClaims); ok { if _, ok := config.Claims.(jwt.MapClaims); ok {
token, err = jwt.Parse(auth, config.keyFunc) token, err = jwt.Parse(auth, config.keyFunc)
} else { } else {
claims := reflect.ValueOf(config.Claims).Interface().(jwt.Claims) t := reflect.ValueOf(config.Claims).Type().Elem()
claims := reflect.New(t).Interface().(jwt.Claims)
token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc) token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc)
} }
if err == nil && token.Valid { if err == nil && token.Valid {
@ -149,7 +155,11 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
c.Set(config.ContextKey, token) c.Set(config.ContextKey, token)
return next(c) return next(c)
} }
return echo.ErrUnauthorized return &echo.HTTPError{
Code: ErrJWTInvalid.Code,
Message: ErrJWTInvalid.Message,
Inner: err,
}
} }
} }
} }
@ -162,7 +172,7 @@ func jwtFromHeader(header string, authScheme string) jwtExtractor {
if len(auth) > l+1 && auth[:l] == authScheme { if len(auth) > l+1 && auth[:l] == authScheme {
return auth[l+1:], nil return auth[l+1:], nil
} }
return "", errors.New("Missing or invalid jwt in the request header") return "", ErrJWTMissing
} }
} }
@ -171,7 +181,7 @@ func jwtFromQuery(param string) jwtExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
token := c.QueryParam(param) token := c.QueryParam(param)
if token == "" { if token == "" {
return "", errors.New("Missing jwt in the query string") return "", ErrJWTMissing
} }
return token, nil return token, nil
} }
@ -182,7 +192,7 @@ func jwtFromCookie(name string) jwtExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
cookie, err := c.Cookie(name) cookie, err := c.Cookie(name)
if err != nil { if err != nil {
return "", errors.New("Missing jwt in the cookie") return "", ErrJWTMissing
} }
return cookie.Value, nil return cookie.Value, nil
} }

View File

@ -72,7 +72,7 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
config.KeyLookup = DefaultKeyAuthConfig.KeyLookup config.KeyLookup = DefaultKeyAuthConfig.KeyLookup
} }
if config.Validator == nil { if config.Validator == nil {
panic("key-auth middleware requires a validator function") panic("echo: key-auth middleware requires a validator function")
} }
// Initialize // Initialize

View File

@ -1,7 +1,6 @@
package middleware package middleware
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
@ -54,35 +53,38 @@ type (
} }
) )
var (
// DefaultProxyConfig is the default Proxy middleware config.
DefaultProxyConfig = ProxyConfig{
Skipper: DefaultSkipper,
}
)
func proxyHTTP(t *ProxyTarget) http.Handler { func proxyHTTP(t *ProxyTarget) http.Handler {
return httputil.NewSingleHostReverseProxy(t.URL) return httputil.NewSingleHostReverseProxy(t.URL)
} }
func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler { func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h, ok := w.(http.Hijacker) in, _, err := c.Response().Hijack()
if !ok {
c.Error(errors.New("proxy raw, not a hijacker"))
return
}
in, _, err := h.Hijack()
if err != nil { if err != nil {
c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", r.URL, err)) c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", t.URL, err))
return return
} }
defer in.Close() defer in.Close()
out, err := net.Dial("tcp", t.URL.Host) out, err := net.Dial("tcp", t.URL.Host)
if err != nil { if err != nil {
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", r.URL, err)) he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", t.URL, err))
c.Error(he) c.Error(he)
return return
} }
defer out.Close() defer out.Close()
// Write header
err = r.Write(out) err = r.Write(out)
if err != nil { if err != nil {
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request copy error=%v, url=%s", r.URL, err)) he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", t.URL, err))
c.Error(he) c.Error(he)
return return
} }
@ -97,7 +99,7 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
go cp(in, out) go cp(in, out)
err = <-errc err = <-errc
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
c.Logger().Errorf("proxy raw, error=%v, url=%s", r.URL, err) c.Logger().Errorf("proxy raw, copy body error=%v, url=%s", t.URL, err)
} }
}) })
} }
@ -118,8 +120,18 @@ func (r *RoundRobinBalancer) Next() *ProxyTarget {
return t return t
} }
// Proxy returns an HTTP/WebSocket reverse proxy middleware. // Proxy returns a Proxy middleware.
func Proxy(config ProxyConfig) echo.MiddlewareFunc { //
// Proxy middleware forwards the request to upstream server using a configured load balancing technique.
func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
c := DefaultProxyConfig
c.Balancer = balancer
return ProxyWithConfig(c)
}
// ProxyWithConfig returns a Proxy middleware with config.
// See: `Proxy()`
func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
// Defaults // Defaults
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultLoggerConfig.Skipper config.Skipper = DefaultLoggerConfig.Skipper
@ -130,6 +142,10 @@ func Proxy(config ProxyConfig) echo.MiddlewareFunc {
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) {
if config.Skipper(c) {
return next(c)
}
req := c.Request() req := c.Request()
res := c.Response() res := c.Response()
tgt := config.Balancer.Next() tgt := config.Balancer.Next()

View File

@ -5,7 +5,6 @@ import (
"runtime" "runtime"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/labstack/gommon/color"
) )
type ( type (
@ -64,17 +63,14 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
var err error err, ok := r.(error)
switch r := r.(type) { if !ok {
case error:
err = r
default:
err = fmt.Errorf("%v", r) err = fmt.Errorf("%v", r)
} }
stack := make([]byte, config.StackSize) stack := make([]byte, config.StackSize)
length := runtime.Stack(stack, !config.DisableStackAll) length := runtime.Stack(stack, !config.DisableStackAll)
if !config.DisablePrintStack { if !config.DisablePrintStack {
c.Logger().Printf("[%s] %s %s\n", color.Red("PANIC RECOVER"), err, stack[:length]) c.Logger().Printf("[PANIC RECOVER] %v %s\n", err, stack[:length])
} }
c.Error(err) c.Error(err)
} }

View File

@ -2,6 +2,7 @@ package middleware
import ( import (
"fmt" "fmt"
"net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -66,7 +67,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
} }
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) (err error) {
if config.Skipper(c) { if config.Skipper(c) {
return next(c) return next(c)
} }
@ -75,17 +76,25 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`. if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
p = c.Param("*") p = c.Param("*")
} }
p, err = echo.PathUnescape(p)
if err != nil {
return
}
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if config.HTML5 && path.Ext(p) == "" { if err = next(c); err != nil {
if he, ok := err.(*echo.HTTPError); ok {
if config.HTML5 && he.Code == http.StatusNotFound {
return c.File(filepath.Join(config.Root, config.Index)) return c.File(filepath.Join(config.Root, config.Index))
} }
return next(c)
} }
return err return
}
}
return
} }
if fi.IsDir() { if fi.IsDir() {
@ -99,7 +108,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return next(c) return next(c)
} }
return err return
} }
return c.File(index) return c.File(index)
@ -110,20 +119,20 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
} }
} }
func listDir(name string, res *echo.Response) error { func listDir(name string, res *echo.Response) (err error) {
dir, err := os.Open(name) dir, err := os.Open(name)
if err != nil { if err != nil {
return err return
} }
dirs, err := dir.Readdir(-1) dirs, err := dir.Readdir(-1)
if err != nil { if err != nil {
return err return
} }
// Create a directory index // Create a directory index
res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
if _, err = fmt.Fprintf(res, "<pre>\n"); err != nil { if _, err = fmt.Fprintf(res, "<pre>\n"); err != nil {
return err return
} }
for _, d := range dirs { for _, d := range dirs {
name := d.Name() name := d.Name()
@ -133,9 +142,9 @@ func listDir(name string, res *echo.Response) error {
name += "/" name += "/"
} }
if _, err = fmt.Fprintf(res, "<a href=\"%s\" style=\"color: %s;\">%s</a>\n", name, color, name); err != nil { if _, err = fmt.Fprintf(res, "<a href=\"%s\" style=\"color: %s;\">%s</a>\n", name, color, name); err != nil {
return err return
} }
} }
_, err = fmt.Fprintf(res, "</pre>\n") _, err = fmt.Fprintf(res, "</pre>\n")
return err return
} }

View File

@ -11,11 +11,12 @@ type (
// by an HTTP handler to construct an HTTP response. // by an HTTP handler to construct an HTTP response.
// See: https://golang.org/pkg/net/http/#ResponseWriter // See: https://golang.org/pkg/net/http/#ResponseWriter
Response struct { Response struct {
echo *Echo
beforeFuncs []func()
Writer http.ResponseWriter Writer http.ResponseWriter
Status int Status int
Size int64 Size int64
Committed bool Committed bool
echo *Echo
} }
) )
@ -34,6 +35,11 @@ func (r *Response) Header() http.Header {
return r.Writer.Header() return r.Writer.Header()
} }
// Before registers a function which is called just before the response is written.
func (r *Response) Before(fn func()) {
r.beforeFuncs = append(r.beforeFuncs, 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
@ -43,6 +49,9 @@ func (r *Response) WriteHeader(code int) {
r.echo.Logger.Warn("response already committed") r.echo.Logger.Warn("response already committed")
return return
} }
for _, fn := range r.beforeFuncs {
fn()
}
r.Status = code r.Status = code
r.Writer.WriteHeader(code) r.Writer.WriteHeader(code)
r.Committed = true r.Committed = true

View File

@ -394,7 +394,7 @@ func (r *Router) Find(method, path string, c Context) {
if cn = cn.findChildByKind(akind); cn == nil { if cn = cn.findChildByKind(akind); cn == nil {
if nn != nil { if nn != nil {
cn = nn cn = nn
nn = nil // Next nn = cn.parent // Next (Issue #954)
search = ns search = ns
if nk == pkind { if nk == pkind {
goto Param goto Param

12
vendor/github.com/labstack/echo/util_go17.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
// +build go1.7, !go1.8
package echo
import (
"net/url"
)
// PathUnescape is wraps `url.QueryUnescape`
func PathUnescape(s string) (string, error) {
return url.QueryUnescape(s)
}

10
vendor/github.com/labstack/echo/util_go18.go generated vendored Normal file
View File

@ -0,0 +1,10 @@
// +build go1.8
package echo
import "net/url"
// PathUnescape is wraps `url.PathUnescape`
func PathUnescape(s string) (string, error) {
return url.PathUnescape(s)
}

View File

@ -16,64 +16,62 @@ func (c *Client) registerBuiltins() {
c.Handlers.mu.Lock() c.Handlers.mu.Lock()
// Built-in things that should always be supported. // Built-in things that should always be supported.
c.Handlers.register(true, RPL_WELCOME, HandlerFunc(func(c *Client, e Event) { c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect))
go handleConnect(c, e) c.Handlers.register(true, false, PING, HandlerFunc(handlePING))
})) c.Handlers.register(true, false, PONG, HandlerFunc(handlePONG))
c.Handlers.register(true, PING, HandlerFunc(handlePING))
c.Handlers.register(true, PONG, HandlerFunc(handlePONG))
if !c.Config.disableTracking { if !c.Config.disableTracking {
// Joins/parts/anything that may add/remove/rename users. // Joins/parts/anything that may add/remove/rename users.
c.Handlers.register(true, JOIN, HandlerFunc(handleJOIN)) c.Handlers.register(true, false, JOIN, HandlerFunc(handleJOIN))
c.Handlers.register(true, PART, HandlerFunc(handlePART)) c.Handlers.register(true, false, PART, HandlerFunc(handlePART))
c.Handlers.register(true, KICK, HandlerFunc(handleKICK)) c.Handlers.register(true, false, KICK, HandlerFunc(handleKICK))
c.Handlers.register(true, QUIT, HandlerFunc(handleQUIT)) c.Handlers.register(true, false, QUIT, HandlerFunc(handleQUIT))
c.Handlers.register(true, NICK, HandlerFunc(handleNICK)) c.Handlers.register(true, false, NICK, HandlerFunc(handleNICK))
c.Handlers.register(true, RPL_NAMREPLY, HandlerFunc(handleNAMES)) c.Handlers.register(true, false, RPL_NAMREPLY, HandlerFunc(handleNAMES))
// Modes. // Modes.
c.Handlers.register(true, MODE, HandlerFunc(handleMODE)) c.Handlers.register(true, false, MODE, HandlerFunc(handleMODE))
c.Handlers.register(true, RPL_CHANNELMODEIS, HandlerFunc(handleMODE)) c.Handlers.register(true, false, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
// WHO/WHOX responses. // WHO/WHOX responses.
c.Handlers.register(true, RPL_WHOREPLY, HandlerFunc(handleWHO)) c.Handlers.register(true, false, RPL_WHOREPLY, HandlerFunc(handleWHO))
c.Handlers.register(true, RPL_WHOSPCRPL, HandlerFunc(handleWHO)) c.Handlers.register(true, false, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
// Other misc. useful stuff. // Other misc. useful stuff.
c.Handlers.register(true, TOPIC, HandlerFunc(handleTOPIC)) c.Handlers.register(true, false, TOPIC, HandlerFunc(handleTOPIC))
c.Handlers.register(true, RPL_TOPIC, HandlerFunc(handleTOPIC)) c.Handlers.register(true, false, RPL_TOPIC, HandlerFunc(handleTOPIC))
c.Handlers.register(true, RPL_MYINFO, HandlerFunc(handleMYINFO)) c.Handlers.register(true, false, RPL_MYINFO, HandlerFunc(handleMYINFO))
c.Handlers.register(true, RPL_ISUPPORT, HandlerFunc(handleISUPPORT)) c.Handlers.register(true, false, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
c.Handlers.register(true, RPL_MOTDSTART, HandlerFunc(handleMOTD)) c.Handlers.register(true, false, RPL_MOTDSTART, HandlerFunc(handleMOTD))
c.Handlers.register(true, RPL_MOTD, HandlerFunc(handleMOTD)) c.Handlers.register(true, false, RPL_MOTD, HandlerFunc(handleMOTD))
// Keep users lastactive times up to date. // Keep users lastactive times up to date.
c.Handlers.register(true, PRIVMSG, HandlerFunc(updateLastActive)) c.Handlers.register(true, false, PRIVMSG, HandlerFunc(updateLastActive))
c.Handlers.register(true, NOTICE, HandlerFunc(updateLastActive)) c.Handlers.register(true, false, NOTICE, HandlerFunc(updateLastActive))
c.Handlers.register(true, TOPIC, HandlerFunc(updateLastActive)) c.Handlers.register(true, false, TOPIC, HandlerFunc(updateLastActive))
c.Handlers.register(true, KICK, HandlerFunc(updateLastActive)) c.Handlers.register(true, false, KICK, HandlerFunc(updateLastActive))
// CAP IRCv3-specific tracking and functionality. // CAP IRCv3-specific tracking and functionality.
c.Handlers.register(true, CAP, HandlerFunc(handleCAP)) c.Handlers.register(true, false, CAP, HandlerFunc(handleCAP))
c.Handlers.register(true, CAP_CHGHOST, HandlerFunc(handleCHGHOST)) c.Handlers.register(true, false, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY)) c.Handlers.register(true, false, CAP_AWAY, HandlerFunc(handleAWAY))
c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT)) c.Handlers.register(true, false, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
c.Handlers.register(true, ALL_EVENTS, HandlerFunc(handleTags)) c.Handlers.register(true, false, ALL_EVENTS, HandlerFunc(handleTags))
// SASL IRCv3 support. // SASL IRCv3 support.
c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL)) c.Handlers.register(true, false, AUTHENTICATE, HandlerFunc(handleSASL))
c.Handlers.register(true, RPL_SASLSUCCESS, HandlerFunc(handleSASL)) c.Handlers.register(true, false, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
c.Handlers.register(true, RPL_NICKLOCKED, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLFAIL, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, ERR_SASLFAIL, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLTOOLONG, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLABORTED, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, ERR_SASLABORTED, HandlerFunc(handleSASLError))
c.Handlers.register(true, RPL_SASLMECHS, HandlerFunc(handleSASLError)) c.Handlers.register(true, false, RPL_SASLMECHS, HandlerFunc(handleSASLError))
} }
// Nickname collisions. // Nickname collisions.
c.Handlers.register(true, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler)) c.Handlers.register(true, false, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler)) c.Handlers.register(true, false, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler)) c.Handlers.register(true, false, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler))
c.Handlers.mu.Unlock() c.Handlers.mu.Unlock()
} }
@ -389,7 +387,7 @@ func handleISUPPORT(c *Client, e Event) {
c.state.Lock() c.state.Lock()
// Skip the first parameter, as it's our nickname. // Skip the first parameter, as it's our nickname.
for i := 1; i < len(e.Params); i++ { for i := 1; i < len(e.Params); i++ {
j := strings.IndexByte(e.Params[i], 0x3D) // = j := strings.IndexByte(e.Params[i], '=')
if j < 1 || (j+1) == len(e.Params[i]) { if j < 1 || (j+1) == len(e.Params[i]) {
c.state.serverOptions[e.Params[i]] = "" c.state.serverOptions[e.Params[i]] = ""

View File

@ -136,7 +136,7 @@ func handleCAP(c *Client, e Event) {
} }
// Let them know which ones we'd like to enable. // Let them know which ones we'd like to enable.
c.write(&Event{Command: CAP, Params: []string{CAP_REQ}, Trailing: strings.Join(c.state.tmpCap, " ")}) c.write(&Event{Command: CAP, Params: []string{CAP_REQ}, Trailing: strings.Join(c.state.tmpCap, " "), EmptyTrailing: true})
// Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests // Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests
// due to cap-notify, we can re-evaluate what we can support. // due to cap-notify, we can re-evaluate what we can support.
@ -375,10 +375,10 @@ func handleTags(c *Client, e Event) {
} }
const ( const (
prefixTag byte = 0x40 // @ prefixTag byte = '@'
prefixTagValue byte = 0x3D // = prefixTagValue byte = '='
prefixUserTag byte = 0x2B // + prefixUserTag byte = '+'
tagSeparator byte = 0x3B // ; tagSeparator byte = ';'
maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included. maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included.
) )
@ -618,7 +618,7 @@ func validTag(name string) bool {
for i := 0; i < len(name); i++ { for i := 0; i < len(name); i++ {
// A-Z, a-z, 0-9, -/._ // A-Z, a-z, 0-9, -/._
if (name[i] < 0x41 || name[i] > 0x5A) && (name[i] < 0x61 || name[i] > 0x7A) && (name[i] < 0x2D || name[i] > 0x39) && name[i] != 0x5F { if (name[i] < 'A' || name[i] > 'Z') && (name[i] < 'a' || name[i] > 'z') && (name[i] < '-' || name[i] > '9') && name[i] != '_' {
return false return false
} }
} }
@ -631,7 +631,7 @@ func validTag(name string) bool {
func validTagValue(value string) bool { func validTagValue(value string) bool {
for i := 0; i < len(value); i++ { for i := 0; i < len(value); i++ {
// Don't allow any invisible chars within the tag, or semicolons. // Don't allow any invisible chars within the tag, or semicolons.
if value[i] < 0x21 || value[i] > 0x7E || value[i] == 0x3B { if value[i] < '!' || value[i] > '~' || value[i] == ';' {
return false return false
} }
} }

View File

@ -191,18 +191,6 @@ func (conf *Config) isValid() error {
// connected. // connected.
var ErrNotConnected = errors.New("client is not connected to server") var ErrNotConnected = errors.New("client is not connected to server")
// ErrDisconnected is called when Config.Retries is less than 1, and we
// non-intentionally disconnected from the server.
var ErrDisconnected = errors.New("unexpectedly disconnected")
// ErrInvalidTarget should be returned if the target which you are
// attempting to send an event to is invalid or doesn't match RFC spec.
type ErrInvalidTarget struct {
Target string
}
func (e *ErrInvalidTarget) Error() string { return "invalid target: " + e.Target }
// New creates a new IRC client with the specified server, name and config. // New creates a new IRC client with the specified server, name and config.
func New(config Config) *Client { func New(config Config) *Client {
c := &Client{ c := &Client{
@ -253,6 +241,37 @@ func (c *Client) String() string {
) )
} }
// TLSConnectionState returns the TLS connection state from tls.Conn{}, which
// is useful to return needed TLS fingerprint info, certificates, verify cert
// expiration dates, etc. Will only return an error if the underlying
// connection wasn't established using TLS (see ErrConnNotTLS), or if the
// client isn't connected.
func (c *Client) TLSConnectionState() (*tls.ConnectionState, error) {
c.mu.RLock()
defer c.mu.RUnlock()
if c.conn == nil {
return nil, ErrNotConnected
}
c.conn.mu.RLock()
defer c.conn.mu.RUnlock()
if !c.conn.connected {
return nil, ErrNotConnected
}
if tlsConn, ok := c.conn.sock.(*tls.Conn); ok {
cs := tlsConn.ConnectionState()
return &cs, nil
}
return nil, ErrConnNotTLS
}
// ErrConnNotTLS is returned when Client.TLSConnectionState() is called, and
// the connection to the server wasn't made with TLS.
var ErrConnNotTLS = errors.New("underlying connection is not tls")
// Close closes the network connection to the server, and sends a STOPPED // Close closes the network connection to the server, and sends a STOPPED
// event. This should cause Connect() to return with nil. This should be // event. This should cause Connect() to return with nil. This should be
// safe to call multiple times. See Connect()'s documentation on how // safe to call multiple times. See Connect()'s documentation on how
@ -387,7 +406,7 @@ func (c *Client) ConnSince() (since *time.Duration, err error) {
} }
// IsConnected returns true if the client is connected to the server. // IsConnected returns true if the client is connected to the server.
func (c *Client) IsConnected() (connected bool) { func (c *Client) IsConnected() bool {
c.mu.RLock() c.mu.RLock()
if c.conn == nil { if c.conn == nil {
c.mu.RUnlock() c.mu.RUnlock()
@ -395,7 +414,7 @@ func (c *Client) IsConnected() (connected bool) {
} }
c.conn.mu.RLock() c.conn.mu.RLock()
connected = c.conn.connected connected := c.conn.connected
c.conn.mu.RUnlock() c.conn.mu.RUnlock()
c.mu.RUnlock() c.mu.RUnlock()
@ -445,9 +464,9 @@ func (c *Client) GetHost() string {
return c.state.host return c.state.host
} }
// Channels returns the active list of channels that the client is in. // ChannelList returns the active list of channel names that the client is in.
// Panics if tracking is disabled. // Panics if tracking is disabled.
func (c *Client) Channels() []string { func (c *Client) ChannelList() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.RLock() c.state.RLock()
@ -463,9 +482,26 @@ func (c *Client) Channels() []string {
return channels return channels
} }
// Users returns the active list of users that the client is tracking across // Channels returns the active channels that the client is in. Panics if
// all files. Panics if tracking is disabled. // tracking is disabled.
func (c *Client) Users() []string { func (c *Client) Channels() []*Channel {
c.panicIfNotTracking()
c.state.RLock()
channels := make([]*Channel, len(c.state.channels))
var i int
for channel := range c.state.channels {
channels[i] = c.state.channels[channel].Copy()
i++
}
c.state.RUnlock()
return channels
}
// UserList returns the active list of nicknames that the client is tracking
// across all networks. Panics if tracking is disabled.
func (c *Client) UserList() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.RLock() c.state.RLock()
@ -481,6 +517,23 @@ func (c *Client) Users() []string {
return users return users
} }
// Users returns the active users that the client is tracking across all
// networks. Panics if tracking is disabled.
func (c *Client) Users() []*User {
c.panicIfNotTracking()
c.state.RLock()
users := make([]*User, len(c.state.users))
var i int
for user := range c.state.users {
users[i] = c.state.users[user].Copy()
i++
}
c.state.RUnlock()
return users
}
// LookupChannel looks up a given channel in state. If the channel doesn't // LookupChannel looks up a given channel in state. If the channel doesn't
// exist, nil is returned. Panics if tracking is disabled. // exist, nil is returned. Panics if tracking is disabled.
func (c *Client) LookupChannel(name string) *Channel { func (c *Client) LookupChannel(name string) *Channel {
@ -562,30 +615,30 @@ func (c *Client) NetworkName() (name string) {
// supplied this information during connection. May be empty if the server // supplied this information during connection. May be empty if the server
// does not support RPL_MYINFO. Will panic if used when tracking has been // does not support RPL_MYINFO. Will panic if used when tracking has been
// disabled. // disabled.
func (c *Client) ServerVersion() (version string) { func (c *Client) ServerVersion() string {
c.panicIfNotTracking() c.panicIfNotTracking()
version, _ = c.GetServerOption("VERSION") version, _ := c.GetServerOption("VERSION")
return version return version
} }
// ServerMOTD returns the servers message of the day, if the server has sent // ServerMOTD returns the servers message of the day, if the server has sent
// it upon connect. Will panic if used when tracking has been disabled. // it upon connect. Will panic if used when tracking has been disabled.
func (c *Client) ServerMOTD() (motd string) { func (c *Client) ServerMOTD() string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.RLock() c.state.RLock()
motd = c.state.motd motd := c.state.motd
c.state.RUnlock() c.state.RUnlock()
return motd return motd
} }
// Lag is the latency between the server and the client. This is measured by // Latency is the latency between the server and the client. This is measured
// determining the difference in time between when we ping the server, and // by determining the difference in time between when we ping the server, and
// when we receive a pong. // when we receive a pong.
func (c *Client) Lag() time.Duration { func (c *Client) Latency() time.Duration {
c.mu.RLock() c.mu.RLock()
c.conn.mu.RLock() c.conn.mu.RLock()
delta := c.conn.lastPong.Sub(c.conn.lastPing) delta := c.conn.lastPong.Sub(c.conn.lastPing)

View File

@ -14,6 +14,7 @@ import (
type Input struct { type Input struct {
Origin *girc.Event Origin *girc.Event
Args []string Args []string
RawArgs string
} }
// Command is an IRC command, supporting aliases, help documentation and easy // Command is an IRC command, supporting aliases, help documentation and easy
@ -191,6 +192,7 @@ func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) {
in := &Input{ in := &Input{
Origin: &event, Origin: &event,
Args: args, Args: args,
RawArgs: parsed[2],
} }
go cmd.Fn(client, in) go cmd.Fn(client, in)

View File

@ -16,18 +16,13 @@ type Commands struct {
} }
// Nick changes the client nickname. // Nick changes the client nickname.
func (cmd *Commands) Nick(name string) error { func (cmd *Commands) Nick(name string) {
if !IsValidNick(name) {
return &ErrInvalidTarget{Target: name}
}
cmd.c.Send(&Event{Command: NICK, Params: []string{name}}) cmd.c.Send(&Event{Command: NICK, Params: []string{name}})
return nil
} }
// Join attempts to enter a list of IRC channels, at bulk if possible to // Join attempts to enter a list of IRC channels, at bulk if possible to
// prevent sending extensive JOIN commands. // prevent sending extensive JOIN commands.
func (cmd *Commands) Join(channels ...string) error { func (cmd *Commands) Join(channels ...string) {
// We can join multiple channels at once, however we need to ensure that // We can join multiple channels at once, however we need to ensure that
// we are not exceeding the line length. (see maxLength) // we are not exceeding the line length. (see maxLength)
max := maxLength - len(JOIN) - 1 max := maxLength - len(JOIN) - 1
@ -35,10 +30,6 @@ func (cmd *Commands) Join(channels ...string) error {
var buffer string var buffer string
for i := 0; i < len(channels); i++ { for i := 0; i < len(channels); i++ {
if !IsValidChannel(channels[i]) {
return &ErrInvalidTarget{Target: channels[i]}
}
if len(buffer+","+channels[i]) > max { if len(buffer+","+channels[i]) > max {
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}}) cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
buffer = "" buffer = ""
@ -53,91 +44,74 @@ func (cmd *Commands) Join(channels ...string) error {
if i == len(channels)-1 { if i == len(channels)-1 {
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}}) cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
return nil return
} }
} }
return nil
} }
// JoinKey attempts to enter an IRC channel with a password. // JoinKey attempts to enter an IRC channel with a password.
func (cmd *Commands) JoinKey(channel, password string) error { func (cmd *Commands) JoinKey(channel, password string) {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
}
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}}) cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
return nil
} }
// Part leaves an IRC channel. // Part leaves an IRC channel.
func (cmd *Commands) Part(channel, message string) error { func (cmd *Commands) Part(channels ...string) {
if !IsValidChannel(channel) { for i := 0; i < len(channels); i++ {
return &ErrInvalidTarget{Target: channel} cmd.c.Send(&Event{Command: PART, Params: []string{channels[i]}})
} }
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}})
return nil
} }
// PartMessage leaves an IRC channel with a specified leave message. // PartMessage leaves an IRC channel with a specified leave message.
func (cmd *Commands) PartMessage(channel, message string) error { func (cmd *Commands) PartMessage(channel, message string) {
if !IsValidChannel(channel) { cmd.c.Send(&Event{Command: PART, Params: []string{channel}, Trailing: message, EmptyTrailing: true})
return &ErrInvalidTarget{Target: channel}
}
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
return nil
} }
// SendCTCP sends a CTCP request to target. Note that this method uses // SendCTCP sends a CTCP request to target. Note that this method uses
// PRIVMSG specifically. // PRIVMSG specifically. ctcpType is the CTCP command, e.g. "FINGER", "TIME",
func (cmd *Commands) SendCTCP(target, ctcpType, message string) error { // "VERSION", etc.
func (cmd *Commands) SendCTCP(target, ctcpType, message string) {
out := encodeCTCPRaw(ctcpType, message) out := encodeCTCPRaw(ctcpType, message)
if out == "" { if out == "" {
return errors.New("invalid CTCP") panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message))
} }
return cmd.Message(target, out) cmd.Message(target, out)
} }
// SendCTCPf sends a CTCP request to target using a specific format. Note that // SendCTCPf sends a CTCP request to target using a specific format. Note that
// this method uses PRIVMSG specifically. // this method uses PRIVMSG specifically. ctcpType is the CTCP command, e.g.
func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) error { // "FINGER", "TIME", "VERSION", etc.
return cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...)) func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) {
cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...))
} }
// SendCTCPReplyf sends a CTCP response to target using a specific format. // SendCTCPReplyf sends a CTCP response to target using a specific format.
// Note that this method uses NOTICE specifically. // Note that this method uses NOTICE specifically. ctcpType is the CTCP
func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error { // command, e.g. "FINGER", "TIME", "VERSION", etc.
return cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...)) func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) {
cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...))
} }
// SendCTCPReply sends a CTCP response to target. Note that this method uses // SendCTCPReply sends a CTCP response to target. Note that this method uses
// NOTICE specifically. // NOTICE specifically.
func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) error { func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) {
out := encodeCTCPRaw(ctcpType, message) out := encodeCTCPRaw(ctcpType, message)
if out == "" { if out == "" {
return errors.New("invalid CTCP") panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message))
} }
return cmd.Notice(target, out) cmd.Notice(target, out)
} }
// Message sends a PRIVMSG to target (either channel, service, or user). // Message sends a PRIVMSG to target (either channel, service, or user).
func (cmd *Commands) Message(target, message string) error { func (cmd *Commands) Message(target, message string) {
if !IsValidNick(target) && !IsValidChannel(target) { cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message, EmptyTrailing: true})
return &ErrInvalidTarget{Target: target}
}
cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
return nil
} }
// Messagef sends a formated PRIVMSG to target (either channel, service, or // Messagef sends a formated PRIVMSG to target (either channel, service, or
// user). // user).
func (cmd *Commands) Messagef(target, format string, a ...interface{}) error { func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
return cmd.Message(target, fmt.Sprintf(format, a...)) cmd.Message(target, fmt.Sprintf(format, a...))
} }
// ErrInvalidSource is returned when a method needs to know the origin of an // ErrInvalidSource is returned when a method needs to know the origin of an
@ -146,94 +120,95 @@ func (cmd *Commands) Messagef(target, format string, a ...interface{}) error {
var ErrInvalidSource = errors.New("event has nil or invalid source address") var ErrInvalidSource = errors.New("event has nil or invalid source address")
// Reply sends a reply to channel or user, based on where the supplied event // Reply sends a reply to channel or user, based on where the supplied event
// originated from. See also ReplyTo(). // originated from. See also ReplyTo(). Panics if the incoming event has no
func (cmd *Commands) Reply(event Event, message string) error { // source.
func (cmd *Commands) Reply(event Event, message string) {
if event.Source == nil { if event.Source == nil {
return ErrInvalidSource panic(ErrInvalidSource)
} }
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
return cmd.Message(event.Params[0], message) cmd.Message(event.Params[0], message)
return
} }
return cmd.Message(event.Source.Name, message) cmd.Message(event.Source.Name, message)
} }
// Replyf sends a reply to channel or user with a format string, based on // Replyf sends a reply to channel or user with a format string, based on
// where the supplied event originated from. See also ReplyTof(). // where the supplied event originated from. See also ReplyTof(). Panics if
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error { // the incoming event has no source.
return cmd.Reply(event, fmt.Sprintf(format, a...)) func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) {
cmd.Reply(event, fmt.Sprintf(format, a...))
} }
// ReplyTo sends a reply to a channel or user, based on where the supplied // ReplyTo sends a reply to a channel or user, based on where the supplied
// event originated from. ReplyTo(), when originating from a channel will // event originated from. ReplyTo(), when originating from a channel will
// default to replying with "<user>, <message>". See also Reply(). // default to replying with "<user>, <message>". See also Reply(). Panics if
func (cmd *Commands) ReplyTo(event Event, message string) error { // the incoming event has no source.
func (cmd *Commands) ReplyTo(event Event, message string) {
if event.Source == nil { if event.Source == nil {
return ErrInvalidSource panic(ErrInvalidSource)
} }
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
return cmd.Message(event.Params[0], event.Source.Name+", "+message) cmd.Message(event.Params[0], event.Source.Name+", "+message)
return
} }
return cmd.Message(event.Source.Name, message) cmd.Message(event.Source.Name, message)
} }
// ReplyTof sends a reply to a channel or user with a format string, based // ReplyTof sends a reply to a channel or user with a format string, based
// on where the supplied event originated from. ReplyTo(), when originating // on where the supplied event originated from. ReplyTo(), when originating
// from a channel will default to replying with "<user>, <message>". See // from a channel will default to replying with "<user>, <message>". See
// also Replyf(). // also Replyf(). Panics if the incoming event has no source.
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error { func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) {
return cmd.ReplyTo(event, fmt.Sprintf(format, a...)) cmd.ReplyTo(event, fmt.Sprintf(format, a...))
} }
// Action sends a PRIVMSG ACTION (/me) to target (either channel, service, // Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
// or user). // or user).
func (cmd *Commands) Action(target, message string) error { func (cmd *Commands) Action(target, message string) {
if !IsValidNick(target) && !IsValidChannel(target) {
return &ErrInvalidTarget{Target: target}
}
cmd.c.Send(&Event{ cmd.c.Send(&Event{
Command: PRIVMSG, Command: PRIVMSG,
Params: []string{target}, Params: []string{target},
Trailing: fmt.Sprintf("\001ACTION %s\001", message), Trailing: fmt.Sprintf("\001ACTION %s\001", message),
}) })
return nil
} }
// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel, // Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
// service, or user). // service, or user).
func (cmd *Commands) Actionf(target, format string, a ...interface{}) error { func (cmd *Commands) Actionf(target, format string, a ...interface{}) {
return cmd.Action(target, fmt.Sprintf(format, a...)) cmd.Action(target, fmt.Sprintf(format, a...))
} }
// Notice sends a NOTICE to target (either channel, service, or user). // Notice sends a NOTICE to target (either channel, service, or user).
func (cmd *Commands) Notice(target, message string) error { func (cmd *Commands) Notice(target, message string) {
if !IsValidNick(target) && !IsValidChannel(target) { cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message, EmptyTrailing: true})
return &ErrInvalidTarget{Target: target}
}
cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
return nil
} }
// Noticef sends a formated NOTICE to target (either channel, service, or // Noticef sends a formated NOTICE to target (either channel, service, or
// user). // user).
func (cmd *Commands) Noticef(target, format string, a ...interface{}) error { func (cmd *Commands) Noticef(target, format string, a ...interface{}) {
return cmd.Notice(target, fmt.Sprintf(format, a...)) cmd.Notice(target, fmt.Sprintf(format, a...))
} }
// SendRaw sends a raw string back to the server, without carriage returns // SendRaw sends a raw string (or multiple) to the server, without carriage
// or newlines. // returns or newlines. Returns an error if one of the raw strings cannot be
func (cmd *Commands) SendRaw(raw string) error { // properly parsed.
e := ParseEvent(raw) func (cmd *Commands) SendRaw(raw ...string) error {
if e == nil { var event *Event
return errors.New("invalid event: " + raw)
for i := 0; i < len(raw); i++ {
event = ParseEvent(raw[i])
if event == nil {
return errors.New("invalid event: " + raw[i])
}
cmd.c.Send(event)
} }
cmd.c.Send(e)
return nil return nil
} }
@ -246,31 +221,26 @@ func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
// Topic sets the topic of channel to message. Does not verify the length // Topic sets the topic of channel to message. Does not verify the length
// of the topic. // of the topic.
func (cmd *Commands) Topic(channel, message string) { func (cmd *Commands) Topic(channel, message string) {
cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message}) cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message, EmptyTrailing: true})
} }
// Who sends a WHO query to the server, which will attempt WHOX by default. // Who sends a WHO query to the server, which will attempt WHOX by default.
// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This // See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This
// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with // sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with
// girc's builtin tracking functionality. // girc's builtin tracking functionality.
func (cmd *Commands) Who(target string) error { func (cmd *Commands) Who(users ...string) {
if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) { for i := 0; i < len(users); i++ {
return &ErrInvalidTarget{Target: target} cmd.c.Send(&Event{Command: WHO, Params: []string{users[i], "%tcuhnr,2"}})
} }
cmd.c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhnr,2"}})
return nil
} }
// Whois sends a WHOIS query to the server, targeted at a specific user. // Whois sends a WHOIS query to the server, targeted at a specific user (or
// as WHOIS is a bit slower, you may want to use WHO for brief user info. // set of users). As WHOIS is a bit slower, you may want to use WHO for brief
func (cmd *Commands) Whois(nick string) error { // user info.
if !IsValidNick(nick) { func (cmd *Commands) Whois(users ...string) {
return &ErrInvalidTarget{Target: nick} for i := 0; i < len(users); i++ {
cmd.c.Send(&Event{Command: WHOIS, Params: []string{users[i]}})
} }
cmd.c.Send(&Event{Command: WHOIS, Params: []string{nick}})
return nil
} }
// Ping sends a PING query to the server, with a specific identifier that // Ping sends a PING query to the server, with a specific identifier that
@ -294,36 +264,40 @@ func (cmd *Commands) Oper(user, pass string) {
// Kick sends a KICK query to the server, attempting to kick nick from // Kick sends a KICK query to the server, attempting to kick nick from
// channel, with reason. If reason is blank, one will not be sent to the // channel, with reason. If reason is blank, one will not be sent to the
// server. // server.
func (cmd *Commands) Kick(channel, nick, reason string) error { func (cmd *Commands) Kick(channel, user, reason string) {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
}
if !IsValidNick(nick) {
return &ErrInvalidTarget{Target: nick}
}
if reason != "" { if reason != "" {
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason}) cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}, Trailing: reason, EmptyTrailing: true})
return nil
} }
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}}) cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}})
return nil }
// Ban adds the +b mode on the given mask on a channel.
func (cmd *Commands) Ban(channel, mask string) {
cmd.Mode(channel, "+b", mask)
}
// Unban removes the +b mode on the given mask on a channel.
func (cmd *Commands) Unban(channel, mask string) {
cmd.Mode(channel, "-b", mask)
}
// Mode sends a mode change to the server which should be applied to target
// (usually a channel or user), along with a set of modes (generally "+m",
// "+mmmm", or "-m", where "m" is the mode you want to change). Params is only
// needed if the mode change requires a parameter (ban or invite-only exclude.)
func (cmd *Commands) Mode(target, modes string, params ...string) {
out := []string{target, modes}
out = append(out, params...)
cmd.c.Send(&Event{Command: MODE, Params: out})
} }
// Invite sends a INVITE query to the server, to invite nick to channel. // Invite sends a INVITE query to the server, to invite nick to channel.
func (cmd *Commands) Invite(channel, nick string) error { func (cmd *Commands) Invite(channel string, users ...string) {
if !IsValidChannel(channel) { for i := 0; i < len(users); i++ {
return &ErrInvalidTarget{Target: channel} cmd.c.Send(&Event{Command: INVITE, Params: []string{users[i], channel}})
} }
if !IsValidNick(nick) {
return &ErrInvalidTarget{Target: nick}
}
cmd.c.Send(&Event{Command: INVITE, Params: []string{nick, channel}})
return nil
} }
// Away sends a AWAY query to the server, suggesting that the client is no // Away sends a AWAY query to the server, suggesting that the client is no
@ -348,10 +322,10 @@ func (cmd *Commands) Back() {
// Supports multiple channels at once, in hopes it will reduce extensive // Supports multiple channels at once, in hopes it will reduce extensive
// LIST queries to the server. Supply no channels to run a list against the // LIST queries to the server. Supply no channels to run a list against the
// entire server (warning, that may mean LOTS of channels!) // entire server (warning, that may mean LOTS of channels!)
func (cmd *Commands) List(channels ...string) error { func (cmd *Commands) List(channels ...string) {
if len(channels) == 0 { if len(channels) == 0 {
cmd.c.Send(&Event{Command: LIST}) cmd.c.Send(&Event{Command: LIST})
return nil return
} }
// We can LIST multiple channels at once, however we need to ensure that // We can LIST multiple channels at once, however we need to ensure that
@ -361,10 +335,6 @@ func (cmd *Commands) List(channels ...string) error {
var buffer string var buffer string
for i := 0; i < len(channels); i++ { for i := 0; i < len(channels); i++ {
if !IsValidChannel(channels[i]) {
return &ErrInvalidTarget{Target: channels[i]}
}
if len(buffer+","+channels[i]) > max { if len(buffer+","+channels[i]) > max {
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}}) cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
buffer = "" buffer = ""
@ -379,20 +349,13 @@ func (cmd *Commands) List(channels ...string) error {
if i == len(channels)-1 { if i == len(channels)-1 {
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}}) cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
return nil return
} }
} }
return nil
} }
// Whowas sends a WHOWAS query to the server. amount is the amount of results // Whowas sends a WHOWAS query to the server. amount is the amount of results
// you want back. // you want back.
func (cmd *Commands) Whowas(nick string, amount int) error { func (cmd *Commands) Whowas(user string, amount int) {
if !IsValidNick(nick) { cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, string(amount)}})
return &ErrInvalidTarget{Target: nick}
}
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}})
return nil
} }

View File

@ -58,7 +58,7 @@ func decodeCTCP(e *Event) *CTCPEvent {
if s < 0 { if s < 0 {
for i := 0; i < len(text); i++ { for i := 0; i < len(text); i++ {
// Check for A-Z, 0-9. // Check for A-Z, 0-9.
if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) { if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
return nil return nil
} }
} }
@ -74,7 +74,7 @@ func decodeCTCP(e *Event) *CTCPEvent {
// Loop through checking the tag first. // Loop through checking the tag first.
for i := 0; i < s; i++ { for i := 0; i < s; i++ {
// Check for A-Z, 0-9. // Check for A-Z, 0-9.
if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) { if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
return nil return nil
} }
} }
@ -168,7 +168,7 @@ func (c *CTCP) parseCMD(cmd string) string {
for i := 0; i < len(cmd); i++ { for i := 0; i < len(cmd); i++ {
// Check for A-Z, 0-9. // Check for A-Z, 0-9.
if (cmd[i] < 0x41 || cmd[i] > 0x5A) && (cmd[i] < 0x30 || cmd[i] > 0x39) { if (cmd[i] < 'A' || cmd[i] > 'Z') && (cmd[i] < '0' || cmd[i] > '9') {
return "" return ""
} }
} }

View File

@ -11,7 +11,7 @@ import (
) )
const ( const (
eventSpace byte = 0x20 // Separator. eventSpace byte = ' ' // Separator.
maxLength = 510 // Maximum length is 510 (2 for line endings). maxLength = 510 // Maximum length is 510 (2 for line endings).
) )
@ -256,7 +256,7 @@ func (e *Event) Bytes() []byte {
// Strip newlines and carriage returns. // Strip newlines and carriage returns.
for i := 0; i < len(out); i++ { for i := 0; i < len(out); i++ {
if out[i] == 0x0A || out[i] == 0x0D { if out[i] == '\n' || out[i] == '\r' {
out = append(out[:i], out[i+1:]...) out = append(out[:i], out[i+1:]...)
i-- // Decrease the index so we can pick up where we left off. i-- // Decrease the index so we can pick up where we left off.
} }
@ -432,9 +432,9 @@ func (e *Event) StripAction() string {
} }
const ( const (
messagePrefix byte = 0x3A // ":" -- prefix or last argument messagePrefix byte = ':' // Prefix or last argument.
prefixIdent byte = 0x21 // "!" -- username prefixIdent byte = '!' // Username.
prefixHost byte = 0x40 // "@" -- hostname prefixHost byte = '@' // Hostname.
) )
// Source represents the sender of an IRC event, see RFC1459 section 2.3.1. // Source represents the sender of an IRC event, see RFC1459 section 2.3.1.

View File

@ -12,8 +12,8 @@ import (
) )
const ( const (
fmtOpenChar = 0x7B // { fmtOpenChar = '{'
fmtCloseChar = 0x7D // } fmtCloseChar = '}'
) )
var fmtColors = map[string]int{ var fmtColors = map[string]int{
@ -113,7 +113,7 @@ func Fmt(text string) string {
if last > -1 { if last > -1 {
// A-Z, a-z, and "," // A-Z, a-z, and ","
if text[i] != 0x2c && (text[i] <= 0x41 || text[i] >= 0x5a) && (text[i] <= 0x61 || text[i] >= 0x7a) { if text[i] != ',' && (text[i] <= 'A' || text[i] >= 'Z') && (text[i] <= 'a' || text[i] >= 'z') {
last = -1 last = -1
continue continue
} }
@ -127,10 +127,10 @@ func Fmt(text string) string {
// See Fmt() for more information. // See Fmt() for more information.
func TrimFmt(text string) string { func TrimFmt(text string) string {
for color := range fmtColors { for color := range fmtColors {
text = strings.Replace(text, "{"+color+"}", "", -1) text = strings.Replace(text, string(fmtOpenChar)+color+string(fmtCloseChar), "", -1)
} }
for code := range fmtCodes { for code := range fmtCodes {
text = strings.Replace(text, "{"+code+"}", "", -1) text = strings.Replace(text, string(fmtOpenChar)+code+string(fmtCloseChar), "", -1)
} }
return text return text
@ -175,9 +175,10 @@ func IsValidChannel(channel string) bool {
return false return false
} }
// #, +, !<channelid>, or & // #, +, !<channelid>, ~, or &
// Including "*" in the prefix list, as this is commonly used (e.g. ZNC) // Including "*" and "~" in the prefix list, as these are commonly used
if bytes.IndexByte([]byte{0x21, 0x23, 0x26, 0x2A, 0x2B}, channel[0]) == -1 { // (e.g. ZNC.)
if bytes.IndexByte([]byte{'!', '#', '&', '*', '~', '+'}, channel[0]) == -1 {
return false return false
} }
@ -186,14 +187,14 @@ func IsValidChannel(channel string) bool {
// 1 (prefix) + 5 (id) + 1 (+, channel name) // 1 (prefix) + 5 (id) + 1 (+, channel name)
// On some networks, this may be extended with ISUPPORT capabilities, // On some networks, this may be extended with ISUPPORT capabilities,
// however this is extremely uncommon. // however this is extremely uncommon.
if channel[0] == 0x21 { if channel[0] == '!' {
if len(channel) < 7 { if len(channel) < 7 {
return false return false
} }
// check for valid ID // check for valid ID
for i := 1; i < 6; i++ { for i := 1; i < 6; i++ {
if (channel[i] < 0x30 || channel[i] > 0x39) && (channel[i] < 0x41 || channel[i] > 0x5A) { if (channel[i] < '0' || channel[i] > '9') && (channel[i] < 'A' || channel[i] > 'Z') {
return false return false
} }
} }
@ -222,17 +223,15 @@ func IsValidNick(nick string) bool {
return false return false
} }
nick = ToRFC1459(nick)
// Check the first index. Some characters aren't allowed for the first // Check the first index. Some characters aren't allowed for the first
// index of an IRC nickname. // index of an IRC nickname.
if nick[0] < 0x41 || nick[0] > 0x7D { if (nick[0] < 'A' || nick[0] > '}') && nick[0] != '?' {
// a-z, A-Z, and _\[]{}^| // a-z, A-Z, '_\[]{}^|', and '?' in the case of znc.
return false return false
} }
for i := 1; i < len(nick); i++ { for i := 1; i < len(nick); i++ {
if (nick[i] < 0x41 || nick[i] > 0x7D) && (nick[i] < 0x30 || nick[i] > 0x39) && nick[i] != 0x2D { if (nick[i] < 'A' || nick[i] > '}') && (nick[i] < '0' || nick[i] > '9') && nick[i] != '-' {
// a-z, A-Z, 0-9, -, and _\[]{}^| // a-z, A-Z, 0-9, -, and _\[]{}^|
return false return false
} }
@ -261,10 +260,8 @@ func IsValidUser(name string) bool {
return false return false
} }
name = ToRFC1459(name)
// "~" is prepended (commonly) if there was no ident server response. // "~" is prepended (commonly) if there was no ident server response.
if name[0] == 0x7E { if name[0] == '~' {
// Means name only contained "~". // Means name only contained "~".
if len(name) < 2 { if len(name) < 2 {
return false return false
@ -274,12 +271,12 @@ func IsValidUser(name string) bool {
} }
// Check to see if the first index is alphanumeric. // Check to see if the first index is alphanumeric.
if (name[0] < 0x41 || name[0] > 0x4A) && (name[0] < 0x61 || name[0] > 0x7A) && (name[0] < 0x30 || name[0] > 0x39) { if (name[0] < 'A' || name[0] > 'J') && (name[0] < 'a' || name[0] > 'z') && (name[0] < '0' || name[0] > '9') {
return false return false
} }
for i := 1; i < len(name); i++ { for i := 1; i < len(name); i++ {
if (name[i] < 0x41 || name[i] > 0x7D) && (name[i] < 0x30 || name[i] > 0x39) && name[i] != 0x2D && name[i] != 0x2E { if (name[i] < 'A' || name[i] > '}') && (name[i] < '0' || name[i] > '9') && name[i] != '-' && name[i] != '.' {
// a-z, A-Z, 0-9, -, and _\[]{}^| // a-z, A-Z, 0-9, -, and _\[]{}^|
return false return false
} }
@ -290,8 +287,13 @@ func IsValidUser(name string) bool {
// ToRFC1459 converts a string to the stripped down conversion within RFC // ToRFC1459 converts a string to the stripped down conversion within RFC
// 1459. This will do things like replace an "A" with an "a", "[]" with "{}", // 1459. This will do things like replace an "A" with an "a", "[]" with "{}",
// and so forth. Useful to compare two nicknames or channels. // and so forth. Useful to compare two nicknames or channels. Note that this
func ToRFC1459(input string) (out string) { // should not be used to normalize nicknames or similar, as this may convert
// valid input characters to non-rfc-valid characters. As such, it's main use
// is for comparing two nicks.
func ToRFC1459(input string) string {
var out string
for i := 0; i < len(input); i++ { for i := 0; i < len(input); i++ {
if input[i] >= 65 && input[i] <= 94 { if input[i] >= 65 && input[i] <= 94 {
out += string(rune(input[i]) + 32) out += string(rune(input[i]) + 32)

View File

@ -29,11 +29,12 @@ func (c *Client) RunHandlers(event *Event) {
} }
} }
// Regular wildcard handlers. // Background handlers first.
c.Handlers.exec(ALL_EVENTS, c, event.Copy()) c.Handlers.exec(ALL_EVENTS, true, c, event.Copy())
c.Handlers.exec(event.Command, true, c, event.Copy())
// Then regular handlers. c.Handlers.exec(ALL_EVENTS, false, c, event.Copy())
c.Handlers.exec(event.Command, c, event.Copy()) c.Handlers.exec(event.Command, false, c, event.Copy())
// Check if it's a CTCP. // Check if it's a CTCP.
if ctcp := decodeCTCP(event.Copy()); ctcp != nil { if ctcp := decodeCTCP(event.Copy()); ctcp != nil {
@ -144,7 +145,7 @@ func (c *Caller) cuid(cmd string, n int) (cuid, uid string) {
// cuidToID allows easy mapping between a generated cuid and the caller // cuidToID allows easy mapping between a generated cuid and the caller
// external/internal handler maps. // external/internal handler maps.
func (c *Caller) cuidToID(input string) (cmd, uid string) { func (c *Caller) cuidToID(input string) (cmd, uid string) {
i := strings.IndexByte(input, 0x3A) i := strings.IndexByte(input, ':')
if i < 0 { if i < 0 {
return "", "" return "", ""
} }
@ -160,9 +161,9 @@ type execStack struct {
// exec executes all handlers pertaining to specified event. Internal first, // exec executes all handlers pertaining to specified event. Internal first,
// then external. // then external.
// //
// Please note that there is no specific order/priority for which the // Please note that there is no specific order/priority for which the handlers
// handler types themselves or the handlers are executed. // are executed.
func (c *Caller) exec(command string, client *Client, event *Event) { func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
// Build a stack of handlers which can be executed concurrently. // Build a stack of handlers which can be executed concurrently.
var stack []execStack var stack []execStack
@ -170,13 +171,21 @@ func (c *Caller) exec(command string, client *Client, event *Event) {
// Get internal handlers first. // Get internal handlers first.
if _, ok := c.internal[command]; ok { if _, ok := c.internal[command]; ok {
for cuid := range c.internal[command] { for cuid := range c.internal[command] {
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
continue
}
stack = append(stack, execStack{c.internal[command][cuid], cuid}) stack = append(stack, execStack{c.internal[command][cuid], cuid})
} }
} }
// Aaand then external handlers. // Then external handlers.
if _, ok := c.external[command]; ok { if _, ok := c.external[command]; ok {
for cuid := range c.external[command] { for cuid := range c.external[command] {
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
continue
}
stack = append(stack, execStack{c.external[command][cuid], cuid}) stack = append(stack, execStack{c.external[command][cuid], cuid})
} }
} }
@ -189,18 +198,29 @@ func (c *Caller) exec(command string, client *Client, event *Event) {
wg.Add(len(stack)) wg.Add(len(stack))
for i := 0; i < len(stack); i++ { for i := 0; i < len(stack); i++ {
go func(index int) { go func(index int) {
c.debug.Printf("executing handler %s for event %s (%d of %d)", stack[index].cuid, command, index+1, len(stack)) defer wg.Done()
c.debug.Printf("[%d/%d] exec %s => %s", index+1, len(stack), stack[index].cuid, command)
start := time.Now() start := time.Now()
// If they want to catch any panics, add to defer stack. if bg {
go func() {
if client.Config.RecoverFunc != nil { if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, event, stack[index].cuid, 3) defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
} }
stack[index].Execute(client, *event) stack[index].Execute(client, *event)
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
}()
c.debug.Printf("execution of %s took %s (%d of %d)", stack[index].cuid, time.Since(start), index+1, len(stack)) return
wg.Done() }
if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
}
stack[index].Execute(client, *event)
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
}(i) }(i)
} }
@ -281,9 +301,9 @@ func (c *Caller) remove(cuid string) (success bool) {
// sregister is much like Caller.register(), except that it safely locks // sregister is much like Caller.register(), except that it safely locks
// the Caller mutex. // the Caller mutex.
func (c *Caller) sregister(internal bool, cmd string, handler Handler) (cuid string) { func (c *Caller) sregister(internal, bg bool, cmd string, handler Handler) (cuid string) {
c.mu.Lock() c.mu.Lock()
cuid = c.register(internal, cmd, handler) cuid = c.register(internal, bg, cmd, handler)
c.mu.Unlock() c.mu.Unlock()
return cuid return cuid
@ -291,30 +311,34 @@ func (c *Caller) sregister(internal bool, cmd string, handler Handler) (cuid str
// register will register a handler in the internal tracker. Unsafe (you // register will register a handler in the internal tracker. Unsafe (you
// must lock c.mu yourself!) // must lock c.mu yourself!)
func (c *Caller) register(internal bool, cmd string, handler Handler) (cuid string) { func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid string) {
var uid string var uid string
cmd = strings.ToUpper(cmd) cmd = strings.ToUpper(cmd)
cuid, uid = c.cuid(cmd, 20)
if bg {
uid += ":bg"
cuid += ":bg"
}
if internal { if internal {
if _, ok := c.internal[cmd]; !ok { if _, ok := c.internal[cmd]; !ok {
c.internal[cmd] = map[string]Handler{} c.internal[cmd] = map[string]Handler{}
} }
cuid, uid = c.cuid(cmd, 20)
c.internal[cmd][uid] = handler c.internal[cmd][uid] = handler
} else { } else {
if _, ok := c.external[cmd]; !ok { if _, ok := c.external[cmd]; !ok {
c.external[cmd] = map[string]Handler{} c.external[cmd] = map[string]Handler{}
} }
cuid, uid = c.cuid(cmd, 20)
c.external[cmd][uid] = handler c.external[cmd][uid] = handler
} }
_, file, line, _ := runtime.Caller(3) _, file, line, _ := runtime.Caller(3)
c.debug.Printf("registering handler for %q with cuid %q (internal: %t) from: %s:%d", cmd, cuid, internal, file, line) c.debug.Printf("reg %q => %s [int:%t bg:%t] %s:%d", uid, cmd, internal, bg, file, line)
return cuid return cuid
} }
@ -323,31 +347,20 @@ func (c *Caller) register(internal bool, cmd string, handler Handler) (cuid stri
// given event. cuid is the handler uid which can be used to remove the // given event. cuid is the handler uid which can be used to remove the
// handler with Caller.Remove(). // handler with Caller.Remove().
func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) { func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) {
return c.sregister(false, cmd, handler) return c.sregister(false, false, cmd, handler)
} }
// Add registers the handler function for the given event. cuid is the // Add registers the handler function for the given event. cuid is the
// handler uid which can be used to remove the handler with Caller.Remove(). // handler uid which can be used to remove the handler with Caller.Remove().
func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) { func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) {
return c.sregister(false, cmd, HandlerFunc(handler)) return c.sregister(false, false, cmd, HandlerFunc(handler))
} }
// AddBg registers the handler function for the given event and executes it // AddBg registers the handler function for the given event and executes it
// in a go-routine. cuid is the handler uid which can be used to remove the // in a go-routine. cuid is the handler uid which can be used to remove the
// handler with Caller.Remove(). // handler with Caller.Remove().
func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) { func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) {
return c.sregister(false, cmd, HandlerFunc(func(client *Client, event Event) { return c.sregister(false, true, cmd, HandlerFunc(handler))
// Setting up background-based handlers this way allows us to get
// clean call stacks for use with panic recovery.
go func() {
// If they want to catch any panics, add to defer stack.
if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, &event, "goroutine", 3)
}
handler(client, event)
}()
}))
} }
// AddTmp adds a "temporary" handler, which is good for one-time or few-time // AddTmp adds a "temporary" handler, which is good for one-time or few-time
@ -361,47 +374,37 @@ func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (c
// //
// Additionally, AddTmp has a useful option, deadline. When set to greater // Additionally, AddTmp has a useful option, deadline. When set to greater
// than 0, deadline will be the amount of time that passes before the handler // than 0, deadline will be the amount of time that passes before the handler
// is removed from the stack, regardless if the handler returns true or not. // is removed from the stack, regardless of if the handler returns true or not.
// This is useful in that it ensures that the handler is cleaned up if the // This is useful in that it ensures that the handler is cleaned up if the
// server does not respond appropriately, or takes too long to respond. // server does not respond appropriately, or takes too long to respond.
// //
// Note that handlers supplied with AddTmp are executed in a goroutine to // Note that handlers supplied with AddTmp are executed in a goroutine to
// ensure that they are not blocking other handlers. Additionally, use cuid // ensure that they are not blocking other handlers. However, if you are
// with Caller.Remove() to prematurely remove the handler from the stack, // creating a temporary handler from another handler, it should be a
// bypassing the timeout or waiting for the handler to return that it wants // background handler.
// to be removed from the stack. //
// Use cuid with Caller.Remove() to prematurely remove the handler from the
// stack, bypassing the timeout or waiting for the handler to return that it
// wants to be removed from the stack.
func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) { func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) {
var uid string
cuid, uid = c.cuid(cmd, 20)
done = make(chan struct{}) done = make(chan struct{})
c.mu.Lock() cuid = c.sregister(false, true, cmd, HandlerFunc(func(client *Client, event Event) {
if _, ok := c.external[cmd]; !ok {
c.external[cmd] = map[string]Handler{}
}
c.external[cmd][uid] = HandlerFunc(func(client *Client, event Event) {
// Setting up background-based handlers this way allows us to get
// clean call stacks for use with panic recovery.
go func() {
// If they want to catch any panics, add to defer stack.
if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, &event, "tmp-goroutine", 3)
}
remove := handler(client, event) remove := handler(client, event)
if remove { if remove {
if ok := c.Remove(cuid); ok { if ok := c.Remove(cuid); ok {
close(done) close(done)
} }
} }
}() }))
})
c.mu.Unlock()
if deadline > 0 { if deadline > 0 {
go func() { go func() {
<-time.After(deadline) select {
case <-time.After(deadline):
case <-done:
}
if ok := c.Remove(cuid); ok { if ok := c.Remove(cuid); ok {
close(done) close(done)
} }

View File

@ -206,11 +206,11 @@ func (c *CModes) Parse(flags string, args []string) (out []CMode) {
var argCount int var argCount int
for i := 0; i < len(flags); i++ { for i := 0; i < len(flags); i++ {
if flags[i] == 0x2B { if flags[i] == '+' {
add = true add = true
continue continue
} }
if flags[i] == 0x2D { if flags[i] == '-' {
add = false add = false
continue continue
} }
@ -265,7 +265,7 @@ func IsValidChannelMode(raw string) bool {
for i := 0; i < len(raw); i++ { for i := 0; i < len(raw); i++ {
// Allowed are: ",", A-Z and a-z. // Allowed are: ",", A-Z and a-z.
if raw[i] != 0x2C && (raw[i] < 0x41 || raw[i] > 0x5A) && (raw[i] < 0x61 || raw[i] > 0x7A) { if raw[i] != ',' && (raw[i] < 'A' || raw[i] > 'Z') && (raw[i] < 'a' || raw[i] > 'z') {
return false return false
} }
} }
@ -279,7 +279,7 @@ func isValidUserPrefix(raw string) bool {
return false return false
} }
if raw[0] != 0x28 { // (. if raw[0] != '(' {
return false return false
} }
@ -288,7 +288,7 @@ func isValidUserPrefix(raw string) bool {
// Skip the first one as we know it's (. // Skip the first one as we know it's (.
for i := 1; i < len(raw); i++ { for i := 1; i < len(raw); i++ {
if raw[i] == 0x29 { // ). if raw[i] == ')' {
passedKeys = true passedKeys = true
continue continue
} }

View File

@ -79,7 +79,7 @@ func (cli *Client) BuildBaseURL(urlPath ...string) string {
return hsURL.String() return hsURL.String()
} }
// BuildURLWithQuery builds a URL with query paramters in addition to the Client's homeserver/prefix/access_token set already. // BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already.
func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string { func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string {
u, _ := url.Parse(cli.BuildURL(urlPath...)) u, _ := url.Parse(cli.BuildURL(urlPath...))
q := u.Query() q := u.Query()
@ -387,6 +387,20 @@ func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{
return return
} }
// GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", mxid, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname // SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname
func (cli *Client) SetDisplayName(displayName string) (err error) { func (cli *Client) SetDisplayName(displayName string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname") urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
@ -450,6 +464,35 @@ func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
TextMessage{"m.text", text}) TextMessage{"m.text", text})
} }
// SendImage sends an m.room.message event into the given room with a msgtype of m.image
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
ImageMessage{
MsgType: "m.image",
Body: body,
URL: url,
})
}
// SendVideo sends an m.room.message event into the given room with a msgtype of m.video
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
VideoMessage{
MsgType: "m.video",
Body: body,
URL: url,
})
}
// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) {
return cli.SendMessageEvent(roomID, "m.room.message",
TextMessage{"m.notice", text})
}
// RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid // RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) { func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) {
txnID := txnID() txnID := txnID()
@ -518,6 +561,14 @@ func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanU
return return
} }
// UserTyping sets the typing status of the user. See https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
func (cli *Client) UserTyping(roomID string, typing bool, timeout int64) (resp *RespTyping, err error) {
req := ReqTyping{Typing: typing, Timeout: timeout}
u := cli.BuildURL("rooms", roomID, "typing", cli.UserID)
_, err = cli.MakeRequest("PUT", u, req, &resp)
return
}
// StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with // StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with
// the HTTP response body, or return an error. // the HTTP response body, or return an error.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey // See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
@ -556,8 +607,15 @@ func (cli *Client) UploadToContentRepo(content io.Reader, contentType string, co
return nil, err return nil, err
} }
if res.StatusCode != 200 { if res.StatusCode != 200 {
contents, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, HTTPError{ return nil, HTTPError{
Message: "Upload request failed", Message: "Upload request failed - Failed to read response body: " + err.Error(),
Code: res.StatusCode,
}
}
return nil, HTTPError{
Message: "Upload request failed: " + string(contents),
Code: res.StatusCode, Code: res.StatusCode,
} }
} }
@ -588,6 +646,34 @@ func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
return return
} }
// Messages returns a list of message and state events for a room. It uses
// pagination query parameters to paginate history in the room.
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) {
query := map[string]string{
"from": from,
"dir": string(dir),
}
if to != "" {
query["to"] = to
}
if limit != 0 {
query["limit"] = strconv.Itoa(limit)
}
urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query)
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
// TurnServer returns turn server details and credentials for the client to use when initiating calls.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-voip-turnserver
func (cli *Client) TurnServer() (resp *RespTurnServer, err error) {
urlPath := cli.BuildURL("voip", "turnServer")
_, err = cli.MakeRequest("GET", urlPath, nil, &resp)
return
}
func txnID() string { func txnID() string {
return "go" + strconv.FormatInt(time.Now().UnixNano(), 10) return "go" + strconv.FormatInt(time.Now().UnixNano(), 10)
} }

View File

@ -7,10 +7,10 @@ import (
// Event represents a single Matrix event. // Event represents a single Matrix event.
type Event struct { type Event struct {
StateKey string `json:"state_key"` // The state key for the event. Only present on State Events. StateKey *string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events.
Sender string `json:"sender"` // The user ID of the sender of the event Sender string `json:"sender"` // The user ID of the sender of the event
Type string `json:"type"` // The event type Type string `json:"type"` // The event type
Timestamp int `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server
ID string `json:"event_id"` // The unique ID of this event ID string `json:"event_id"` // The unique ID of this event
RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence) RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence)
Content map[string]interface{} `json:"content"` // The JSON content of the event. Content map[string]interface{} `json:"content"` // The JSON content of the event.
@ -44,12 +44,31 @@ type TextMessage struct {
Body string `json:"body"` Body string `json:"body"`
} }
// ImageInfo contains info about an image // ImageInfo contains info about an image - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
type ImageInfo struct { type ImageInfo struct {
Height uint `json:"h"` Height uint `json:"h,omitempty"`
Width uint `json:"w"` Width uint `json:"w,omitempty"`
Mimetype string `json:"mimetype"` Mimetype string `json:"mimetype,omitempty"`
Size uint `json:"size"` Size uint `json:"size,omitempty"`
}
// VideoInfo contains info about a video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoInfo struct {
Mimetype string `json:"mimetype,omitempty"`
ThumbnailInfo ImageInfo `json:"thumbnail_info"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Duration uint `json:"duration,omitempty"`
Size uint `json:"size,omitempty"`
}
// VideoMessage is an m.video - http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
type VideoMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
URL string `json:"url"`
Info VideoInfo `json:"info"`
} }
// ImageMessage is an m.image event // ImageMessage is an m.image event

43
vendor/github.com/matrix-org/gomatrix/filter.go generated vendored Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2017 Jan Christian Grünhage
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomatrix
//Filter is used by clients to specify how the server should filter responses to e.g. sync requests
//Specified by: https://matrix.org/docs/spec/client_server/r0.2.0.html#filtering
type Filter struct {
AccountData FilterPart `json:"account_data,omitempty"`
EventFields []string `json:"event_fields,omitempty"`
EventFormat string `json:"event_format,omitempty"`
Presence FilterPart `json:"presence,omitempty"`
Room struct {
AccountData FilterPart `json:"account_data,omitempty"`
Ephemeral FilterPart `json:"ephemeral,omitempty"`
IncludeLeave bool `json:"include_leave,omitempty"`
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
State FilterPart `json:"state,omitempty"`
Timeline FilterPart `json:"timeline,omitempty"`
} `json:"room,omitempty"`
}
type FilterPart struct {
NotRooms []string `json:"not_rooms,omitempty"`
Rooms []string `json:"rooms,omitempty"`
Limit *int `json:"limit,omitempty"`
NotSenders []string `json:"not_senders,omitempty"`
NotTypes []string `json:"not_types,omitempty"`
Senders []string `json:"senders,omitempty"`
Types []string `json:"types,omitempty"`
}

View File

@ -70,3 +70,9 @@ type ReqBanUser struct {
type ReqUnbanUser struct { type ReqUnbanUser struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
} }
// ReqTyping is the JSON request for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type ReqTyping struct {
Typing bool `json:"typing"`
Timeout int64 `json:"timeout"`
}

View File

@ -45,6 +45,9 @@ type RespBanUser struct{}
// RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban // RespUnbanUser is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
type RespUnbanUser struct{} type RespUnbanUser struct{}
// RespTyping is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
type RespTyping struct{}
// RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680 // RespJoinedRooms is the JSON response for TODO-SPEC https://github.com/matrix-org/synapse/pull/1680
type RespJoinedRooms struct { type RespJoinedRooms struct {
JoinedRooms []string `json:"joined_rooms"` JoinedRooms []string `json:"joined_rooms"`
@ -58,6 +61,13 @@ type RespJoinedMembers struct {
} `json:"joined"` } `json:"joined"`
} }
// RespMessages is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
type RespMessages struct {
Start string `json:"start"`
Chunk []Event `json:"chunk"`
End string `json:"end"`
}
// RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid // RespSendEvent is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
type RespSendEvent struct { type RespSendEvent struct {
EventID string `json:"event_id"` EventID string `json:"event_id"`
@ -90,6 +100,11 @@ func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool {
return false return false
} }
// RespUserDisplayName is the JSON response for https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
type RespUserDisplayName struct {
DisplayName string `json:"displayname"`
}
// RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register // RespRegister is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
type RespRegister struct { type RespRegister struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
@ -125,6 +140,16 @@ type RespSync struct {
Events []Event `json:"events"` Events []Event `json:"events"`
} `json:"presence"` } `json:"presence"`
Rooms struct { Rooms struct {
Leave map[string]struct {
State struct {
Events []Event `json:"events"`
} `json:"state"`
Timeline struct {
Events []Event `json:"events"`
Limited bool `json:"limited"`
PrevBatch string `json:"prev_batch"`
} `json:"timeline"`
} `json:"leave"`
Join map[string]struct { Join map[string]struct {
State struct { State struct {
Events []Event `json:"events"` Events []Event `json:"events"`
@ -142,3 +167,10 @@ type RespSync struct {
} `json:"invite"` } `json:"invite"`
} `json:"rooms"` } `json:"rooms"`
} }
type RespTurnServer struct {
Username string `json:"username"`
Password string `json:"password"`
TTL int `json:"ttl"`
URIs []string `json:"uris"`
}

View File

@ -13,7 +13,7 @@ func (room Room) UpdateState(event *Event) {
if !exists { if !exists {
room.State[event.Type] = make(map[string]*Event) room.State[event.Type] = make(map[string]*Event)
} }
room.State[event.Type][event.StateKey] = event room.State[event.Type][*event.StateKey] = event
} }
// GetStateEvent returns the state event for the given type/state_key combo, or nil. // GetStateEvent returns the state event for the given type/state_key combo, or nil.

View File

@ -73,6 +73,16 @@ func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error)
s.notifyListeners(&event) s.notifyListeners(&event)
} }
} }
for roomID, roomData := range res.Rooms.Leave {
room := s.getOrCreateRoom(roomID)
for _, event := range roomData.Timeline.Events {
if event.StateKey != nil {
event.RoomID = roomID
room.UpdateState(&event)
s.notifyListeners(&event)
}
}
}
return return
} }
@ -102,7 +112,7 @@ func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool
for roomID, roomData := range resp.Rooms.Join { for roomID, roomData := range resp.Rooms.Join {
for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- { for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
e := roomData.Timeline.Events[i] e := roomData.Timeline.Events[i]
if e.Type == "m.room.member" && e.StateKey == s.UserID { if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.UserID {
m := e.Content["membership"] m := e.Content["membership"]
mship, ok := m.(string) mship, ok := m.(string)
if !ok { if !ok {

View File

@ -1,20 +0,0 @@
package slack
import (
"net"
"net/url"
)
var portMapping = map[string]string{"ws": "80", "wss": "443"}
func websocketizeURLPort(orig string) (string, error) {
urlObj, err := url.ParseRequestURI(orig)
if err != nil {
return "", err
}
_, _, err = net.SplitHostPort(urlObj.Host)
if err != nil {
return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil
}
return orig, nil
}

View File

@ -25,6 +25,7 @@ type AttachmentAction struct {
SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu. SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu.
OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional. OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional.
Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional. Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional.
URL string `json:"url,omitempty"` // Optional.
} }
// AttachmentActionOption the individual option to appear in action menu. // AttachmentActionOption the individual option to appear in action menu.
@ -48,6 +49,9 @@ type AttachmentActionCallback struct {
Channel Channel `json:"channel"` Channel Channel `json:"channel"`
User User `json:"user"` User User `json:"user"`
Name string `json:"name"`
Value string `json:"value"`
OriginalMessage Message `json:"original_message"` OriginalMessage Message `json:"original_message"`
ActionTs string `json:"action_ts"` ActionTs string `json:"action_ts"`

View File

@ -38,51 +38,51 @@ func channelRequest(ctx context.Context, path string, values url.Values, debug b
} }
// ArchiveChannel archives the given channel // ArchiveChannel archives the given channel
func (api *Client) ArchiveChannel(channel string) error { // see https://api.slack.com/methods/channels.archive
return api.ArchiveChannelContext(context.Background(), channel) func (api *Client) ArchiveChannel(channelID string) error {
return api.ArchiveChannelContext(context.Background(), channelID)
} }
// ArchiveChannelContext archives the given channel with a custom context // ArchiveChannelContext archives the given channel with a custom context
func (api *Client) ArchiveChannelContext(ctx context.Context, channel string) error { // see https://api.slack.com/methods/channels.archive
func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
} }
_, err := channelRequest(ctx, "channels.archive", values, api.debug) _, err := channelRequest(ctx, "channels.archive", values, api.debug)
if err != nil {
return err return err
}
return nil
} }
// UnarchiveChannel unarchives the given channel // UnarchiveChannel unarchives the given channel
func (api *Client) UnarchiveChannel(channel string) error { // see https://api.slack.com/methods/channels.unarchive
return api.UnarchiveChannelContext(context.Background(), channel) func (api *Client) UnarchiveChannel(channelID string) error {
return api.UnarchiveChannelContext(context.Background(), channelID)
} }
// UnarchiveChannelContext unarchives the given channel with a custom context // UnarchiveChannelContext unarchives the given channel with a custom context
func (api *Client) UnarchiveChannelContext(ctx context.Context, channel string) error { // see https://api.slack.com/methods/channels.unarchive
func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
} }
_, err := channelRequest(ctx, "channels.unarchive", values, api.debug) _, err := channelRequest(ctx, "channels.unarchive", values, api.debug)
if err != nil {
return err return err
}
return nil
} }
// CreateChannel creates a channel with the given name and returns a *Channel // CreateChannel creates a channel with the given name and returns a *Channel
func (api *Client) CreateChannel(channel string) (*Channel, error) { // see https://api.slack.com/methods/channels.create
return api.CreateChannelContext(context.Background(), channel) func (api *Client) CreateChannel(channelName string) (*Channel, error) {
return api.CreateChannelContext(context.Background(), channelName)
} }
// CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context // CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context
func (api *Client) CreateChannelContext(ctx context.Context, channel string) (*Channel, error) { // see https://api.slack.com/methods/channels.create
func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"name": {channel}, "name": {channelName},
} }
response, err := channelRequest(ctx, "channels.create", values, api.debug) response, err := channelRequest(ctx, "channels.create", values, api.debug)
if err != nil { if err != nil {
@ -92,15 +92,17 @@ func (api *Client) CreateChannelContext(ctx context.Context, channel string) (*C
} }
// GetChannelHistory retrieves the channel history // GetChannelHistory retrieves the channel history
func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) { // see https://api.slack.com/methods/channels.history
return api.GetChannelHistoryContext(context.Background(), channel, params) func (api *Client) GetChannelHistory(channelID string, params HistoryParameters) (*History, error) {
return api.GetChannelHistoryContext(context.Background(), channelID, params)
} }
// GetChannelHistoryContext retrieves the channel history with a custom context // GetChannelHistoryContext retrieves the channel history with a custom context
func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) { // see https://api.slack.com/methods/channels.history
func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
} }
if params.Latest != DEFAULT_HISTORY_LATEST { if params.Latest != DEFAULT_HISTORY_LATEST {
values.Add("latest", params.Latest) values.Add("latest", params.Latest)
@ -133,15 +135,17 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string,
} }
// GetChannelInfo retrieves the given channel // GetChannelInfo retrieves the given channel
func (api *Client) GetChannelInfo(channel string) (*Channel, error) { // see https://api.slack.com/methods/channels.info
return api.GetChannelInfoContext(context.Background(), channel) func (api *Client) GetChannelInfo(channelID string) (*Channel, error) {
return api.GetChannelInfoContext(context.Background(), channelID)
} }
// GetChannelInfoContext retrieves the given channel with a custom context // GetChannelInfoContext retrieves the given channel with a custom context
func (api *Client) GetChannelInfoContext(ctx context.Context, channel string) (*Channel, error) { // see https://api.slack.com/methods/channels.info
func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
} }
response, err := channelRequest(ctx, "channels.info", values, api.debug) response, err := channelRequest(ctx, "channels.info", values, api.debug)
if err != nil { if err != nil {
@ -151,15 +155,17 @@ func (api *Client) GetChannelInfoContext(ctx context.Context, channel string) (*
} }
// InviteUserToChannel invites a user to a given channel and returns a *Channel // InviteUserToChannel invites a user to a given channel and returns a *Channel
func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) { // see https://api.slack.com/methods/channels.invite
return api.InviteUserToChannelContext(context.Background(), channel, user) func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) {
return api.InviteUserToChannelContext(context.Background(), channelID, user)
} }
// InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context // InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context
func (api *Client) InviteUserToChannelContext(ctx context.Context, channel, user string) (*Channel, error) { // see https://api.slack.com/methods/channels.invite
func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
"user": {user}, "user": {user},
} }
response, err := channelRequest(ctx, "channels.invite", values, api.debug) response, err := channelRequest(ctx, "channels.invite", values, api.debug)
@ -170,15 +176,17 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channel, user
} }
// JoinChannel joins the currently authenticated user to a channel // JoinChannel joins the currently authenticated user to a channel
func (api *Client) JoinChannel(channel string) (*Channel, error) { // see https://api.slack.com/methods/channels.join
return api.JoinChannelContext(context.Background(), channel) func (api *Client) JoinChannel(channelName string) (*Channel, error) {
return api.JoinChannelContext(context.Background(), channelName)
} }
// JoinChannelContext joins the currently authenticated user to a channel with a custom context // JoinChannelContext joins the currently authenticated user to a channel with a custom context
func (api *Client) JoinChannelContext(ctx context.Context, channel string) (*Channel, error) { // see https://api.slack.com/methods/channels.join
func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"name": {channel}, "name": {channelName},
} }
response, err := channelRequest(ctx, "channels.join", values, api.debug) response, err := channelRequest(ctx, "channels.join", values, api.debug)
if err != nil { if err != nil {
@ -188,15 +196,17 @@ func (api *Client) JoinChannelContext(ctx context.Context, channel string) (*Cha
} }
// LeaveChannel makes the authenticated user leave the given channel // LeaveChannel makes the authenticated user leave the given channel
func (api *Client) LeaveChannel(channel string) (bool, error) { // see https://api.slack.com/methods/channels.leave
return api.LeaveChannelContext(context.Background(), channel) func (api *Client) LeaveChannel(channelID string) (bool, error) {
return api.LeaveChannelContext(context.Background(), channelID)
} }
// LeaveChannelContext makes the authenticated user leave the given channel with a custom context // LeaveChannelContext makes the authenticated user leave the given channel with a custom context
func (api *Client) LeaveChannelContext(ctx context.Context, channel string) (bool, error) { // see https://api.slack.com/methods/channels.leave
func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
} }
response, err := channelRequest(ctx, "channels.leave", values, api.debug) response, err := channelRequest(ctx, "channels.leave", values, api.debug)
if err != nil { if err != nil {
@ -209,30 +219,31 @@ func (api *Client) LeaveChannelContext(ctx context.Context, channel string) (boo
} }
// KickUserFromChannel kicks a user from a given channel // KickUserFromChannel kicks a user from a given channel
func (api *Client) KickUserFromChannel(channel, user string) error { // see https://api.slack.com/methods/channels.kick
return api.KickUserFromChannelContext(context.Background(), channel, user) func (api *Client) KickUserFromChannel(channelID, user string) error {
return api.KickUserFromChannelContext(context.Background(), channelID, user)
} }
// KickUserFromChannelContext kicks a user from a given channel with a custom context // KickUserFromChannelContext kicks a user from a given channel with a custom context
func (api *Client) KickUserFromChannelContext(ctx context.Context, channel, user string) error { // see https://api.slack.com/methods/channels.kick
func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
"user": {user}, "user": {user},
} }
_, err := channelRequest(ctx, "channels.kick", values, api.debug) _, err := channelRequest(ctx, "channels.kick", values, api.debug)
if err != nil {
return err return err
}
return nil
} }
// GetChannels retrieves all the channels // GetChannels retrieves all the channels
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) { func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived) return api.GetChannelsContext(context.Background(), excludeArchived)
} }
// GetChannelsContext retrieves all the channels with a custom context // GetChannelsContext retrieves all the channels with a custom context
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) { func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
@ -252,35 +263,36 @@ func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool)
// timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls // timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls
// (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A // (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A
// timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. // timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout.
func (api *Client) SetChannelReadMark(channel, ts string) error { // see https://api.slack.com/methods/channels.mark
return api.SetChannelReadMarkContext(context.Background(), channel, ts) func (api *Client) SetChannelReadMark(channelID, ts string) error {
return api.SetChannelReadMarkContext(context.Background(), channelID, ts)
} }
// SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context // SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context
// For more details see SetChannelReadMark documentation // For more details see SetChannelReadMark documentation
func (api *Client) SetChannelReadMarkContext(ctx context.Context, channel, ts string) error { // see https://api.slack.com/methods/channels.mark
func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
"ts": {ts}, "ts": {ts},
} }
_, err := channelRequest(ctx, "channels.mark", values, api.debug) _, err := channelRequest(ctx, "channels.mark", values, api.debug)
if err != nil {
return err return err
}
return nil
} }
// RenameChannel renames a given channel // RenameChannel renames a given channel
func (api *Client) RenameChannel(channel, name string) (*Channel, error) { // see https://api.slack.com/methods/channels.rename
return api.RenameChannelContext(context.Background(), channel, name) func (api *Client) RenameChannel(channelID, name string) (*Channel, error) {
return api.RenameChannelContext(context.Background(), channelID, name)
} }
// RenameChannelContext renames a given channel with a custom context // RenameChannelContext renames a given channel with a custom context
func (api *Client) RenameChannelContext(ctx context.Context, channel, name string) (*Channel, error) { // see https://api.slack.com/methods/channels.rename
func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
"name": {name}, "name": {name},
} }
// XXX: the created entry in this call returns a string instead of a number // XXX: the created entry in this call returns a string instead of a number
@ -293,15 +305,17 @@ func (api *Client) RenameChannelContext(ctx context.Context, channel, name strin
} }
// SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set // SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set
func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) { // see https://api.slack.com/methods/channels.setPurpose
return api.SetChannelPurposeContext(context.Background(), channel, purpose) func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error) {
return api.SetChannelPurposeContext(context.Background(), channelID, purpose)
} }
// SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context // SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context
func (api *Client) SetChannelPurposeContext(ctx context.Context, channel, purpose string) (string, error) { // see https://api.slack.com/methods/channels.setPurpose
func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
"purpose": {purpose}, "purpose": {purpose},
} }
response, err := channelRequest(ctx, "channels.setPurpose", values, api.debug) response, err := channelRequest(ctx, "channels.setPurpose", values, api.debug)
@ -312,15 +326,17 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channel, purpos
} }
// SetChannelTopic sets the channel topic and returns the topic that was successfully set // SetChannelTopic sets the channel topic and returns the topic that was successfully set
func (api *Client) SetChannelTopic(channel, topic string) (string, error) { // see https://api.slack.com/methods/channels.setTopic
return api.SetChannelTopicContext(context.Background(), channel, topic) func (api *Client) SetChannelTopic(channelID, topic string) (string, error) {
return api.SetChannelTopicContext(context.Background(), channelID, topic)
} }
// SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context // SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context
func (api *Client) SetChannelTopicContext(ctx context.Context, channel, topic string) (string, error) { // see https://api.slack.com/methods/channels.setTopic
func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
"topic": {topic}, "topic": {topic},
} }
response, err := channelRequest(ctx, "channels.setTopic", values, api.debug) response, err := channelRequest(ctx, "channels.setTopic", values, api.debug)
@ -331,15 +347,17 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channel, topic st
} }
// GetChannelReplies gets an entire thread (a message plus all the messages in reply to it). // GetChannelReplies gets an entire thread (a message plus all the messages in reply to it).
func (api *Client) GetChannelReplies(channel, thread_ts string) ([]Message, error) { // see https://api.slack.com/methods/channels.replies
return api.GetChannelRepliesContext(context.Background(), channel, thread_ts) func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, error) {
return api.GetChannelRepliesContext(context.Background(), channelID, thread_ts)
} }
// GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context // GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context
func (api *Client) GetChannelRepliesContext(ctx context.Context, channel, thread_ts string) ([]Message, error) { // see https://api.slack.com/methods/channels.replies
func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.config.token},
"channel": {channel}, "channel": {channelID},
"thread_ts": {thread_ts}, "thread_ts": {thread_ts},
} }
response, err := channelRequest(ctx, "channels.replies", values, api.debug) response, err := channelRequest(ctx, "channels.replies", values, api.debug)

View File

@ -11,6 +11,7 @@ import (
const ( const (
DEFAULT_MESSAGE_USERNAME = "" DEFAULT_MESSAGE_USERNAME = ""
DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
DEFAULT_MESSAGE_REPLY_BROADCAST = false
DEFAULT_MESSAGE_ASUSER = false DEFAULT_MESSAGE_ASUSER = false
DEFAULT_MESSAGE_PARSE = "" DEFAULT_MESSAGE_PARSE = ""
DEFAULT_MESSAGE_LINK_NAMES = 0 DEFAULT_MESSAGE_LINK_NAMES = 0
@ -36,6 +37,7 @@ type PostMessageParameters struct {
AsUser bool `json:"as_user"` AsUser bool `json:"as_user"`
Parse string `json:"parse"` Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"` ThreadTimestamp string `json:"thread_ts"`
ReplyBroadcast bool `json:"reply_broadcast"`
LinkNames int `json:"link_names"` LinkNames int `json:"link_names"`
Attachments []Attachment `json:"attachments"` Attachments []Attachment `json:"attachments"`
UnfurlLinks bool `json:"unfurl_links"` UnfurlLinks bool `json:"unfurl_links"`
@ -44,12 +46,17 @@ type PostMessageParameters struct {
IconEmoji string `json:"icon_emoji"` IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"` Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool `json:"escape_text"` EscapeText bool `json:"escape_text"`
// chat.postEphemeral support
Channel string `json:"channel"`
User string `json:"user"`
} }
// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set
func NewPostMessageParameters() PostMessageParameters { func NewPostMessageParameters() PostMessageParameters {
return PostMessageParameters{ return PostMessageParameters{
Username: DEFAULT_MESSAGE_USERNAME, Username: DEFAULT_MESSAGE_USERNAME,
User: DEFAULT_MESSAGE_USERNAME,
AsUser: DEFAULT_MESSAGE_ASUSER, AsUser: DEFAULT_MESSAGE_ASUSER,
Parse: DEFAULT_MESSAGE_PARSE, Parse: DEFAULT_MESSAGE_PARSE,
LinkNames: DEFAULT_MESSAGE_LINK_NAMES, LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
@ -102,6 +109,37 @@ func (api *Client) PostMessageContext(ctx context.Context, channel, text string,
return respChannel, respTimestamp, err return respChannel, respTimestamp, err
} }
// PostEphemeral sends an ephemeral message to a user in a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) (string, error) {
options = append(options, MsgOptionPostEphemeral())
return api.PostEphemeralContext(
context.Background(),
channel,
userID,
options...,
)
}
// PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context
// For more details, see PostEphemeral documentation
func (api *Client) PostEphemeralContext(ctx context.Context, channel, userID string, options ...MsgOption) (string, error) {
path, values, err := ApplyMsgOptions(api.config.token, channel, options...)
if err != nil {
return "", err
}
values.Add("user", userID)
response, err := chatRequest(ctx, path, values, api.debug)
if err != nil {
return "", err
}
return response.Timestamp, nil
}
// UpdateMessage updates a message in a channel // UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) {
return api.UpdateMessageContext(context.Background(), channel, timestamp, text) return api.UpdateMessageContext(context.Background(), channel, timestamp, text)
@ -174,6 +212,7 @@ const (
chatUpdate sendMode = "chat.update" chatUpdate sendMode = "chat.update"
chatPostMessage sendMode = "chat.postMessage" chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete" chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral"
) )
type sendConfig struct { type sendConfig struct {
@ -193,6 +232,15 @@ func MsgOptionPost() MsgOption {
} }
} }
// MsgOptionPostEphemeral posts an ephemeral message
func MsgOptionPostEphemeral() MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostEphemeral
config.values.Del("ts")
return nil
}
}
// MsgOptionUpdate updates a message based on the timestamp. // MsgOptionUpdate updates a message based on the timestamp.
func MsgOptionUpdate(timestamp string) MsgOption { func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error { return func(config *sendConfig) error {
@ -279,6 +327,11 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
config.values.Set("username", string(params.Username)) config.values.Set("username", string(params.Username))
} }
// chat.postEphemeral support
if params.User != DEFAULT_MESSAGE_USERNAME {
config.values.Set("user", params.User)
}
// never generates an error. // never generates an error.
MsgOptionAsUser(params.AsUser)(config) MsgOptionAsUser(params.AsUser)(config)
@ -314,6 +367,9 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP {
config.values.Set("thread_ts", params.ThreadTimestamp) config.values.Set("thread_ts", params.ThreadTimestamp)
} }
if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST {
config.values.Set("reply_broadcast", "true")
}
return nil return nil
} }

View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
channels, err := api.GetChannels(false)
if err != nil {
fmt.Printf("%s\n", err)
return
}
for _, channel := range channels {
fmt.Println(channel.Name)
// channel is of type conversation & groupConversation
// see all available methods in `conversation.go`
}
}

30
vendor/github.com/nlopes/slack/examples/files/files.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
params := slack.FileUploadParameters{
Title: "Batman Example",
//Filetype: "txt",
File: "example.txt",
//Content: "Nan Nan Nan Nan Nan Nan Nan Nan Batman",
}
file, err := api.UploadFile(params)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("Name: %s, URL: %s\n", file.Name, file.URL)
err = api.DeleteFile(file.ID)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("File %s deleted successfully.\n", file.Name)
}

View File

@ -0,0 +1,22 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
// If you set debugging, it will log all requests to the console
// Useful when encountering issues
// api.SetDebug(true)
groups, err := api.GetGroups(false)
if err != nil {
fmt.Printf("%s\n", err)
return
}
for _, group := range groups {
fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name)
}
}

21
vendor/github.com/nlopes/slack/examples/ims/ims.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
userID := "USER_ID"
_, _, channelID, err := api.OpenIMChannel(userID)
if err != nil {
fmt.Printf("%s\n", err)
}
api.PostMessage(channelID, "Hello World!", slack.PostMessageParameters{})
}

View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
params := slack.PostMessageParameters{}
attachment := slack.Attachment{
Pretext: "some pretext",
Text: "some text",
// Uncomment the following part to send a field too
/*
Fields: []slack.AttachmentField{
slack.AttachmentField{
Title: "a",
Value: "no",
},
},
*/
}
params.Attachments = []slack.Attachment{attachment}
channelID, timestamp, err := api.PostMessage("CHANNEL_ID", "Some text", params)
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("Message successfully sent to channel %s at %s", channelID, timestamp)
}

123
vendor/github.com/nlopes/slack/examples/pins/pins.go generated vendored Normal file
View File

@ -0,0 +1,123 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
/*
WARNING: This example is destructive in the sense that it create a channel called testpinning
*/
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
var (
postAsUserName string
postAsUserID string
postToChannelID string
)
// Find the user to post as.
authTest, err := api.AuthTest()
if err != nil {
fmt.Printf("Error getting channels: %s\n", err)
return
}
channelName := "testpinning"
// Post as the authenticated user.
postAsUserName = authTest.User
postAsUserID = authTest.UserID
// Create a temporary channel
channel, err := api.CreateChannel(channelName)
if err != nil {
// If the channel exists, that means we just need to unarchive it
if err.Error() == "name_taken" {
err = nil
channels, err := api.GetChannels(false)
if err != nil {
fmt.Println("Could not retrieve channels")
return
}
for _, archivedChannel := range channels {
if archivedChannel.Name == channelName {
if archivedChannel.IsArchived {
err = api.UnarchiveChannel(archivedChannel.ID)
if err != nil {
fmt.Printf("Could not unarchive %s: %s\n", archivedChannel.ID, err)
return
}
}
channel = &archivedChannel
break
}
}
}
if err != nil {
fmt.Printf("Error setting test channel for pinning: %s\n", err)
return
}
}
postToChannelID = channel.ID
fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, postToChannelID)
// Post a message.
postParams := slack.PostMessageParameters{}
channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
if err != nil {
fmt.Printf("Error posting message: %s\n", err)
return
}
// Grab a reference to the message.
msgRef := slack.NewRefToMessage(channelID, timestamp)
// Add message pin to channel
if err := api.AddPin(channelID, msgRef); err != nil {
fmt.Printf("Error adding pin: %s\n", err)
return
}
// List all of the users pins.
listPins, _, err := api.ListPins(channelID)
if err != nil {
fmt.Printf("Error listing pins: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("All pins by %s...\n", authTest.User)
for _, item := range listPins {
fmt.Printf(" > Item type: %s\n", item.Type)
}
// Remove the pin.
err = api.RemovePin(channelID, msgRef)
if err != nil {
fmt.Printf("Error remove pin: %s\n", err)
return
}
if err = api.ArchiveChannel(channelID); err != nil {
fmt.Printf("Error archiving channel: %s\n", err)
return
}
}

View File

@ -0,0 +1,126 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
var (
postAsUserName string
postAsUserID string
postToUserName string
postToUserID string
postToChannelID string
)
// Find the user to post as.
authTest, err := api.AuthTest()
if err != nil {
fmt.Printf("Error getting channels: %s\n", err)
return
}
// Post as the authenticated user.
postAsUserName = authTest.User
postAsUserID = authTest.UserID
// Posting to DM with self causes a conversation with slackbot.
postToUserName = authTest.User
postToUserID = authTest.UserID
// Find the channel.
_, _, chanID, err := api.OpenIMChannel(postToUserID)
if err != nil {
fmt.Printf("Error opening IM: %s\n", err)
return
}
postToChannelID = chanID
fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID)
// Post a message.
postParams := slack.PostMessageParameters{}
channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
if err != nil {
fmt.Printf("Error posting message: %s\n", err)
return
}
// Grab a reference to the message.
msgRef := slack.NewRefToMessage(channelID, timestamp)
// React with :+1:
if err := api.AddReaction("+1", msgRef); err != nil {
fmt.Printf("Error adding reaction: %s\n", err)
return
}
// React with :-1:
if err := api.AddReaction("cry", msgRef); err != nil {
fmt.Printf("Error adding reaction: %s\n", err)
return
}
// Get all reactions on the message.
msgReactions, err := api.GetReactions(msgRef, slack.NewGetReactionsParameters())
if err != nil {
fmt.Printf("Error getting reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("%d reactions to message...\n", len(msgReactions))
for _, r := range msgReactions {
fmt.Printf(" %d users say %s\n", r.Count, r.Name)
}
// List all of the users reactions.
listReactions, _, err := api.ListReactions(slack.NewListReactionsParameters())
if err != nil {
fmt.Printf("Error listing reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("All reactions by %s...\n", authTest.User)
for _, item := range listReactions {
fmt.Printf("%d on a %s...\n", len(item.Reactions), item.Type)
for _, r := range item.Reactions {
fmt.Printf(" %s (along with %d others)\n", r.Name, r.Count-1)
}
}
// Remove the :cry: reaction.
err = api.RemoveReaction("cry", msgRef)
if err != nil {
fmt.Printf("Error remove reaction: %s\n", err)
return
}
// Get all reactions on the message.
msgReactions, err = api.GetReactions(msgRef, slack.NewGetReactionsParameters())
if err != nil {
fmt.Printf("Error getting reactions: %s\n", err)
return
}
fmt.Printf("\n")
fmt.Printf("%d reactions to message after removing cry...\n", len(msgReactions))
for _, r := range msgReactions {
fmt.Printf(" %d users say %s\n", r.Count, r.Name)
}
}

46
vendor/github.com/nlopes/slack/examples/stars/stars.go generated vendored Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"flag"
"fmt"
"github.com/nlopes/slack"
)
func main() {
var (
apiToken string
debug bool
)
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
flag.BoolVar(&debug, "debug", false, "Show JSON output")
flag.Parse()
api := slack.New(apiToken)
if debug {
api.SetDebug(true)
}
// Get all stars for the usr.
params := slack.NewStarsParameters()
starredItems, _, err := api.GetStarred(params)
if err != nil {
fmt.Printf("Error getting stars: %s\n", err)
return
}
for _, s := range starredItems {
var desc string
switch s.Type {
case slack.TYPE_MESSAGE:
desc = s.Message.Text
case slack.TYPE_FILE:
desc = s.File.Name
case slack.TYPE_FILE_COMMENT:
desc = s.File.Name + " - " + s.Comment.Comment
case slack.TYPE_CHANNEL, slack.TYPE_IM, slack.TYPE_GROUP:
desc = s.Channel
}
fmt.Printf("Starred %s: %s\n", s.Type, desc)
}
}

25
vendor/github.com/nlopes/slack/examples/team/team.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
//Example for single user
billingActive, err := api.GetBillableInfo("U023BECGF")
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("ID: U023BECGF, BillingActive: %v\n\n\n", billingActive["U023BECGF"])
//Example for team
billingActiveForTeam, _ := api.GetBillableInfoForTeam()
for id, value := range billingActiveForTeam {
fmt.Printf("ID: %v, BillingActive: %v\n", id, value)
}
}

17
vendor/github.com/nlopes/slack/examples/users/users.go generated vendored Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"fmt"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR_TOKEN_HERE")
user, err := api.GetUserInfo("U023BECGF")
if err != nil {
fmt.Printf("%s\n", err)
return
}
fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email)
}

View File

@ -0,0 +1,54 @@
package main
import (
"fmt"
"log"
"os"
"github.com/nlopes/slack"
)
func main() {
api := slack.New("YOUR TOKEN HERE")
logger := log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)
slack.SetLogger(logger)
api.SetDebug(true)
rtm := api.NewRTM()
go rtm.ManageConnection()
for msg := range rtm.IncomingEvents {
fmt.Print("Event Received: ")
switch ev := msg.Data.(type) {
case *slack.HelloEvent:
// Ignore hello
case *slack.ConnectedEvent:
fmt.Println("Infos:", ev.Info)
fmt.Println("Connection counter:", ev.ConnectionCount)
// Replace C2147483705 with your Channel ID
rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "C2147483705"))
case *slack.MessageEvent:
fmt.Printf("Message: %v\n", ev)
case *slack.PresenceChangeEvent:
fmt.Printf("Presence Change: %v\n", ev)
case *slack.LatencyReport:
fmt.Printf("Current latency: %v\n", ev.Value)
case *slack.RTMError:
fmt.Printf("Error: %s\n", ev.Error())
case *slack.InvalidAuthEvent:
fmt.Printf("Invalid credentials")
return
default:
// Ignore other events..
// fmt.Printf("Unexpected: %v\n", msg.Data)
}
}
}

View File

@ -267,10 +267,7 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) error {
"file": {fileID}, "file": {fileID},
} }
_, err := fileRequest(ctx, "files.delete", values, api.debug) _, err := fileRequest(ctx, "files.delete", values, api.debug)
if err != nil {
return err return err
}
return nil
} }

View File

@ -208,10 +208,7 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) error {
"channel": {group}, "channel": {group},
} }
_, err := groupRequest(ctx, "groups.leave", values, api.debug) _, err := groupRequest(ctx, "groups.leave", values, api.debug)
if err != nil {
return err return err
}
return nil
} }
// KickUserFromGroup kicks a user from a group // KickUserFromGroup kicks a user from a group
@ -227,10 +224,7 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str
"user": {user}, "user": {user},
} }
_, err := groupRequest(ctx, "groups.kick", values, api.debug) _, err := groupRequest(ctx, "groups.kick", values, api.debug)
if err != nil {
return err return err
}
return nil
} }
// GetGroups retrieves all groups // GetGroups retrieves all groups
@ -289,10 +283,7 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string
"ts": {ts}, "ts": {ts},
} }
_, err := groupRequest(ctx, "groups.mark", values, api.debug) _, err := groupRequest(ctx, "groups.mark", values, api.debug)
if err != nil {
return err return err
}
return nil
} }
// OpenGroup opens a private group // OpenGroup opens a private group

View File

@ -3,6 +3,7 @@ package slack
// OutgoingMessage is used for the realtime API, and seems incomplete. // OutgoingMessage is used for the realtime API, and seems incomplete.
type OutgoingMessage struct { type OutgoingMessage struct {
ID int `json:"id"` ID int `json:"id"`
// channel ID
Channel string `json:"channel,omitempty"` Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
@ -121,12 +122,12 @@ type Pong struct {
// NewOutgoingMessage prepares an OutgoingMessage that the user can // NewOutgoingMessage prepares an OutgoingMessage that the user can
// use to send a message. Use this function to properly set the // use to send a message. Use this function to properly set the
// messageID. // messageID.
func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage { func (rtm *RTM) NewOutgoingMessage(text string, channelID string) *OutgoingMessage {
id := rtm.idGen.Next() id := rtm.idGen.Next()
return &OutgoingMessage{ return &OutgoingMessage{
ID: id, ID: id,
Type: "message", Type: "message",
Channel: channel, Channel: channelID,
Text: text, Text: text,
} }
} }
@ -134,11 +135,11 @@ func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage
// NewTypingMessage prepares an OutgoingMessage that the user can // NewTypingMessage prepares an OutgoingMessage that the user can
// use to send as a typing indicator. Use this function to properly set the // use to send as a typing indicator. Use this function to properly set the
// messageID. // messageID.
func (rtm *RTM) NewTypingMessage(channel string) *OutgoingMessage { func (rtm *RTM) NewTypingMessage(channelID string) *OutgoingMessage {
id := rtm.idGen.Next() id := rtm.idGen.Next()
return &OutgoingMessage{ return &OutgoingMessage{
ID: id, ID: id,
Type: "typing", Type: "typing",
Channel: channel, Channel: channelID,
} }
} }

View File

@ -13,6 +13,7 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
) )
@ -42,6 +43,14 @@ func (s WebError) Error() string {
return string(s) return string(s)
} }
type RateLimitedError struct {
RetryAfter time.Duration
}
func (e *RateLimitedError) Error() string {
return fmt.Sprintf("Slack rate limit exceeded, retry after %s", e.RetryAfter)
}
func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) { func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) {
body := &bytes.Buffer{} body := &bytes.Buffer{}
wr := multipart.NewWriter(body) wr := multipart.NewWriter(body)
@ -79,12 +88,7 @@ func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error
logger.Printf("parseResponseBody: %s\n", string(response)) logger.Printf("parseResponseBody: %s\n", string(response))
} }
err = json.Unmarshal(response, &intf) return json.Unmarshal(response, &intf)
if err != nil {
return err
}
return nil
} }
func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
@ -112,8 +116,16 @@ func postWithMultipartResponse(ctx context.Context, path, name, fieldname string
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it. // Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != 200 { if resp.StatusCode != http.StatusOK {
logResponse(resp, debug) logResponse(resp, debug)
return fmt.Errorf("Slack server error: %s.", resp.Status) return fmt.Errorf("Slack server error: %s.", resp.Status)
} }
@ -136,8 +148,16 @@ func postForm(ctx context.Context, endpoint string, values url.Values, intf inte
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it. // Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != 200 { if resp.StatusCode != http.StatusOK {
logResponse(resp, debug) logResponse(resp, debug)
return fmt.Errorf("Slack server error: %s.", resp.Status) return fmt.Errorf("Slack server error: %s.", resp.Status)
} }

View File

@ -27,17 +27,8 @@ func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketUR
if !response.Ok { if !response.Ok {
return nil, "", response.Error return nil, "", response.Error
} }
// websocket.Dial does not accept url without the port (yet)
// Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
// but slack returns the address with no port, so we have to fix it
api.Debugln("Using URL:", response.Info.URL) api.Debugln("Using URL:", response.Info.URL)
websocketURL, err = websocketizeURLPort(response.Info.URL) return &response.Info, response.Info.URL, nil
if err != nil {
return nil, "", fmt.Errorf("parsing response URL: %s", err)
}
return &response.Info, websocketURL, nil
} }
// ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block. // ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block.
@ -59,17 +50,8 @@ func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocket
if !response.Ok { if !response.Ok {
return nil, "", response.Error return nil, "", response.Error
} }
// websocket.Dial does not accept url without the port (yet)
// Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
// but slack returns the address with no port, so we have to fix it
api.Debugln("Using URL:", response.Info.URL) api.Debugln("Using URL:", response.Info.URL)
websocketURL, err = websocketizeURLPort(response.Info.URL) return &response.Info, response.Info.URL, nil
if err != nil {
return nil, "", fmt.Errorf("parsing response URL: %s", err)
}
return &response.Info, websocketURL, nil
} }
// NewRTM returns a RTM, which provides a fully managed connection to // NewRTM returns a RTM, which provides a fully managed connection to
@ -90,6 +72,7 @@ func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
isConnected: false, isConnected: false,
wasIntentional: true, wasIntentional: true,
killChannel: make(chan bool), killChannel: make(chan bool),
disconnected: make(chan struct{}),
forcePing: make(chan bool), forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage), rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1), idGen: NewSafeID(1),

View File

@ -3,12 +3,13 @@ package slack
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log" "log"
"net/url" "net/url"
"os" "os"
) )
var logger *log.Logger // A logger that can be set by consumers var logger stdLogger // A logger that can be set by consumers
/* /*
Added as a var so that we can change this for testing purposes Added as a var so that we can change this for testing purposes
*/ */
@ -41,12 +42,31 @@ type Client struct {
debug bool debug bool
} }
// stdLogger is a logger interface compatible with both stdlib and some
// 3rd party loggers such as logrus.
type stdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
Output(int, string) error
}
// SetLogger let's library users supply a logger, so that api debugging // SetLogger let's library users supply a logger, so that api debugging
// can be logged along with the application's debugging info. // can be logged along with the application's debugging info.
func SetLogger(l *log.Logger) { func SetLogger(l stdLogger) {
logger = l logger = l
} }
// New creates new Client.
func New(token string) *Client { func New(token string) *Client {
s := &Client{} s := &Client{}
s.config.token = token s.config.token = token
@ -83,12 +103,12 @@ func (api *Client) SetDebug(debug bool) {
func (api *Client) Debugf(format string, v ...interface{}) { func (api *Client) Debugf(format string, v ...interface{}) {
if api.debug { if api.debug {
logger.Printf(format, v...) logger.Output(2, fmt.Sprintf(format, v...))
} }
} }
func (api *Client) Debugln(v ...interface{}) { func (api *Client) Debugln(v ...interface{}) {
if api.debug { if api.debug {
logger.Println(v...) logger.Output(2, fmt.Sprintln(v...))
} }
} }

View File

@ -200,10 +200,7 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) error {
"token": {api.config.token}, "token": {api.config.token},
} }
_, err := userRequest(ctx, "users.setActive", values, api.debug) _, err := userRequest(ctx, "users.setActive", values, api.debug)
if err != nil {
return err return err
}
return nil
} }
// SetUserPresence changes the currently authenticated user presence // SetUserPresence changes the currently authenticated user presence
@ -247,8 +244,8 @@ func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityRes
} }
// SetUserPhoto changes the currently authenticated user's profile image // SetUserPhoto changes the currently authenticated user's profile image
func (api *Client) SetUserPhoto(ctx context.Context, image string, params UserSetPhotoParams) error { func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
return api.SetUserPhoto(context.Background(), image, params) return api.SetUserPhotoContext(context.Background(), image, params)
} }
// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context // SetUserPhotoContext changes the currently authenticated user's profile image using a custom context

View File

@ -27,6 +27,7 @@ type RTM struct {
IncomingEvents chan RTMEvent IncomingEvents chan RTMEvent
outgoingMessages chan OutgoingMessage outgoingMessages chan OutgoingMessage
killChannel chan bool killChannel chan bool
disconnected chan struct{} // disconnected is closed when Disconnect is invoked, regardless of connection state. Allows for ManagedConnection to not leak.
forcePing chan bool forcePing chan bool
rawEvents chan json.RawMessage rawEvents chan json.RawMessage
wasIntentional bool wasIntentional bool
@ -59,9 +60,14 @@ type RTMOptions struct {
// Disconnect and wait, blocking until a successful disconnection. // Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error { func (rtm *RTM) Disconnect() error {
// this channel is always closed on disconnect. lets the ManagedConnection() function
// properly clean up.
close(rtm.disconnected)
if !rtm.isConnected { if !rtm.isConnected {
return errors.New("Invalid call to Disconnect - Slack API is already disconnected") return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
} }
rtm.killChannel <- true rtm.killChannel <- true
return nil return nil
} }

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