4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-27 07:39:25 +00:00

Compare commits

..

61 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
Wim
962062fe44 Release v1.4.1 2017-11-13 20:10:04 +01:00
Wim
0578b21270 Fix message sending (slack) 2017-11-13 19:50:18 +01:00
Wim
36a800c3f5 Add support for comments from slack file uploads (slack) 2017-11-13 00:20:31 +01:00
Wim
6d21f84187 Add extension to sticker/video/photo (telegram) 2017-11-12 22:04:35 +01:00
Wim
f1e9833310 Do not ignore empty messages with files for bridges that support it 2017-11-12 18:34:16 +01:00
Wim
46f5acc4f9 Add the download actually to the message (telegram) 2017-11-12 18:09:38 +01:00
Wim
95d4dcaeb3 Add more debug info (telegram) 2017-11-12 17:49:10 +01:00
Wim
64c542e614 Add more debug info (telegram) 2017-11-12 17:46:44 +01:00
Wim
13d081ea80 Fix document bug (telegram) 2017-11-12 17:15:53 +01:00
Wim
c0f9d86287 Fix telegram photo/document input handling (telegram) 2017-11-12 11:46:32 +01:00
Wim
bcdecdaa73 Fix strict user handling of girc (irc). Closes #298 2017-11-11 23:16:58 +01:00
399 changed files with 76000 additions and 26432 deletions

View File

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

View File

@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/labstack/echo"
@ -8,14 +9,13 @@ import (
"github.com/zfjagann/golang-ring"
"net/http"
"sync"
"time"
)
type Api struct {
Config *config.Protocol
Remote chan config.Message
Account string
Messages ring.Ring
sync.RWMutex
*config.BridgeConfig
}
type ApiMessage struct {
@ -33,23 +33,21 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Api {
b := &Api{}
func New(cfg *config.BridgeConfig) *Api {
b := &Api{BridgeConfig: cfg}
e := echo.New()
b.Messages = ring.Ring{}
b.Messages.SetCapacity(cfg.Buffer)
b.Config = &cfg
b.Account = account
b.Remote = c
b.Messages.SetCapacity(b.Config.Buffer)
if b.Config.Token != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return key == b.Config.Token, nil
}))
}
e.GET("/api/messages", b.handleMessages)
e.GET("/api/stream", b.handleStream)
e.POST("/api/message", b.handlePostMessage)
go func() {
flog.Fatal(e.Start(cfg.BindAddress))
flog.Fatal(e.Start(b.Config.BindAddress))
}()
return b
}
@ -103,3 +101,24 @@ func (b *Api) handleMessages(c echo.Context) error {
b.Messages = ring.Ring{}
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/rocketchat"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/sshchat"
"github.com/42wim/matterbridge/bridge/steam"
"github.com/42wim/matterbridge/bridge/telegram"
"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.Account = bridge.Account
b.Joined = make(map[string]bool)
bridgeConfig := &config.BridgeConfig{General: &cfg.General, Account: bridge.Account, Remote: c}
// override config from environment
config.OverrideCfgFromEnv(cfg, protocol, name)
switch protocol {
case "mattermost":
b.Config = cfg.Mattermost[name]
b.Bridger = bmattermost.New(cfg.Mattermost[name], bridge.Account, c)
bridgeConfig.Config = cfg.Mattermost[name]
b.Bridger = bmattermost.New(bridgeConfig)
case "irc":
b.Config = cfg.IRC[name]
b.Bridger = birc.New(cfg.IRC[name], bridge.Account, c)
bridgeConfig.Config = cfg.IRC[name]
b.Bridger = birc.New(bridgeConfig)
case "gitter":
b.Config = cfg.Gitter[name]
b.Bridger = bgitter.New(cfg.Gitter[name], bridge.Account, c)
bridgeConfig.Config = cfg.Gitter[name]
b.Bridger = bgitter.New(bridgeConfig)
case "slack":
b.Config = cfg.Slack[name]
b.Bridger = bslack.New(cfg.Slack[name], bridge.Account, c)
bridgeConfig.Config = cfg.Slack[name]
b.Bridger = bslack.New(bridgeConfig)
case "xmpp":
b.Config = cfg.Xmpp[name]
b.Bridger = bxmpp.New(cfg.Xmpp[name], bridge.Account, c)
bridgeConfig.Config = cfg.Xmpp[name]
b.Bridger = bxmpp.New(bridgeConfig)
case "discord":
b.Config = cfg.Discord[name]
b.Bridger = bdiscord.New(cfg.Discord[name], bridge.Account, c)
bridgeConfig.Config = cfg.Discord[name]
b.Bridger = bdiscord.New(bridgeConfig)
case "telegram":
b.Config = cfg.Telegram[name]
b.Bridger = btelegram.New(cfg.Telegram[name], bridge.Account, c)
bridgeConfig.Config = cfg.Telegram[name]
b.Bridger = btelegram.New(bridgeConfig)
case "rocketchat":
b.Config = cfg.Rocketchat[name]
b.Bridger = brocketchat.New(cfg.Rocketchat[name], bridge.Account, c)
bridgeConfig.Config = cfg.Rocketchat[name]
b.Bridger = brocketchat.New(bridgeConfig)
case "matrix":
b.Config = cfg.Matrix[name]
b.Bridger = bmatrix.New(cfg.Matrix[name], bridge.Account, c)
bridgeConfig.Config = cfg.Matrix[name]
b.Bridger = bmatrix.New(bridgeConfig)
case "steam":
b.Config = cfg.Steam[name]
b.Bridger = bsteam.New(cfg.Steam[name], bridge.Account, c)
bridgeConfig.Config = cfg.Steam[name]
b.Bridger = bsteam.New(bridgeConfig)
case "sshchat":
bridgeConfig.Config = cfg.Sshchat[name]
b.Bridger = bsshchat.New(bridgeConfig)
case "api":
b.Config = cfg.Api[name]
b.Bridger = api.New(cfg.Api[name], bridge.Account, c)
bridgeConfig.Config = cfg.Api[name]
b.Bridger = api.New(bridgeConfig)
}
b.Config = bridgeConfig.Config
return b
}

View File

@ -33,8 +33,10 @@ type Message struct {
}
type FileInfo struct {
Name string
Data *[]byte
Name string
Data *[]byte
Comment string
URL string
}
type ChannelInfo struct {
@ -58,41 +60,48 @@ type Protocol struct {
IgnoreMessages string // all protocols
Jid string // xmpp
Login string // mattermost, matrix
Muc string // xmpp
Name string // all protocols
Nick string // all protocols
NickFormatter string // mattermost, slack
NickServNick string // IRC
NickServUsername string // IRC
NickServPassword string // IRC
NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix
NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack
Protocol string //all protocols
MessageQueue int // IRC, size of message queue for flood control
MessageDelay int // IRC, time in millisecond to wait between messages
MessageLength int // IRC, max length of a message allowed
MessageFormat string // telegram
RemoteNickFormat string // all protocols
Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols
ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost
StripNick bool // all protocols
Team string // mattermost
Token string // gitter, slack, discord, api
URL string // mattermost, slack // DEPRECATED
UseAPI bool // mattermost, slack
UseSASL bool // IRC
UseTLS bool // IRC
UseFirstName bool // telegram
UseUserName bool // discord
UseInsecureURL bool // telegram
WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack
WebhookUse string // mattermost, slack, discord
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
Name string // all protocols
Nick string // all protocols
NickFormatter string // mattermost, slack
NickServNick string // IRC
NickServUsername string // IRC
NickServPassword string // IRC
NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix
NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack
Protocol string // all protocols
RejoinDelay int // IRC
ReplaceMessages [][]string // all protocols
ReplaceNicks [][]string // all protocols
RemoteNickFormat string // all protocols
Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols
ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost
StripNick bool // all protocols
Team string // mattermost
Token string // gitter, slack, discord, api
URL string // mattermost, slack // DEPRECATED
UseAPI bool // mattermost, slack
UseSASL bool // IRC
UseTLS bool // IRC
UseFirstName bool // telegram
UseUserName bool // discord
UseInsecureURL bool // telegram
WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack
WebhookUse string // mattermost, slack, discord
}
type ChannelOptions struct {
@ -134,11 +143,19 @@ type Config struct {
Discord map[string]Protocol
Telegram map[string]Protocol
Rocketchat map[string]Protocol
Sshchat map[string]Protocol
General Protocol
Gateway []Gateway
SameChannelGateway []SameChannelGateway
}
type BridgeConfig struct {
Config Protocol
General *Protocol
Account string
Remote chan Message
}
func NewConfig(cfgfile string) *Config {
var cfg Config
if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil {
@ -166,6 +183,9 @@ func NewConfig(cfgfile string) *Config {
if fail {
log.Fatalf("Fix your config. Please see changelog for more information")
}
if cfg.General.MediaDownloadSize == 0 {
cfg.General.MediaDownloadSize = 1000000
}
return &cfg
}

View File

@ -12,9 +12,6 @@ import (
type bdiscord struct {
c *discordgo.Session
Config *config.Protocol
Remote chan config.Message
Account string
Channels []*discordgo.Channel
Nick string
UseChannelID bool
@ -24,6 +21,7 @@ type bdiscord struct {
webhookToken string
channelInfoMap map[string]*config.ChannelInfo
sync.RWMutex
*config.BridgeConfig
}
var flog *log.Entry
@ -33,11 +31,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *bdiscord {
b := &bdiscord{}
b.Config = &cfg
b.Remote = c
b.Account = account
func New(cfg *config.BridgeConfig) *bdiscord {
b := &bdiscord{BridgeConfig: cfg}
b.userMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo)
if b.Config.WebhookURL != "" {
@ -151,11 +146,12 @@ func (b *bdiscord) Send(msg config.Message) (string, error) {
fi := f.(config.FileInfo)
files := []*discordgo.File{}
files = append(files, &discordgo.File{fi.Name, "", bytes.NewReader(*fi.Data)})
_, err = b.c.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: msg.Text, Files: files})
_, err = b.c.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: msg.Username + fi.Comment, Files: files})
if err != nil {
flog.Errorf("file upload failed: %#v", err)
}
}
return "", nil
}
}

View File

@ -9,13 +9,11 @@ import (
)
type Bgitter struct {
c *gitter.Gitter
Config *config.Protocol
Remote chan config.Message
Account string
User *gitter.User
Users []gitter.User
Rooms []gitter.Room
c *gitter.Gitter
User *gitter.User
Users []gitter.User
Rooms []gitter.Room
*config.BridgeConfig
}
var flog *log.Entry
@ -25,12 +23,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Bgitter {
b := &Bgitter{}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
func New(cfg *config.BridgeConfig) *Bgitter {
return &Bgitter{BridgeConfig: cfg}
}
func (b *Bgitter) Connect() error {
@ -125,6 +119,23 @@ func (b *Bgitter) Send(msg config.Message) (string, error) {
}
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)
if err != nil {
return "", err

View File

@ -26,3 +26,15 @@ func DownloadFile(url string) (*[]byte, error) {
resp.Body.Close()
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"
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
"github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset"
@ -24,12 +25,11 @@ type Birc struct {
i *girc.Client
Nick string
names map[string][]string
Config *config.Protocol
Remote chan config.Message
connected chan struct{}
Local chan config.Message // local queue for flood control
Account string
FirstConnection bool
*config.BridgeConfig
}
var flog *log.Entry
@ -39,13 +39,11 @@ func init() {
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.Config = &cfg
b.BridgeConfig = cfg
b.Nick = b.Config.Nick
b.Remote = c
b.names = make(map[string][]string)
b.Account = account
b.connected = make(chan struct{})
if b.Config.MessageDelay == 0 {
b.Config.MessageDelay = 1300
@ -81,12 +79,22 @@ func (b *Birc) Connect() error {
if err != nil {
return err
}
// fix strict user handling of girc
user := b.Config.Nick
for !girc.IsValidUser(user) {
if len(user) == 1 {
user = "matterbridge"
break
}
user = user[1:]
}
i := girc.New(girc.Config{
Server: server,
ServerPass: b.Config.Password,
Port: port,
Nick: b.Config.Nick,
User: b.Config.Nick,
User: user,
Name: b.Config.Nick,
SSL: b.Config.UseTLS,
TLSConfig: &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify, ServerName: server},
@ -168,9 +176,27 @@ func (b *Birc) Send(msg config.Message) (string, error) {
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") {
input := []rune(text)
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-1 {
@ -241,6 +267,7 @@ func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
channel := event.Params[0]
if event.Command == "KICK" {
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}
return
}
@ -296,14 +323,13 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if event.Source.Name == b.Nick {
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)
msg := ""
if event.Command == "CTCP_ACTION" {
// msg = event.Source.Name + " "
if event.IsAction() {
rmsg.Event = config.EVENT_USER_ACTION
}
msg += event.Trailing
msg += event.StripAction()
// strip IRC colors
re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`)
msg = re.ReplaceAllString(msg, "")

View File

@ -1,22 +1,24 @@
package bmatrix
import (
"bytes"
"mime"
"regexp"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
matrix "github.com/matrix-org/gomatrix"
)
type Bmatrix struct {
mc *matrix.Client
Config *config.Protocol
Remote chan config.Message
Account string
UserID string
RoomMap map[string]string
sync.RWMutex
*config.BridgeConfig
}
var flog *log.Entry
@ -26,12 +28,9 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Bmatrix {
b := &Bmatrix{}
func New(cfg *config.BridgeConfig) *Bmatrix {
b := &Bmatrix{BridgeConfig: cfg}
b.RoomMap = make(map[string]string)
b.Config = &cfg
b.Account = account
b.Remote = c
return b
}
@ -87,6 +86,44 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
matrix.TextMessage{"m.emote", msg.Username + msg.Text})
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)
return "", nil
}
@ -104,7 +141,13 @@ func (b *Bmatrix) getRoomID(channel string) string {
func (b *Bmatrix) handlematrix() error {
syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
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()
channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock()
@ -121,10 +164,31 @@ func (b *Bmatrix) handlematrix() error {
if ev.Content["msgtype"].(string) == "m.emote" {
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)
b.Remote <- rmsg
}
flog.Debugf("Received: %#v", ev)
})
go func() {
for {

View File

@ -32,10 +32,8 @@ type MMMessage struct {
type Bmattermost struct {
MMhook
MMapi
Config *config.Protocol
Remote chan config.Message
TeamId string
Account string
TeamId string
*config.BridgeConfig
}
var flog *log.Entry
@ -45,11 +43,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Bmattermost {
b := &Bmattermost{}
b.Config = &cfg
b.Remote = c
b.Account = account
func New(cfg *config.BridgeConfig) *Bmattermost {
b := &Bmattermost{BridgeConfig: cfg}
b.mmMap = make(map[string]string)
return b
}
@ -190,9 +185,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
flog.Debugf("ERROR %#v", err)
return "", err
}
message = "uploaded a file: " + fi.Name
message = fi.Comment
if b.Config.PrefixMessagesWithNick {
message = nick + "uploaded a file: " + fi.Name
message = nick + fi.Comment
}
res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id})
}

View File

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

View File

@ -7,7 +7,7 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/matterhook"
log "github.com/Sirupsen/logrus"
"github.com/matterbridge/slack"
"github.com/nlopes/slack"
"html"
"io"
"net/http"
@ -27,14 +27,12 @@ type MMMessage struct {
type Bslack struct {
mh *matterhook.Client
sc *slack.Client
Config *config.Protocol
rtm *slack.RTM
Plus bool
Remote chan config.Message
Users []slack.User
Account string
si *slack.Info
channels []slack.Channel
*config.BridgeConfig
}
var flog *log.Entry
@ -44,12 +42,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Bslack {
b := &Bslack{}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
func New(cfg *config.BridgeConfig) *Bslack {
return &Bslack{BridgeConfig: cfg}
}
func (b *Bslack) Command(cmd string) string {
@ -113,7 +107,7 @@ func (b *Bslack) Disconnect() error {
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// 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") {
// TODO check if bot has already joined channel
return nil
@ -161,7 +155,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
np.AsUser = true
}
np.Username = nick
np.IconURL = config.GetIconURL(&msg, b.Config)
np.IconURL = config.GetIconURL(&msg, &b.Config)
if msg.Avatar != "" {
np.IconURL = msg.Avatar
}
@ -195,9 +189,10 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
_, err = b.sc.UploadFile(slack.FileUploadParameters{
Reader: bytes.NewReader(*fi.Data),
Filename: fi.Name,
Channels: []string{schannel.ID},
Reader: bytes.NewReader(*fi.Data),
Filename: fi.Name,
Channels: []string{schannel.ID},
InitialComment: fi.Comment,
})
if err != nil {
flog.Errorf("uploadfile %#v", err)
@ -293,12 +288,17 @@ func (b *Bslack) handleSlack() {
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
if message.Raw.File != nil {
// limit to 1MB for now
if message.Raw.File.Size <= 1000000 {
if message.Raw.File.Size <= b.General.MediaDownloadSize {
comment := ""
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
if err != nil {
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
} else {
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data})
results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(msg.Text, -1)
if len(results) > 0 {
comment = results[0][1]
}
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data, Comment: comment})
}
}
}
@ -309,9 +309,11 @@ func (b *Bslack) handleSlack() {
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
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) {
case *slack.MessageEvent:
flog.Debugf("Receiving from slackclient %#v", ev)
if len(ev.Attachments) > 0 {
// skip messages we made ourselves
if ev.Attachments[0].CallbackID == "matterbridge" {
@ -394,6 +396,8 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
}
case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
flog.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
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 {
c *steam.Client
connected chan struct{}
Config *config.Protocol
Remote chan config.Message
Account string
userMap map[steamid.SteamId]string
sync.RWMutex
*config.BridgeConfig
}
var flog *log.Entry
@ -30,11 +28,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Bsteam {
b := &Bsteam{}
b.Config = &cfg
b.Remote = c
b.Account = account
func New(cfg *config.BridgeConfig) *Bsteam {
b := &Bsteam{BridgeConfig: cfg}
b.userMap = make(map[steamid.SteamId]string)
b.connected = make(chan struct{})
return b

View File

@ -3,6 +3,7 @@ package btelegram
import (
"regexp"
"strconv"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
@ -11,10 +12,8 @@ import (
)
type Btelegram struct {
c *tgbotapi.BotAPI
Config *config.Protocol
Remote chan config.Message
Account string
c *tgbotapi.BotAPI
*config.BridgeConfig
}
var flog *log.Entry
@ -24,12 +23,8 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Btelegram {
b := &Btelegram{}
b.Config = &cfg
b.Remote = c
b.Account = account
return b
func New(cfg *config.BridgeConfig) *Btelegram {
return &Btelegram{BridgeConfig: cfg}
}
func (b *Btelegram) Connect() error {
@ -89,6 +84,9 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
return "", err
}
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML
}
_, err = b.c.Send(m)
if err != nil {
return "", err
@ -113,20 +111,14 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
if err != nil {
log.Errorf("file upload failed: %#v", err)
}
if fi.Comment != "" {
b.sendMessage(chatid, msg.Username+fi.Comment)
}
}
return "", nil
}
}
m := tgbotapi.NewMessage(chatid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML
}
res, err := b.c.Send(m)
if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
return b.sendMessage(chatid, msg.Username+msg.Text)
}
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
@ -178,12 +170,34 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
if message.Video != nil {
b.handleDownload(message.Video, &fmsg)
}
if message.Photo != nil && b.Config.UseInsecureURL {
if message.Photo != nil {
b.handleDownload(message.Photo, &fmsg)
}
if message.Document != nil && b.Config.UseInsecureURL {
b.handleDownload(message.Sticker, &fmsg)
text = text + " " + message.Document.FileName + " : " + b.getFileDirectURL(message.Document.FileID)
if message.Document != nil {
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
@ -208,7 +222,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
if text != "" || len(fmsg.Extra) > 0 {
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID)}
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID), Extra: fmsg.Extra}
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
}
@ -228,28 +242,55 @@ func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) {
url := ""
name := ""
text := ""
fileid := ""
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:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
name = "sticker"
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
if !strings.HasSuffix(name, ".webp") {
name = name + ".webp"
}
text = " " + url
fileid = v.FileID
case *tgbotapi.Video:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
name = "video"
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
fileid = v.FileID
case *[]tgbotapi.PhotoSize:
photos := *v
size = photos[len(photos)-1].FileSize
url = b.getFileDirectURL(photos[len(photos)-1].FileID)
name = "photo"
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
case *tgbotapi.Document:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
name = v.FileName
text = " " + v.FileName + " : " + url
fileid = v.FileID
}
if b.Config.UseInsecureURL {
msg.Text = text
@ -257,12 +298,26 @@ 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
// limit to 1MB for now
if size <= 1000000 {
flog.Debugf("trying to download %#v fileid %#v with size %#v", name, fileid, size)
if size <= b.General.MediaDownloadSize {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data})
}
}
}
func (b *Btelegram) sendMessage(chatid int64, text string) (string, error) {
m := tgbotapi.NewMessage(chatid, text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML
}
res, err := b.c.Send(m)
if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
}

View File

@ -14,9 +14,7 @@ import (
type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
Config *config.Protocol
Remote chan config.Message
Account string
*config.BridgeConfig
}
var flog *log.Entry
@ -26,12 +24,9 @@ func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg config.Protocol, account string, c chan config.Message) *Bxmpp {
b := &Bxmpp{}
func New(cfg *config.BridgeConfig) *Bxmpp {
b := &Bxmpp{BridgeConfig: cfg}
b.xmppMap = make(map[string]string)
b.Config = &cfg
b.Account = account
b.Remote = c
return b
}
@ -85,6 +80,19 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
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.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})
return "", nil
}

View File

@ -1,3 +1,56 @@
# 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
## Bugfix
* telegram: fix issue with uploading for images/documents/stickers
* slack: remove double messages sent to other bridges when uploading files
* irc: Fix strict user handling of girc (irc). Closes #298
# v1.4.0
## Breaking changes
* general: `[general]` settings don't override the specific bridge settings

View File

@ -1,13 +1,16 @@
package gateway
import (
"bytes"
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
// "github.com/davecgh/go-spew/spew"
"crypto/sha1"
"github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib"
"net/http"
"regexp"
"strings"
"time"
@ -152,7 +155,11 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
// only slack now, check will have to be done in the different bridges.
// we need to check if we can't use fallback or text in other bridges
if msg.Extra != nil {
if dest.Protocol != "slack" {
if dest.Protocol != "discord" &&
dest.Protocol != "slack" &&
dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" &&
dest.Protocol != "matrix" {
if msg.Text == "" {
return brMsgIDs
}
@ -210,8 +217,8 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
return true
}
if msg.Text == "" {
// we have an attachment
if msg.Extra != nil && msg.Extra["attachments"] != nil {
// we have an attachment or actual bytes
if msg.Extra != nil && (msg.Extra["attachments"] != nil || len(msg.Extra["file"]) > 0) {
return false
}
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
@ -251,6 +258,20 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
if nick == "" {
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 {
// fix utf-8 issue #193
i := 0
@ -284,9 +305,50 @@ func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string
func (gw *Gateway) modifyMessage(msg *config.Message) {
// replace :emoji: to unicode
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
}
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 {
return msg.Channel + msg.Account
}

View File

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

View File

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

View File

@ -80,6 +80,15 @@ MessageQueue=30
#OPTIONAL (default 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.
#Messages from those users will not be sent to other bridges.
#OPTIONAL
@ -91,6 +100,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -154,6 +180,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#Messages you want to replace.
#It replaces outgoing messages from the bridge.
#So you need to place it by the sending bridge definition.
#Regular expressions supported
#Some examples:
#This replaces cat => dog and sleep => awake
#ReplaceMessages=[ ["cat","dog"], ["sleep","awake"] ]
#This Replaces every number with number. 123 => numbernumbernumber
#ReplaceMessages=[ ["[0-9]","number"] ]
#OPTIONAL (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#Nicks you want to replace.
#See ReplaceMessages for syntaxA
#OPTIONAL (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -208,6 +251,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -322,6 +382,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -366,6 +443,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -457,6 +551,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -525,6 +636,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -592,6 +720,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -660,6 +805,23 @@ IgnoreNicks="ircspammer1 ircspammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -720,6 +882,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -774,6 +953,23 @@ IgnoreNicks="spammer1 spammer2"
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -838,6 +1034,30 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#OPTIONAL (default 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
###################################################################

View File

@ -817,9 +817,14 @@ func (m *MMClient) StatusLoop() {
backoff = time.Second * 60
case <-time.After(time.Second * 5):
if retries > 3 {
m.log.Debug("StatusLoop() timeout")
m.Logout()
m.WsQuit = false
m.Login()
err := m.Login()
if err != nil {
log.Errorf("Login failed: %#v", err)
break
}
if m.OnWsConnect != nil {
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) {
file, err = url.QueryUnescape(file) // Issue #839
if err != nil {
return
}
f, err := os.Open(file)
if err != nil {
return ErrNotFound
return NotFoundHandler(c)
}
defer f.Close()
@ -510,7 +505,7 @@ func (c *context) File(file string) (err error) {
file = filepath.Join(file, indexPage)
f, err = os.Open(file)
if err != nil {
return ErrNotFound
return NotFoundHandler(c)
}
defer f.Close()
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) {
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)
return
}

View File

@ -72,6 +72,7 @@ type (
TLSServer *http.Server
Listener net.Listener
TLSListener net.Listener
AutoTLSManager autocert.Manager
DisableHTTP2 bool
Debug bool
HideBanner bool
@ -79,22 +80,22 @@ type (
Binder Binder
Validator Validator
Renderer Renderer
AutoTLSManager autocert.Manager
// Mutex sync.RWMutex
Logger Logger
}
// Route contains a handler and information for matching against requests.
Route struct {
Method string `json:"method"`
Path string `json:"path"`
Handler string `json:"handler"`
Method string `json:"method"`
Path string `json:"path"`
Name string `json:"name"`
}
// HTTPError represents an error that occurred while handling a request.
HTTPError struct {
Code int
Message interface{}
Inner error // Stores the error returned by an external dependency
}
// MiddlewareFunc defines a function to process middleware.
@ -121,7 +122,7 @@ type (
// i is the interface for Echo and Group.
i interface {
GET(string, HandlerFunc, ...MiddlewareFunc)
GET(string, HandlerFunc, ...MiddlewareFunc) *Route
}
)
@ -212,7 +213,7 @@ const (
)
const (
version = "3.1.0"
version = "3.2.5"
website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = `
@ -282,7 +283,7 @@ func New() (e *Echo) {
e.TLSServer.Handler = e
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
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.pool.New = func() interface{} {
return e.NewContext(nil, nil)
@ -319,6 +320,9 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
if he, ok := err.(*HTTPError); ok {
code = he.Code
msg = he.Message
if he.Inner != nil {
msg = fmt.Sprintf("%v, %v", err, he.Inner)
}
} else if e.Debug {
msg = err.Error()
} else {
@ -328,19 +332,19 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
msg = Map{"message": msg}
}
e.Logger.Error(err)
// Send response
if !c.Response().Committed {
if c.Request().Method == HEAD { // Issue #608
if err := c.NoContent(code); err != nil {
goto ERROR
}
err = c.NoContent(code)
} else {
if err := c.JSON(code, msg); err != nil {
goto ERROR
}
err = c.JSON(code, msg)
}
if err != nil {
e.Logger.Error(err)
}
}
ERROR:
e.Logger.Error(err)
}
// 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
// router with optional route-level middleware.
func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(CONNECT, path, h, m...)
func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(CONNECT, path, h, m...)
}
// DELETE registers a new DELETE route for a path with matching handler in the router
// with optional route-level middleware.
func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(DELETE, path, h, m...)
func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(DELETE, path, h, m...)
}
// GET registers a new GET route for a path with matching handler in the router
// with optional route-level middleware.
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(GET, path, h, m...)
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(GET, path, h, m...)
}
// HEAD registers a new HEAD route for a path with matching handler in the
// router with optional route-level middleware.
func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(HEAD, path, h, m...)
func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(HEAD, path, h, m...)
}
// OPTIONS registers a new OPTIONS route for a path with matching handler in the
// router with optional route-level middleware.
func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(OPTIONS, path, h, m...)
func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(OPTIONS, path, h, m...)
}
// PATCH registers a new PATCH route for a path with matching handler in the
// router with optional route-level middleware.
func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(PATCH, path, h, m...)
func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(PATCH, path, h, m...)
}
// POST registers a new POST route for a path with matching handler in the
// router with optional route-level middleware.
func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(POST, path, h, m...)
func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(POST, path, h, m...)
}
// PUT registers a new PUT route for a path with matching handler in the
// router with optional route-level middleware.
func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(PUT, path, h, m...)
func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(PUT, path, h, m...)
}
// TRACE registers a new TRACE route for a path with matching handler in the
// router with optional route-level middleware.
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) {
e.add(TRACE, path, h, m...)
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return e.Add(TRACE, path, h, m...)
}
// Any registers a new route for all HTTP methods and path with matching handler
// 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 {
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
// 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 {
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
// provided root directory.
func (e *Echo) Static(prefix, root string) {
func (e *Echo) Static(prefix, root string) *Route {
if root == "" {
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 {
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)
}
i.GET(prefix, h)
if prefix == "/" {
i.GET(prefix+"*", h)
} else {
i.GET(prefix+"/*", h)
return i.GET(prefix+"*", h)
}
return i.GET(prefix+"/*", h)
}
// File registers a new route with path to serve a static file.
func (e *Echo) File(path, file string) {
e.GET(path, func(c Context) error {
func (e *Echo) File(path, file string) *Route {
return e.GET(path, func(c Context) error {
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)
e.router.Add(method, path, func(c Context) error {
h := handler
@ -463,11 +477,12 @@ func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...Middl
return h(c)
})
r := &Route{
Method: method,
Path: path,
Handler: name,
Method: method,
Path: path,
Name: name,
}
e.router.routes[method+path] = r
return r
}
// 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.
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)
ln := len(params)
n := 0
name := handlerName(handler)
for _, r := range e.router.routes {
if r.Handler == name {
if r.Name == name {
for i, l := 0, len(r.Path); i < l; i++ {
if r.Path[i] == ':' && n < ln {
for ; i < l && r.Path[i] != '/'; i++ {
@ -502,11 +527,6 @@ func (e *Echo) URI(handler HandlerFunc, params ...interface{}) 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.
func (e *Echo) Routes() []*Route {
routes := []*Route{}
@ -653,7 +673,7 @@ func NewHTTPError(code int, message ...interface{}) *HTTPError {
// Error makes it compatible with `error` interface.
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`.

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
// doesn't find a match, making none of the group middleware process.
g.echo.Any(path.Clean(g.prefix+"/*"), func(c Context) error {
return ErrNotFound
return NotFoundHandler(c)
}, g.middleware...)
}
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group.
func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(CONNECT, path, h, m...)
func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(CONNECT, path, h, m...)
}
// DELETE implements `Echo#DELETE()` for sub-routes within the Group.
func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(DELETE, path, h, m...)
func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(DELETE, path, h, m...)
}
// GET implements `Echo#GET()` for sub-routes within the Group.
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(GET, path, h, m...)
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(GET, path, h, m...)
}
// HEAD implements `Echo#HEAD()` for sub-routes within the Group.
func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(HEAD, path, h, m...)
func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(HEAD, path, h, m...)
}
// OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group.
func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(OPTIONS, path, h, m...)
func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(OPTIONS, path, h, m...)
}
// PATCH implements `Echo#PATCH()` for sub-routes within the Group.
func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(PATCH, path, h, m...)
func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(PATCH, path, h, m...)
}
// POST implements `Echo#POST()` for sub-routes within the Group.
func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(POST, path, h, m...)
func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(POST, path, h, m...)
}
// PUT implements `Echo#PUT()` for sub-routes within the Group.
func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(PUT, path, h, m...)
func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(PUT, path, h, m...)
}
// TRACE implements `Echo#TRACE()` for sub-routes within the Group.
func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) {
g.add(TRACE, path, h, m...)
func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
return g.Add(TRACE, path, h, m...)
}
// Any implements `Echo#Any()` for sub-routes within the Group.
func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) {
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.
func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) {
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)
}
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
// multiple routes, which would lead to later add() calls overwriting the
// middleware from earlier calls.
m := []MiddlewareFunc{}
m = append(m, g.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 (
"encoding/base64"
"strconv"
"strings"
"github.com/labstack/echo"
)
@ -27,7 +28,7 @@ type (
)
const (
basic = "Basic"
basic = "basic"
defaultRealm = "Restricted"
)
@ -54,7 +55,7 @@ func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc {
func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
// Defaults
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 {
config.Skipper = DefaultBasicAuthConfig.Skipper
@ -72,7 +73,7 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
auth := c.Request().Header.Get(echo.HeaderAuthorization)
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:])
if err != nil {
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 (
// DefaultBodyLimitConfig is the default Gzip middleware config.
// DefaultBodyLimitConfig is the default BodyLimit middleware config.
DefaultBodyLimitConfig = BodyLimitConfig{
Skipper: DefaultSkipper,
}
@ -60,7 +60,7 @@ func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {
limit, err := bytes.Parse(config.Limit)
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
pool := limitedReaderPool(config)

View File

@ -67,7 +67,7 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
res := c.Response()
res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
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
w, err := gzip.NewWriterLevel(rw, config.Level)
if err != nil {
@ -98,6 +98,7 @@ func (w *gzipResponseWriter) WriteHeader(code int) {
if code == http.StatusNoContent { // Issue #489
w.ResponseWriter.Header().Del(echo.HeaderContentEncoding)
}
w.Header().Del(echo.HeaderContentLength) // Issue #444
w.ResponseWriter.WriteHeader(code)
}

View File

@ -1,7 +1,6 @@
package middleware
import (
"errors"
"fmt"
"net/http"
"reflect"
@ -57,6 +56,12 @@ const (
AlgorithmHS256 = "HS256"
)
// Errors
var (
ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "Missing or malformed jwt")
ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired jwt")
)
var (
// DefaultJWTConfig is the default JWT auth middleware config.
DefaultJWTConfig = JWTConfig{
@ -77,7 +82,7 @@ var (
//
// See: https://jwt.io/introduction
// See `JWTConfig.TokenLookup`
func JWT(key []byte) echo.MiddlewareFunc {
func JWT(key interface{}) echo.MiddlewareFunc {
c := DefaultJWTConfig
c.SigningKey = key
return JWTWithConfig(c)
@ -134,14 +139,15 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
auth, err := extractor(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
return err
}
token := new(jwt.Token)
// Issue #647, #656
if _, ok := config.Claims.(jwt.MapClaims); ok {
token, err = jwt.Parse(auth, config.keyFunc)
} 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)
}
if err == nil && token.Valid {
@ -149,7 +155,11 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
c.Set(config.ContextKey, token)
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 {
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) {
token := c.QueryParam(param)
if token == "" {
return "", errors.New("Missing jwt in the query string")
return "", ErrJWTMissing
}
return token, nil
}
@ -182,7 +192,7 @@ func jwtFromCookie(name string) jwtExtractor {
return func(c echo.Context) (string, error) {
cookie, err := c.Cookie(name)
if err != nil {
return "", errors.New("Missing jwt in the cookie")
return "", ErrJWTMissing
}
return cookie.Value, nil
}

View File

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

View File

@ -1,7 +1,6 @@
package middleware
import (
"errors"
"fmt"
"io"
"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 {
return httputil.NewSingleHostReverseProxy(t.URL)
}
func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h, ok := w.(http.Hijacker)
if !ok {
c.Error(errors.New("proxy raw, not a hijacker"))
return
}
in, _, err := h.Hijack()
in, _, err := c.Response().Hijack()
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
}
defer in.Close()
out, err := net.Dial("tcp", t.URL.Host)
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)
return
}
defer out.Close()
// Write header
err = r.Write(out)
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)
return
}
@ -97,7 +99,7 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
go cp(in, out)
err = <-errc
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
}
// Proxy returns an HTTP/WebSocket reverse proxy middleware.
func Proxy(config ProxyConfig) echo.MiddlewareFunc {
// Proxy returns a Proxy middleware.
//
// 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
if config.Skipper == nil {
config.Skipper = DefaultLoggerConfig.Skipper
@ -130,6 +142,10 @@ func Proxy(config ProxyConfig) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
res := c.Response()
tgt := config.Balancer.Next()

View File

@ -5,7 +5,6 @@ import (
"runtime"
"github.com/labstack/echo"
"github.com/labstack/gommon/color"
)
type (
@ -64,17 +63,14 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
defer func() {
if r := recover(); r != nil {
var err error
switch r := r.(type) {
case error:
err = r
default:
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
stack := make([]byte, config.StackSize)
length := runtime.Stack(stack, !config.DisableStackAll)
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)
}

View File

@ -2,6 +2,7 @@ package middleware
import (
"fmt"
"net/http"
"os"
"path"
"path/filepath"
@ -66,7 +67,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c echo.Context) (err error) {
if config.Skipper(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*`.
p = c.Param("*")
}
p, err = echo.PathUnescape(p)
if err != nil {
return
}
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
fi, err := os.Stat(name)
if err != nil {
if os.IsNotExist(err) {
if config.HTML5 && path.Ext(p) == "" {
return c.File(filepath.Join(config.Root, config.Index))
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
}
return next(c)
}
return err
return
}
if fi.IsDir() {
@ -99,7 +108,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if os.IsNotExist(err) {
return next(c)
}
return err
return
}
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)
if err != nil {
return err
return
}
dirs, err := dir.Readdir(-1)
if err != nil {
return err
return
}
// Create a directory index
res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
if _, err = fmt.Fprintf(res, "<pre>\n"); err != nil {
return err
return
}
for _, d := range dirs {
name := d.Name()
@ -133,9 +142,9 @@ func listDir(name string, res *echo.Response) error {
name += "/"
}
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")
return err
return
}

View File

@ -11,11 +11,12 @@ type (
// by an HTTP handler to construct an HTTP response.
// See: https://golang.org/pkg/net/http/#ResponseWriter
Response struct {
Writer http.ResponseWriter
Status int
Size int64
Committed bool
echo *Echo
echo *Echo
beforeFuncs []func()
Writer http.ResponseWriter
Status int
Size int64
Committed bool
}
)
@ -34,6 +35,11 @@ func (r *Response) Header() http.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
// not called explicitly, the first call to Write will trigger an implicit
// 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")
return
}
for _, fn := range r.beforeFuncs {
fn()
}
r.Status = code
r.Writer.WriteHeader(code)
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 nn != nil {
cn = nn
nn = nil // Next
nn = cn.parent // Next (Issue #954)
search = ns
if nk == pkind {
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()
// Built-in things that should always be supported.
c.Handlers.register(true, RPL_WELCOME, HandlerFunc(func(c *Client, e Event) {
go handleConnect(c, e)
}))
c.Handlers.register(true, PING, HandlerFunc(handlePING))
c.Handlers.register(true, PONG, HandlerFunc(handlePONG))
c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect))
c.Handlers.register(true, false, PING, HandlerFunc(handlePING))
c.Handlers.register(true, false, PONG, HandlerFunc(handlePONG))
if !c.Config.disableTracking {
// Joins/parts/anything that may add/remove/rename users.
c.Handlers.register(true, JOIN, HandlerFunc(handleJOIN))
c.Handlers.register(true, PART, HandlerFunc(handlePART))
c.Handlers.register(true, KICK, HandlerFunc(handleKICK))
c.Handlers.register(true, QUIT, HandlerFunc(handleQUIT))
c.Handlers.register(true, NICK, HandlerFunc(handleNICK))
c.Handlers.register(true, RPL_NAMREPLY, HandlerFunc(handleNAMES))
c.Handlers.register(true, false, JOIN, HandlerFunc(handleJOIN))
c.Handlers.register(true, false, PART, HandlerFunc(handlePART))
c.Handlers.register(true, false, KICK, HandlerFunc(handleKICK))
c.Handlers.register(true, false, QUIT, HandlerFunc(handleQUIT))
c.Handlers.register(true, false, NICK, HandlerFunc(handleNICK))
c.Handlers.register(true, false, RPL_NAMREPLY, HandlerFunc(handleNAMES))
// Modes.
c.Handlers.register(true, MODE, HandlerFunc(handleMODE))
c.Handlers.register(true, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
c.Handlers.register(true, false, MODE, HandlerFunc(handleMODE))
c.Handlers.register(true, false, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
// WHO/WHOX responses.
c.Handlers.register(true, RPL_WHOREPLY, HandlerFunc(handleWHO))
c.Handlers.register(true, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
c.Handlers.register(true, false, RPL_WHOREPLY, HandlerFunc(handleWHO))
c.Handlers.register(true, false, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
// Other misc. useful stuff.
c.Handlers.register(true, TOPIC, HandlerFunc(handleTOPIC))
c.Handlers.register(true, RPL_TOPIC, HandlerFunc(handleTOPIC))
c.Handlers.register(true, RPL_MYINFO, HandlerFunc(handleMYINFO))
c.Handlers.register(true, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
c.Handlers.register(true, RPL_MOTDSTART, HandlerFunc(handleMOTD))
c.Handlers.register(true, RPL_MOTD, HandlerFunc(handleMOTD))
c.Handlers.register(true, false, TOPIC, HandlerFunc(handleTOPIC))
c.Handlers.register(true, false, RPL_TOPIC, HandlerFunc(handleTOPIC))
c.Handlers.register(true, false, RPL_MYINFO, HandlerFunc(handleMYINFO))
c.Handlers.register(true, false, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
c.Handlers.register(true, false, RPL_MOTDSTART, HandlerFunc(handleMOTD))
c.Handlers.register(true, false, RPL_MOTD, HandlerFunc(handleMOTD))
// Keep users lastactive times up to date.
c.Handlers.register(true, PRIVMSG, HandlerFunc(updateLastActive))
c.Handlers.register(true, NOTICE, HandlerFunc(updateLastActive))
c.Handlers.register(true, TOPIC, HandlerFunc(updateLastActive))
c.Handlers.register(true, KICK, HandlerFunc(updateLastActive))
c.Handlers.register(true, false, PRIVMSG, HandlerFunc(updateLastActive))
c.Handlers.register(true, false, NOTICE, HandlerFunc(updateLastActive))
c.Handlers.register(true, false, TOPIC, HandlerFunc(updateLastActive))
c.Handlers.register(true, false, KICK, HandlerFunc(updateLastActive))
// CAP IRCv3-specific tracking and functionality.
c.Handlers.register(true, CAP, HandlerFunc(handleCAP))
c.Handlers.register(true, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY))
c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
c.Handlers.register(true, ALL_EVENTS, HandlerFunc(handleTags))
c.Handlers.register(true, false, CAP, HandlerFunc(handleCAP))
c.Handlers.register(true, false, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
c.Handlers.register(true, false, CAP_AWAY, HandlerFunc(handleAWAY))
c.Handlers.register(true, false, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
c.Handlers.register(true, false, ALL_EVENTS, HandlerFunc(handleTags))
// SASL IRCv3 support.
c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL))
c.Handlers.register(true, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
c.Handlers.register(true, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLFAIL, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
c.Handlers.register(true, ERR_SASLABORTED, HandlerFunc(handleSASLError))
c.Handlers.register(true, RPL_SASLMECHS, HandlerFunc(handleSASLError))
c.Handlers.register(true, false, AUTHENTICATE, HandlerFunc(handleSASL))
c.Handlers.register(true, false, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
c.Handlers.register(true, false, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
c.Handlers.register(true, false, ERR_SASLFAIL, HandlerFunc(handleSASLError))
c.Handlers.register(true, false, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
c.Handlers.register(true, false, ERR_SASLABORTED, HandlerFunc(handleSASLError))
c.Handlers.register(true, false, RPL_SASLMECHS, HandlerFunc(handleSASLError))
}
// Nickname collisions.
c.Handlers.register(true, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, false, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, false, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
c.Handlers.register(true, false, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler))
c.Handlers.mu.Unlock()
}
@ -389,7 +387,7 @@ func handleISUPPORT(c *Client, e Event) {
c.state.Lock()
// Skip the first parameter, as it's our nickname.
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]) {
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.
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
// due to cap-notify, we can re-evaluate what we can support.
@ -375,11 +375,11 @@ func handleTags(c *Client, e Event) {
}
const (
prefixTag byte = 0x40 // @
prefixTagValue byte = 0x3D // =
prefixUserTag byte = 0x2B // +
tagSeparator byte = 0x3B // ;
maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included.
prefixTag byte = '@'
prefixTagValue byte = '='
prefixUserTag byte = '+'
tagSeparator byte = ';'
maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included.
)
// Tags represents the key-value pairs in IRCv3 message tags. The map contains
@ -618,7 +618,7 @@ func validTag(name string) bool {
for i := 0; i < len(name); i++ {
// 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
}
}
@ -631,7 +631,7 @@ func validTag(name string) bool {
func validTagValue(value string) bool {
for i := 0; i < len(value); i++ {
// 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
}
}

View File

@ -191,18 +191,6 @@ func (conf *Config) isValid() error {
// connected.
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.
func New(config Config) *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
// event. This should cause Connect() to return with nil. This should be
// 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.
func (c *Client) IsConnected() (connected bool) {
func (c *Client) IsConnected() bool {
c.mu.RLock()
if c.conn == nil {
c.mu.RUnlock()
@ -395,7 +414,7 @@ func (c *Client) IsConnected() (connected bool) {
}
c.conn.mu.RLock()
connected = c.conn.connected
connected := c.conn.connected
c.conn.mu.RUnlock()
c.mu.RUnlock()
@ -445,9 +464,9 @@ func (c *Client) GetHost() string {
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.
func (c *Client) Channels() []string {
func (c *Client) ChannelList() []string {
c.panicIfNotTracking()
c.state.RLock()
@ -463,9 +482,26 @@ func (c *Client) Channels() []string {
return channels
}
// Users returns the active list of users that the client is tracking across
// all files. Panics if tracking is disabled.
func (c *Client) Users() []string {
// Channels returns the active channels that the client is in. Panics if
// tracking is disabled.
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.state.RLock()
@ -481,6 +517,23 @@ func (c *Client) Users() []string {
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
// exist, nil is returned. Panics if tracking is disabled.
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
// does not support RPL_MYINFO. Will panic if used when tracking has been
// disabled.
func (c *Client) ServerVersion() (version string) {
func (c *Client) ServerVersion() string {
c.panicIfNotTracking()
version, _ = c.GetServerOption("VERSION")
version, _ := c.GetServerOption("VERSION")
return version
}
// 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.
func (c *Client) ServerMOTD() (motd string) {
func (c *Client) ServerMOTD() string {
c.panicIfNotTracking()
c.state.RLock()
motd = c.state.motd
motd := c.state.motd
c.state.RUnlock()
return motd
}
// Lag is the latency between the server and the client. This is measured by
// determining the difference in time between when we ping the server, and
// Latency is the latency between the server and the client. This is measured
// by determining the difference in time between when we ping the server, and
// when we receive a pong.
func (c *Client) Lag() time.Duration {
func (c *Client) Latency() time.Duration {
c.mu.RLock()
c.conn.mu.RLock()
delta := c.conn.lastPong.Sub(c.conn.lastPing)

View File

@ -12,8 +12,9 @@ import (
// Input is a wrapper for events, based around private messages.
type Input struct {
Origin *girc.Event
Args []string
Origin *girc.Event
Args []string
RawArgs string
}
// Command is an IRC command, supporting aliases, help documentation and easy
@ -189,8 +190,9 @@ func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) {
}
in := &Input{
Origin: &event,
Args: args,
Origin: &event,
Args: args,
RawArgs: parsed[2],
}
go cmd.Fn(client, in)

View File

@ -16,18 +16,13 @@ type Commands struct {
}
// Nick changes the client nickname.
func (cmd *Commands) Nick(name string) error {
if !IsValidNick(name) {
return &ErrInvalidTarget{Target: name}
}
func (cmd *Commands) Nick(name string) {
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
// 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 are not exceeding the line length. (see maxLength)
max := maxLength - len(JOIN) - 1
@ -35,10 +30,6 @@ func (cmd *Commands) Join(channels ...string) error {
var buffer string
for i := 0; i < len(channels); i++ {
if !IsValidChannel(channels[i]) {
return &ErrInvalidTarget{Target: channels[i]}
}
if len(buffer+","+channels[i]) > max {
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
buffer = ""
@ -53,91 +44,74 @@ func (cmd *Commands) Join(channels ...string) error {
if i == len(channels)-1 {
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
return nil
return
}
}
return nil
}
// JoinKey attempts to enter an IRC channel with a password.
func (cmd *Commands) JoinKey(channel, password string) error {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
}
func (cmd *Commands) JoinKey(channel, password string) {
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
return nil
}
// Part leaves an IRC channel.
func (cmd *Commands) Part(channel, message string) error {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
func (cmd *Commands) Part(channels ...string) {
for i := 0; i < len(channels); i++ {
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.
func (cmd *Commands) PartMessage(channel, message string) error {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
}
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
return nil
func (cmd *Commands) PartMessage(channel, message string) {
cmd.c.Send(&Event{Command: PART, Params: []string{channel}, Trailing: message, EmptyTrailing: true})
}
// SendCTCP sends a CTCP request to target. Note that this method uses
// PRIVMSG specifically.
func (cmd *Commands) SendCTCP(target, ctcpType, message string) error {
// PRIVMSG specifically. ctcpType is the CTCP command, e.g. "FINGER", "TIME",
// "VERSION", etc.
func (cmd *Commands) SendCTCP(target, ctcpType, message string) {
out := encodeCTCPRaw(ctcpType, message)
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
// this method uses PRIVMSG specifically.
func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) error {
return cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...))
// this method uses PRIVMSG specifically. ctcpType is the CTCP command, e.g.
// "FINGER", "TIME", "VERSION", etc.
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.
// Note that this method uses NOTICE specifically.
func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error {
return cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...))
// Note that this method uses NOTICE specifically. ctcpType is the CTCP
// command, e.g. "FINGER", "TIME", "VERSION", etc.
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
// NOTICE specifically.
func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) error {
func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) {
out := encodeCTCPRaw(ctcpType, message)
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).
func (cmd *Commands) Message(target, message string) error {
if !IsValidNick(target) && !IsValidChannel(target) {
return &ErrInvalidTarget{Target: target}
}
cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
return nil
func (cmd *Commands) Message(target, message string) {
cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message, EmptyTrailing: true})
}
// Messagef sends a formated PRIVMSG to target (either channel, service, or
// user).
func (cmd *Commands) Messagef(target, format string, a ...interface{}) error {
return cmd.Message(target, fmt.Sprintf(format, a...))
func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
cmd.Message(target, fmt.Sprintf(format, a...))
}
// 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")
// Reply sends a reply to channel or user, based on where the supplied event
// originated from. See also ReplyTo().
func (cmd *Commands) Reply(event Event, message string) error {
// originated from. See also ReplyTo(). Panics if the incoming event has no
// source.
func (cmd *Commands) Reply(event Event, message string) {
if event.Source == nil {
return ErrInvalidSource
panic(ErrInvalidSource)
}
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
// where the supplied event originated from. See also ReplyTof().
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error {
return cmd.Reply(event, fmt.Sprintf(format, a...))
// where the supplied event originated from. See also ReplyTof(). Panics if
// the incoming event has no source.
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
// event originated from. ReplyTo(), when originating from a channel will
// default to replying with "<user>, <message>". See also Reply().
func (cmd *Commands) ReplyTo(event Event, message string) error {
// default to replying with "<user>, <message>". See also Reply(). Panics if
// the incoming event has no source.
func (cmd *Commands) ReplyTo(event Event, message string) {
if event.Source == nil {
return ErrInvalidSource
panic(ErrInvalidSource)
}
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
// on where the supplied event originated from. ReplyTo(), when originating
// from a channel will default to replying with "<user>, <message>". See
// also Replyf().
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error {
return cmd.ReplyTo(event, fmt.Sprintf(format, a...))
// also Replyf(). Panics if the incoming event has no source.
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) {
cmd.ReplyTo(event, fmt.Sprintf(format, a...))
}
// Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
// or user).
func (cmd *Commands) Action(target, message string) error {
if !IsValidNick(target) && !IsValidChannel(target) {
return &ErrInvalidTarget{Target: target}
}
func (cmd *Commands) Action(target, message string) {
cmd.c.Send(&Event{
Command: PRIVMSG,
Params: []string{target},
Trailing: fmt.Sprintf("\001ACTION %s\001", message),
})
return nil
}
// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
// service, or user).
func (cmd *Commands) Actionf(target, format string, a ...interface{}) error {
return cmd.Action(target, fmt.Sprintf(format, a...))
func (cmd *Commands) Actionf(target, format string, a ...interface{}) {
cmd.Action(target, fmt.Sprintf(format, a...))
}
// Notice sends a NOTICE to target (either channel, service, or user).
func (cmd *Commands) Notice(target, message string) error {
if !IsValidNick(target) && !IsValidChannel(target) {
return &ErrInvalidTarget{Target: target}
}
cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
return nil
func (cmd *Commands) Notice(target, message string) {
cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message, EmptyTrailing: true})
}
// Noticef sends a formated NOTICE to target (either channel, service, or
// user).
func (cmd *Commands) Noticef(target, format string, a ...interface{}) error {
return cmd.Notice(target, fmt.Sprintf(format, a...))
func (cmd *Commands) Noticef(target, format string, a ...interface{}) {
cmd.Notice(target, fmt.Sprintf(format, a...))
}
// SendRaw sends a raw string back to the server, without carriage returns
// or newlines.
func (cmd *Commands) SendRaw(raw string) error {
e := ParseEvent(raw)
if e == nil {
return errors.New("invalid event: " + raw)
// SendRaw sends a raw string (or multiple) to the server, without carriage
// returns or newlines. Returns an error if one of the raw strings cannot be
// properly parsed.
func (cmd *Commands) SendRaw(raw ...string) error {
var event *Event
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
}
@ -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
// of the topic.
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.
// 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
// girc's builtin tracking functionality.
func (cmd *Commands) Who(target string) error {
if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) {
return &ErrInvalidTarget{Target: target}
func (cmd *Commands) Who(users ...string) {
for i := 0; i < len(users); i++ {
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.
// as WHOIS is a bit slower, you may want to use WHO for brief user info.
func (cmd *Commands) Whois(nick string) error {
if !IsValidNick(nick) {
return &ErrInvalidTarget{Target: nick}
// Whois sends a WHOIS query to the server, targeted at a specific user (or
// set of users). As WHOIS is a bit slower, you may want to use WHO for brief
// user info.
func (cmd *Commands) Whois(users ...string) {
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
@ -294,36 +264,40 @@ func (cmd *Commands) Oper(user, pass string) {
// 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
// server.
func (cmd *Commands) Kick(channel, nick, reason string) error {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
}
if !IsValidNick(nick) {
return &ErrInvalidTarget{Target: nick}
}
func (cmd *Commands) Kick(channel, user, reason string) {
if reason != "" {
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason})
return nil
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}, Trailing: reason, EmptyTrailing: true})
}
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}})
return nil
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}})
}
// 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.
func (cmd *Commands) Invite(channel, nick string) error {
if !IsValidChannel(channel) {
return &ErrInvalidTarget{Target: channel}
func (cmd *Commands) Invite(channel string, users ...string) {
for i := 0; i < len(users); i++ {
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
@ -348,10 +322,10 @@ func (cmd *Commands) Back() {
// 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
// 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 {
cmd.c.Send(&Event{Command: LIST})
return nil
return
}
// 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
for i := 0; i < len(channels); i++ {
if !IsValidChannel(channels[i]) {
return &ErrInvalidTarget{Target: channels[i]}
}
if len(buffer+","+channels[i]) > max {
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
buffer = ""
@ -379,20 +349,13 @@ func (cmd *Commands) List(channels ...string) error {
if i == len(channels)-1 {
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
// you want back.
func (cmd *Commands) Whowas(nick string, amount int) error {
if !IsValidNick(nick) {
return &ErrInvalidTarget{Target: nick}
}
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}})
return nil
func (cmd *Commands) Whowas(user string, amount int) {
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, string(amount)}})
}

View File

@ -58,7 +58,7 @@ func decodeCTCP(e *Event) *CTCPEvent {
if s < 0 {
for i := 0; i < len(text); i++ {
// 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
}
}
@ -74,7 +74,7 @@ func decodeCTCP(e *Event) *CTCPEvent {
// Loop through checking the tag first.
for i := 0; i < s; i++ {
// 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
}
}
@ -168,7 +168,7 @@ func (c *CTCP) parseCMD(cmd string) string {
for i := 0; i < len(cmd); i++ {
// 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 ""
}
}

View File

@ -11,8 +11,8 @@ import (
)
const (
eventSpace byte = 0x20 // Separator.
maxLength = 510 // Maximum length is 510 (2 for line endings).
eventSpace byte = ' ' // Separator.
maxLength = 510 // Maximum length is 510 (2 for line endings).
)
// cutCRFunc is used to trim CR characters from prefixes/messages.
@ -256,7 +256,7 @@ func (e *Event) Bytes() []byte {
// Strip newlines and carriage returns.
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:]...)
i-- // Decrease the index so we can pick up where we left off.
}
@ -432,9 +432,9 @@ func (e *Event) StripAction() string {
}
const (
messagePrefix byte = 0x3A // ":" -- prefix or last argument
prefixIdent byte = 0x21 // "!" -- username
prefixHost byte = 0x40 // "@" -- hostname
messagePrefix byte = ':' // Prefix or last argument.
prefixIdent byte = '!' // Username.
prefixHost byte = '@' // Hostname.
)
// Source represents the sender of an IRC event, see RFC1459 section 2.3.1.

View File

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

View File

@ -29,11 +29,12 @@ func (c *Client) RunHandlers(event *Event) {
}
}
// Regular wildcard handlers.
c.Handlers.exec(ALL_EVENTS, c, event.Copy())
// Background handlers first.
c.Handlers.exec(ALL_EVENTS, true, c, event.Copy())
c.Handlers.exec(event.Command, true, c, event.Copy())
// Then regular handlers.
c.Handlers.exec(event.Command, c, event.Copy())
c.Handlers.exec(ALL_EVENTS, false, c, event.Copy())
c.Handlers.exec(event.Command, false, c, event.Copy())
// Check if it's a CTCP.
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
// external/internal handler maps.
func (c *Caller) cuidToID(input string) (cmd, uid string) {
i := strings.IndexByte(input, 0x3A)
i := strings.IndexByte(input, ':')
if i < 0 {
return "", ""
}
@ -160,9 +161,9 @@ type execStack struct {
// exec executes all handlers pertaining to specified event. Internal first,
// then external.
//
// Please note that there is no specific order/priority for which the
// handler types themselves or the handlers are executed.
func (c *Caller) exec(command string, client *Client, event *Event) {
// Please note that there is no specific order/priority for which the handlers
// are executed.
func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
// Build a stack of handlers which can be executed concurrently.
var stack []execStack
@ -170,13 +171,21 @@ func (c *Caller) exec(command string, client *Client, event *Event) {
// Get internal handlers first.
if _, ok := c.internal[command]; ok {
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})
}
}
// Aaand then external handlers.
// Then external handlers.
if _, ok := c.external[command]; ok {
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})
}
}
@ -189,18 +198,29 @@ func (c *Caller) exec(command string, client *Client, event *Event) {
wg.Add(len(stack))
for i := 0; i < len(stack); i++ {
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()
// If they want to catch any panics, add to defer stack.
if bg {
go func() {
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))
}()
return
}
if client.Config.RecoverFunc != nil {
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
}
stack[index].Execute(client, *event)
c.debug.Printf("execution of %s took %s (%d of %d)", stack[index].cuid, time.Since(start), index+1, len(stack))
wg.Done()
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
}(i)
}
@ -281,9 +301,9 @@ func (c *Caller) remove(cuid string) (success bool) {
// sregister is much like Caller.register(), except that it safely locks
// 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()
cuid = c.register(internal, cmd, handler)
cuid = c.register(internal, bg, cmd, handler)
c.mu.Unlock()
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
// 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
cmd = strings.ToUpper(cmd)
cuid, uid = c.cuid(cmd, 20)
if bg {
uid += ":bg"
cuid += ":bg"
}
if internal {
if _, ok := c.internal[cmd]; !ok {
c.internal[cmd] = map[string]Handler{}
}
cuid, uid = c.cuid(cmd, 20)
c.internal[cmd][uid] = handler
} else {
if _, ok := c.external[cmd]; !ok {
c.external[cmd] = map[string]Handler{}
}
cuid, uid = c.cuid(cmd, 20)
c.external[cmd][uid] = handler
}
_, 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
}
@ -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
// handler with Caller.Remove().
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
// 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) {
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
// in a go-routine. cuid is the handler uid which can be used to remove the
// handler with Caller.Remove().
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) {
// 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)
}()
}))
return c.sregister(false, true, cmd, HandlerFunc(handler))
}
// 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
// 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
// server does not respond appropriately, or takes too long to respond.
//
// Note that handlers supplied with AddTmp are executed in a goroutine to
// ensure that they are not blocking other handlers. Additionally, 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.
// ensure that they are not blocking other handlers. However, if you are
// creating a temporary handler from another handler, it should be a
// background handler.
//
// 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{}) {
var uid string
cuid, uid = c.cuid(cmd, 20)
done = make(chan struct{})
c.mu.Lock()
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)
cuid = c.sregister(false, true, cmd, HandlerFunc(func(client *Client, event Event) {
remove := handler(client, event)
if remove {
if ok := c.Remove(cuid); ok {
close(done)
}
remove := handler(client, event)
if remove {
if ok := c.Remove(cuid); ok {
close(done)
}
}
}()
})
c.mu.Unlock()
}
}))
if deadline > 0 {
go func() {
<-time.After(deadline)
select {
case <-time.After(deadline):
case <-done:
}
if ok := c.Remove(cuid); ok {
close(done)
}

View File

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

View File

@ -79,7 +79,7 @@ func (cli *Client) BuildBaseURL(urlPath ...string) 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 {
u, _ := url.Parse(cli.BuildURL(urlPath...))
q := u.Query()
@ -387,6 +387,20 @@ func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{
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
func (cli *Client) SetDisplayName(displayName string) (err error) {
urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
@ -450,6 +464,35 @@ func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
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
func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) {
txnID := txnID()
@ -518,6 +561,14 @@ func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanU
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
// 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
@ -556,8 +607,15 @@ func (cli *Client) UploadToContentRepo(content io.Reader, contentType string, co
return nil, err
}
if res.StatusCode != 200 {
contents, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, HTTPError{
Message: "Upload request failed - Failed to read response body: " + err.Error(),
Code: res.StatusCode,
}
}
return nil, HTTPError{
Message: "Upload request failed",
Message: "Upload request failed: " + string(contents),
Code: res.StatusCode,
}
}
@ -588,6 +646,34 @@ func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
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 {
return "go" + strconv.FormatInt(time.Now().UnixNano(), 10)
}

View File

@ -7,13 +7,13 @@ import (
// Event represents a single Matrix event.
type Event struct {
StateKey string `json:"state_key"` // The state key for the event. Only present on State Events.
Sender string `json:"sender"` // The user ID of the sender of the event
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
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)
Content map[string]interface{} `json:"content"` // The JSON content of the event.
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
Type string `json:"type"` // The event type
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
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.
}
// Body returns the value of the "body" key in the event content if it is
@ -44,12 +44,31 @@ type TextMessage struct {
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 {
Height uint `json:"h"`
Width uint `json:"w"`
Mimetype string `json:"mimetype"`
Size uint `json:"size"`
Height uint `json:"h,omitempty"`
Width uint `json:"w,omitempty"`
Mimetype string `json:"mimetype,omitempty"`
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

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 {
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
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
type RespJoinedRooms struct {
JoinedRooms []string `json:"joined_rooms"`
@ -58,6 +61,13 @@ type RespJoinedMembers struct {
} `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
type RespSendEvent struct {
EventID string `json:"event_id"`
@ -90,6 +100,11 @@ func (r RespUserInteractive) HasSingleStageFlow(stageName string) bool {
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
type RespRegister struct {
AccessToken string `json:"access_token"`
@ -125,6 +140,16 @@ type RespSync struct {
Events []Event `json:"events"`
} `json:"presence"`
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 {
State struct {
Events []Event `json:"events"`
@ -142,3 +167,10 @@ type RespSync struct {
} `json:"invite"`
} `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 {
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.

View File

@ -73,6 +73,16 @@ func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error)
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
}
@ -102,7 +112,7 @@ func (s *DefaultSyncer) shouldProcessResponse(resp *RespSync, since string) bool
for roomID, roomData := range resp.Rooms.Join {
for i := len(roomData.Timeline.Events) - 1; i >= 0; 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"]
mship, ok := m.(string)
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.
OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional.
Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional.
URL string `json:"url,omitempty"` // Optional.
}
// AttachmentActionOption the individual option to appear in action menu.
@ -48,6 +49,9 @@ type AttachmentActionCallback struct {
Channel Channel `json:"channel"`
User User `json:"user"`
Name string `json:"name"`
Value string `json:"value"`
OriginalMessage Message `json:"original_message"`
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
func (api *Client) ArchiveChannel(channel string) error {
return api.ArchiveChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.archive
func (api *Client) ArchiveChannel(channelID string) error {
return api.ArchiveChannelContext(context.Background(), channelID)
}
// 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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
_, err := channelRequest(ctx, "channels.archive", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// UnarchiveChannel unarchives the given channel
func (api *Client) UnarchiveChannel(channel string) error {
return api.UnarchiveChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.unarchive
func (api *Client) UnarchiveChannel(channelID string) error {
return api.UnarchiveChannelContext(context.Background(), channelID)
}
// 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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
_, err := channelRequest(ctx, "channels.unarchive", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// CreateChannel creates a channel with the given name and returns a *Channel
func (api *Client) CreateChannel(channel string) (*Channel, error) {
return api.CreateChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.create
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
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{
"token": {api.config.token},
"name": {channel},
"name": {channelName},
}
response, err := channelRequest(ctx, "channels.create", values, api.debug)
if err != nil {
@ -92,15 +92,17 @@ func (api *Client) CreateChannelContext(ctx context.Context, channel string) (*C
}
// GetChannelHistory retrieves the channel history
func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) {
return api.GetChannelHistoryContext(context.Background(), channel, params)
// see https://api.slack.com/methods/channels.history
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
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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
if params.Latest != DEFAULT_HISTORY_LATEST {
values.Add("latest", params.Latest)
@ -133,15 +135,17 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channel string,
}
// GetChannelInfo retrieves the given channel
func (api *Client) GetChannelInfo(channel string) (*Channel, error) {
return api.GetChannelInfoContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.info
func (api *Client) GetChannelInfo(channelID string) (*Channel, error) {
return api.GetChannelInfoContext(context.Background(), channelID)
}
// 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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
response, err := channelRequest(ctx, "channels.info", values, api.debug)
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
func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) {
return api.InviteUserToChannelContext(context.Background(), channel, user)
// see https://api.slack.com/methods/channels.invite
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
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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"user": {user},
}
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
func (api *Client) JoinChannel(channel string) (*Channel, error) {
return api.JoinChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.join
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
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{
"token": {api.config.token},
"name": {channel},
"name": {channelName},
}
response, err := channelRequest(ctx, "channels.join", values, api.debug)
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
func (api *Client) LeaveChannel(channel string) (bool, error) {
return api.LeaveChannelContext(context.Background(), channel)
// see https://api.slack.com/methods/channels.leave
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
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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
}
response, err := channelRequest(ctx, "channels.leave", values, api.debug)
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
func (api *Client) KickUserFromChannel(channel, user string) error {
return api.KickUserFromChannelContext(context.Background(), channel, user)
// see https://api.slack.com/methods/channels.kick
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
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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"user": {user},
}
_, err := channelRequest(ctx, "channels.kick", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// GetChannels retrieves all the channels
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived)
}
// 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) {
values := url.Values{
"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
// (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.
func (api *Client) SetChannelReadMark(channel, ts string) error {
return api.SetChannelReadMarkContext(context.Background(), channel, ts)
// see https://api.slack.com/methods/channels.mark
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
// 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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"ts": {ts},
}
_, err := channelRequest(ctx, "channels.mark", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// RenameChannel renames a given channel
func (api *Client) RenameChannel(channel, name string) (*Channel, error) {
return api.RenameChannelContext(context.Background(), channel, name)
// see https://api.slack.com/methods/channels.rename
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
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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"name": {name},
}
// 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
func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) {
return api.SetChannelPurposeContext(context.Background(), channel, purpose)
// see https://api.slack.com/methods/channels.setPurpose
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
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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"purpose": {purpose},
}
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
func (api *Client) SetChannelTopic(channel, topic string) (string, error) {
return api.SetChannelTopicContext(context.Background(), channel, topic)
// see https://api.slack.com/methods/channels.setTopic
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
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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"topic": {topic},
}
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).
func (api *Client) GetChannelReplies(channel, thread_ts string) ([]Message, error) {
return api.GetChannelRepliesContext(context.Background(), channel, thread_ts)
// see https://api.slack.com/methods/channels.replies
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
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{
"token": {api.config.token},
"channel": {channel},
"channel": {channelID},
"thread_ts": {thread_ts},
}
response, err := channelRequest(ctx, "channels.replies", values, api.debug)

View File

@ -11,6 +11,7 @@ import (
const (
DEFAULT_MESSAGE_USERNAME = ""
DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
DEFAULT_MESSAGE_REPLY_BROADCAST = false
DEFAULT_MESSAGE_ASUSER = false
DEFAULT_MESSAGE_PARSE = ""
DEFAULT_MESSAGE_LINK_NAMES = 0
@ -36,6 +37,7 @@ type PostMessageParameters struct {
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"`
ReplyBroadcast bool `json:"reply_broadcast"`
LinkNames int `json:"link_names"`
Attachments []Attachment `json:"attachments"`
UnfurlLinks bool `json:"unfurl_links"`
@ -44,12 +46,17 @@ type PostMessageParameters struct {
IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"`
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
func NewPostMessageParameters() PostMessageParameters {
return PostMessageParameters{
Username: DEFAULT_MESSAGE_USERNAME,
User: DEFAULT_MESSAGE_USERNAME,
AsUser: DEFAULT_MESSAGE_ASUSER,
Parse: DEFAULT_MESSAGE_PARSE,
LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
@ -102,6 +109,37 @@ func (api *Client) PostMessageContext(ctx context.Context, channel, text string,
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
func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) {
return api.UpdateMessageContext(context.Background(), channel, timestamp, text)
@ -171,9 +209,10 @@ func chatRequest(ctx context.Context, path string, values url.Values, debug bool
type sendMode string
const (
chatUpdate sendMode = "chat.update"
chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete"
chatUpdate sendMode = "chat.update"
chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral"
)
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.
func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error {
@ -279,6 +327,11 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
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.
MsgOptionAsUser(params.AsUser)(config)
@ -314,6 +367,9 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP {
config.values.Set("thread_ts", params.ThreadTimestamp)
}
if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST {
config.values.Set("reply_broadcast", "true")
}
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},
}
_, err := fileRequest(ctx, "files.delete", values, api.debug)
if err != nil {
return err
}
return nil
return err
}

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
@ -42,6 +43,14 @@ func (s WebError) Error() string {
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) {
body := &bytes.Buffer{}
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))
}
err = json.Unmarshal(response, &intf)
if err != nil {
return err
}
return nil
return json.Unmarshal(response, &intf)
}
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()
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.
if resp.StatusCode != 200 {
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
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()
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.
if resp.StatusCode != 200 {
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
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 {
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)
websocketURL, err = websocketizeURLPort(response.Info.URL)
if err != nil {
return nil, "", fmt.Errorf("parsing response URL: %s", err)
}
return &response.Info, websocketURL, nil
return &response.Info, response.Info.URL, nil
}
// 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 {
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)
websocketURL, err = websocketizeURLPort(response.Info.URL)
if err != nil {
return nil, "", fmt.Errorf("parsing response URL: %s", err)
}
return &response.Info, websocketURL, nil
return &response.Info, response.Info.URL, nil
}
// NewRTM returns a RTM, which provides a fully managed connection to
@ -90,6 +72,7 @@ func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
isConnected: false,
wasIntentional: true,
killChannel: make(chan bool),
disconnected: make(chan struct{}),
forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1),

View File

@ -3,12 +3,13 @@ package slack
import (
"context"
"errors"
"fmt"
"log"
"net/url"
"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
*/
@ -41,12 +42,31 @@ type Client struct {
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
// can be logged along with the application's debugging info.
func SetLogger(l *log.Logger) {
func SetLogger(l stdLogger) {
logger = l
}
// New creates new Client.
func New(token string) *Client {
s := &Client{}
s.config.token = token
@ -83,12 +103,12 @@ func (api *Client) SetDebug(debug bool) {
func (api *Client) Debugf(format string, v ...interface{}) {
if api.debug {
logger.Printf(format, v...)
logger.Output(2, fmt.Sprintf(format, v...))
}
}
func (api *Client) Debugln(v ...interface{}) {
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},
}
_, err := userRequest(ctx, "users.setActive", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// 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
func (api *Client) SetUserPhoto(ctx context.Context, image string, params UserSetPhotoParams) error {
return api.SetUserPhoto(context.Background(), image, params)
func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
return api.SetUserPhotoContext(context.Background(), image, params)
}
// 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
outgoingMessages chan OutgoingMessage
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
rawEvents chan json.RawMessage
wasIntentional bool
@ -59,9 +60,14 @@ type RTMOptions struct {
// Disconnect and wait, blocking until a successful disconnection.
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 {
return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
}
rtm.killChannel <- true
return nil
}

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