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

Compare commits

..

69 Commits

Author SHA1 Message Date
Wim
822605c157 Release v0.15.0 2017-06-19 20:47:41 +02:00
Wim
e49266ae43 Update gitter vendor. (fixes crash) 2017-06-19 20:27:14 +02:00
Wim
62e9de1a3b Use the last (and biggest) photo to relay (telegram). Closes #184 2017-06-18 23:59:52 +02:00
Wim
2ddc4f7ae9 Add UserID to each message. Closes #200 2017-06-18 15:44:54 +02:00
Wim
2dd402675d Sent only the biggest picture to bridges (telegram) 2017-06-18 01:23:15 +02:00
Wim
25b1af1e11 Add option IgnoreMessages to ignore messages based on regexp. (all). Closes #70 2017-06-18 01:08:11 +02:00
Wim
75fb2b8156 Make reconnection more robust (irc). #153 2017-06-18 00:13:10 +02:00
Wim
2a403f8b85 Add initial sticker/video/photo/document support (telegram). #184 2017-06-17 18:25:17 +02:00
Wim
c3d45a9f06 Do not relay join/part of ourselves (irc). Closes #190 2017-06-17 17:58:56 +02:00
Wim
c07b85b625 Add note about private channels (rocketchat). See #180 2017-06-15 23:05:59 +02:00
Wim
511f653e6e Fix incorrect behaviour of EditDisable (mattermost). Fixes #197 2017-06-15 22:45:34 +02:00
Wim
5636eaca6d Bump version 2017-06-15 22:45:23 +02:00
Wim
4b839b9958 Avoid nil in usermembermap (discord). See #198 2017-06-15 22:29:01 +02:00
Wim
3f79da84d5 Release v0.14.0 2017-06-15 01:44:46 +02:00
Wim
d540638223 Remove debug 2017-06-15 01:30:58 +02:00
Wim
4ec9b6dd4e Add 3.10.0 support (mattermost) 2017-06-15 01:30:05 +02:00
Wim
3bc219167a Remove need for channel when using api. Closes #195 2017-06-15 00:40:23 +02:00
Wim
8a55c97b4e Fix utf-8 issues #193 2017-06-15 00:07:12 +02:00
9e34162a09 remove second flag.Parse() (#196)
flag.Parse() is already being called on line 28 https://github.com/42wim/matterbridge/blob/master/matterbridge.go#L28
and there is no need for calling it again
2017-06-14 17:15:35 +02:00
Wim
860a371eeb Use cache for teamid 2017-06-12 20:30:30 +02:00
Wim
41a46526a1 Add note about file permissions 2017-06-08 23:42:00 +02:00
Wim
46b798ac1b Update documentation (api) 2017-06-08 00:03:06 +02:00
Wim
359d0f2910 Allow reuse of api in different gateways. See #189 2017-06-07 23:54:50 +02:00
Wim
ad3cb0386b Add token authentication (api) 2017-06-06 00:05:32 +02:00
Wim
3a183cb218 Update vendor 2017-06-06 00:04:18 +02:00
Wim
2eecaccd1c Change to lowercase JSON keys (api) 2017-06-05 23:18:13 +02:00
Wim
5f30a98bc1 Add gateway name to messages 2017-06-05 23:12:19 +02:00
Wim
b8a2fcbaff Post valid JSON (api). See #185 2017-06-05 23:08:36 +02:00
Wim
01496cd080 Fix panic (mattermost). Closes #186 2017-06-05 21:35:38 +02:00
Wim
6a968ab82a Bump version 2017-06-03 18:22:09 +02:00
Wim
c0c4890887 Add hashtag to channel (discord) 2017-06-03 18:21:47 +02:00
Wim
171a53592d Add note about lowercase channel (irc) 2017-06-01 21:00:58 +02:00
Wim
7811c330db Release v0.13.0 2017-05-31 23:32:38 +02:00
Wim
9bcd131e66 Reset variables each loop (telegram). Closes #181 2017-05-30 21:14:03 +02:00
Wim
c791423dd5 Add NOPINGNICK option. Closes #175 2017-05-30 00:11:53 +02:00
Wim
80bdf38388 Bump version 2017-05-29 23:54:43 +02:00
Wim
9d9cb32f4e Limit message length (irc). Closes #179 2017-05-29 21:54:34 +02:00
Wim
87229bab13 Fix sending to different channels on same account (slack). Closes #177 2017-05-24 22:10:21 +02:00
Wim
f065e9e4d5 Release v0.12.1 2017-05-23 22:48:05 +02:00
Wim
3812693111 Replace long ids in channel metions (discord). Fixes #174 2017-05-23 22:26:37 +02:00
Wim
dd3c572256 Fix possible crash on nil (discord) 2017-05-22 21:57:19 +02:00
Wim
c5dfe40326 Update documentation about encrypted rooms in matrix 2017-05-21 15:36:40 +02:00
ef278301e3 Fix JoinChannel argument to use IRC channel key (#172) 2017-05-21 15:23:56 +02:00
Wim
2888fd64b0 Add UseFirstName option (telegram). Closes #144 2017-05-15 23:23:10 +02:00
Wim
27c0f37e49 Update matterbridge.toml.sample about NoHomeServerSuffix 2017-05-15 23:11:27 +02:00
Wim
0774f6a5e7 Bump version 2017-05-12 23:20:22 +02:00
Wim
4036d4459b Add NoHomeServerSuffix. Option to disable homeserver on username (matrix). Closes #160. 2017-05-12 23:04:58 +02:00
ee643de5b6 Add Compatibility for Cisco Jabber (xmpp) (#166) 2017-05-11 20:10:53 +02:00
Wim
8c7549a09e Update changelog 2017-05-09 23:46:20 +02:00
Wim
7a16146304 Release v0.12.0 2017-05-09 23:31:26 +02:00
Wim
3d3809a21b Add 3.9.0 support (mattermost) 2017-05-09 23:30:53 +02:00
29465397dd Add support for HTTP{S}_PROXY env variables (#162) 2017-05-08 21:20:52 +02:00
Wim
d300bb1735 Relay messages starting with ! (irc). Closes #164 2017-05-08 21:15:01 +02:00
Wim
2e703472f1 Fix crash on reconnects when server is down. Closes #163 2017-05-08 20:44:36 +02:00
Wim
8fede90b9e Remove examples (vendor issues) 2017-05-05 20:45:11 +02:00
Wim
d128f157c4 Update doc 2017-04-19 21:57:40 +02:00
Wim
4fcedabfd0 Revert "Add support for edited messages (gitter)"
This reverts commit 17b8b86d68.
Reverted because of lingering file descriptors (memory leak)
2017-04-19 19:51:33 +02:00
Wim
246c8e4f74 Ignore error on private channel join (slack) Fixes #150 2017-04-17 18:01:24 +02:00
Wim
4d2207aba7 Add support for edited messages (slack) 2017-04-16 00:16:24 +02:00
Wim
17b8b86d68 Add support for edited messages (gitter) 2017-04-15 23:46:01 +02:00
Wim
fdb57230a3 Add support for edited messages (mattermost) 2017-04-15 20:21:57 +02:00
Wim
7469732bbc Add support for edited messages (telegram) 2017-04-15 19:07:35 +02:00
Wim
d1dd6c3440 Add support for edited messages (discord) 2017-04-15 19:00:15 +02:00
Wim
02612c0061 Add support for sending edited messages 2017-04-15 18:46:25 +02:00
Wim
a4db63a773 Bump version 2017-04-15 16:24:25 +02:00
Wim
035c2b906a Strip custom emoji metadata (discord). Closes #148 2017-04-15 16:23:34 +02:00
Wim
6ea8be5749 Release v0.11.0 2017-04-11 21:51:23 +02:00
Wim
36024d5439 Add 3.8.0 support (mattermost) 2017-04-09 23:15:11 +02:00
Wim
8d52c98373 Update README 2017-04-08 00:57:11 +02:00
74 changed files with 1109 additions and 1665 deletions

View File

@ -28,7 +28,7 @@ Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, R
# Requirements # Requirements
Accounts to one of the supported bridges Accounts to one of the supported bridges
* [Mattermost](https://github.com/mattermost/platform/) 3.5.x - 3.7.x * [Mattermost](https://github.com/mattermost/platform/) 3.5.x - 3.10.x
* [IRC](http://www.mirc.com/servers.html) * [IRC](http://www.mirc.com/servers.html)
* [XMPP](https://jabber.org) * [XMPP](https://jabber.org)
* [Gitter](https://gitter.im) * [Gitter](https://gitter.im)
@ -42,7 +42,7 @@ Accounts to one of the supported bridges
# Installing # Installing
## Binaries ## Binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/) Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/)
* Latest release [v0.10.3](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v0.15.0](https://github.com/42wim/matterbridge/releases/latest)
## Building ## Building
Go 1.6+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH) Go 1.6+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)

View File

@ -4,6 +4,7 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/zfjagann/golang-ring" "github.com/zfjagann/golang-ring"
"net/http" "net/http"
"sync" "sync"
@ -20,7 +21,9 @@ type Api struct {
type ApiMessage struct { type ApiMessage struct {
Text string `json:"text"` Text string `json:"text"`
Username string `json:"username"` Username string `json:"username"`
UserID string `json:"userid"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Gateway string `json:"gateway"`
} }
var flog *log.Entry var flog *log.Entry
@ -38,6 +41,11 @@ func New(cfg config.Protocol, account string, c chan config.Message) *Api {
b.Config = &cfg b.Config = &cfg
b.Account = account b.Account = account
b.Remote = c b.Remote = c
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/messages", b.handleMessages)
e.POST("/api/message", b.handlePostMessage) e.POST("/api/message", b.handlePostMessage)
go func() { go func() {
@ -70,12 +78,16 @@ func (b *Api) handlePostMessage(c echo.Context) error {
if err := c.Bind(message); err != nil { if err := c.Bind(message); err != nil {
return err return err
} }
flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
b.Remote <- config.Message{ b.Remote <- config.Message{
Text: message.Text, Text: message.Text,
Username: message.Username, Username: message.Username,
UserID: message.UserID,
Channel: "api", Channel: "api",
Avatar: message.Avatar, Avatar: message.Avatar,
Account: b.Account, Account: b.Account,
Gateway: message.Gateway,
Protocol: "api",
} }
return c.JSON(http.StatusOK, message) return c.JSON(http.StatusOK, message)
} }
@ -83,9 +95,7 @@ func (b *Api) handlePostMessage(c echo.Context) error {
func (b *Api) handleMessages(c echo.Context) error { func (b *Api) handleMessages(c echo.Context) error {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
for _, msg := range b.Messages.Values() { c.JSONPretty(http.StatusOK, b.Messages.Values(), " ")
c.JSONPretty(http.StatusOK, msg, " ")
}
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
return nil return nil
} }

View File

@ -100,7 +100,7 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map
log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name) log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
mychannel = mychannel + " " + channel.Options.Key mychannel = mychannel + " " + channel.Options.Key
} }
err := b.JoinChannel(channel.Name) err := b.JoinChannel(mychannel)
if err != nil { if err != nil {
return err return err
} }

View File

@ -16,14 +16,16 @@ const (
) )
type Message struct { type Message struct {
Text string Text string `json:"text"`
Channel string Channel string `json:"channel"`
Username string Username string `json:"username"`
Avatar string UserID string `json:"userid"` // userid on the bridge
Account string Avatar string `json:"avatar"`
Event string Account string `json:"account"`
Protocol string Event string `json:"event"`
Timestamp time.Time Protocol string `json:"protocol"`
Gateway string `json:"gateway"`
Timestamp time.Time `json:"timestamp"`
} }
type ChannelInfo struct { type ChannelInfo struct {
@ -39,8 +41,11 @@ type ChannelInfo struct {
type Protocol struct { type Protocol struct {
BindAddress string // mattermost, slack BindAddress string // mattermost, slack
Buffer int // api Buffer int // api
EditSuffix string // mattermost, slack, discord, telegram, gitter
EditDisable bool // mattermost, slack, discord, telegram, gitter
IconURL string // mattermost, slack IconURL string // mattermost, slack
IgnoreNicks string // all protocols IgnoreNicks string // all protocols
IgnoreMessages string // all protocols
Jid string // xmpp Jid string // xmpp
Login string // mattermost, matrix Login string // mattermost, matrix
Muc string // xmpp Muc string // xmpp
@ -50,23 +55,26 @@ type Protocol struct {
NickServNick string // IRC NickServNick string // IRC
NickServPassword string // IRC NickServPassword string // IRC
NicksPerRow int // mattermost, slack NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix
NoTLS bool // mattermost NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP,matrix Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack PrefixMessagesWithNick bool // mattemost, slack
Protocol string //all protocols Protocol string //all protocols
MessageQueue int // IRC, size of message queue for flood control MessageQueue int // IRC, size of message queue for flood control
MessageDelay int // IRC, time in millisecond to wait between messages MessageDelay int // IRC, time in millisecond to wait between messages
MessageLength int // IRC, max length of a message allowed
MessageFormat string // telegram MessageFormat string // telegram
RemoteNickFormat string // all protocols RemoteNickFormat string // all protocols
Server string // IRC,mattermost,XMPP,discord Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols ShowJoinPart bool // all protocols
SkipTLSVerify bool // IRC, mattermost SkipTLSVerify bool // IRC, mattermost
Team string // mattermost Team string // mattermost
Token string // gitter, slack, discord Token string // gitter, slack, discord, api
URL string // mattermost, slack, matrix URL string // mattermost, slack, matrix
UseAPI bool // mattermost, slack UseAPI bool // mattermost, slack
UseSASL bool // IRC UseSASL bool // IRC
UseTLS bool // IRC UseTLS bool // IRC
UseFirstName bool // telegram
} }
type ChannelOptions struct { type ChannelOptions struct {

View File

@ -4,6 +4,7 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"regexp"
"strings" "strings"
"sync" "sync"
) )
@ -51,6 +52,7 @@ func (b *bdiscord) Connect() error {
flog.Info("Connection succeeded") flog.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate) b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.memberUpdate) b.c.AddHandler(b.memberUpdate)
b.c.AddHandler(b.messageUpdate)
err = b.c.Open() err = b.c.Open()
if err != nil { if err != nil {
flog.Debugf("%#v", err) flog.Debugf("%#v", err)
@ -103,6 +105,18 @@ func (b *bdiscord) Send(msg config.Message) error {
return nil return nil
} }
func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
if b.Config.EditDisable {
return
}
// only when message is actually edited
if m.Message.EditedTimestamp != "" {
flog.Debugf("Sending edit message")
m.Content = m.Content + b.Config.EditSuffix
b.messageCreate(s, (*discordgo.MessageCreate)(m))
}
}
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// not relay our own messages // not relay our own messages
if m.Author.Username == b.Nick { if m.Author.Username == b.Nick {
@ -125,8 +139,11 @@ func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
if len(m.MentionRoles) > 0 { if len(m.MentionRoles) > 0 {
m.Message.Content = b.replaceRoleMentions(m.Message.Content) m.Message.Content = b.replaceRoleMentions(m.Message.Content)
} }
m.Message.Content = b.stripCustomoji(m.Message.Content)
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
b.Remote <- config.Message{Username: username, Text: m.ContentWithMentionsReplaced(), Channel: channelName, b.Remote <- config.Message{Username: username, Text: m.ContentWithMentionsReplaced(), Channel: channelName,
Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg"} Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg",
UserID: m.Author.ID}
} }
func (b *bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { func (b *bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
@ -143,18 +160,21 @@ func (b *bdiscord) getNick(user *discordgo.User) string {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
if _, ok := b.userMemberMap[user.ID]; ok { if _, ok := b.userMemberMap[user.ID]; ok {
if b.userMemberMap[user.ID].Nick != "" { if b.userMemberMap[user.ID] != nil {
// only return if nick is set if b.userMemberMap[user.ID].Nick != "" {
return b.userMemberMap[user.ID].Nick // only return if nick is set
return b.userMemberMap[user.ID].Nick
}
// otherwise return username
return user.Username
} }
// otherwise return username
return user.Username
} }
// if we didn't find nick, search for it // if we didn't find nick, search for it
b.userMemberMap[user.ID], err = b.c.GuildMember(b.guildID, user.ID) member, err := b.c.GuildMember(b.guildID, user.ID)
if err != nil { if err != nil {
return user.Username return user.Username
} }
b.userMemberMap[user.ID] = member
// only return if nick is set // only return if nick is set
if b.userMemberMap[user.ID].Nick != "" { if b.userMemberMap[user.ID].Nick != "" {
return b.userMemberMap[user.ID].Nick return b.userMemberMap[user.ID].Nick
@ -195,3 +215,28 @@ func (b *bdiscord) replaceRoleMentions(text string) string {
} }
return text return text
} }
func (b *bdiscord) replaceChannelMentions(text string) string {
var err error
re := regexp.MustCompile("<#[0-9]+>")
text = re.ReplaceAllStringFunc(text, func(m string) string {
channel := b.getChannelName(m[2 : len(m)-1])
// if at first don't succeed, try again
if channel == "" {
b.Channels, err = b.c.GuildChannels(b.guildID)
if err != nil {
return "#unknownchannel"
}
channel = b.getChannelName(m[2 : len(m)-1])
return "#" + channel
}
return "#" + channel
})
return text
}
func (b *bdiscord) stripCustomoji(text string) string {
// <:doge:302803592035958784>
re := regexp.MustCompile("<(:.*?:)[0-9]+>")
return re.ReplaceAllString(text, `$1`)
}

View File

@ -82,7 +82,7 @@ func (b *Bgitter) JoinChannel(channel string) error {
if !strings.HasSuffix(ev.Message.Text, "") { if !strings.HasSuffix(ev.Message.Text, "") {
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
b.Remote <- config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room, b.Remote <- config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username)} Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID}
} }
case *gitter.GitterConnectionClosed: case *gitter.GitterConnectionClosed:
flog.Errorf("connection with gitter closed for room %s", room) flog.Errorf("connection with gitter closed for room %s", room)

View File

@ -15,14 +15,15 @@ import (
) )
type Birc struct { type Birc struct {
i *irc.Connection i *irc.Connection
Nick string Nick string
names map[string][]string names map[string][]string
Config *config.Protocol Config *config.Protocol
Remote chan config.Message Remote chan config.Message
connected chan struct{} connected chan struct{}
Local chan config.Message // local queue for flood control Local chan config.Message // local queue for flood control
Account string Account string
FirstConnection bool
} }
var flog *log.Entry var flog *log.Entry
@ -46,6 +47,10 @@ func New(cfg config.Protocol, account string, c chan config.Message) *Birc {
if b.Config.MessageQueue == 0 { if b.Config.MessageQueue == 0 {
b.Config.MessageQueue = 30 b.Config.MessageQueue = 30
} }
if b.Config.MessageLength == 0 {
b.Config.MessageLength = 400
}
b.FirstConnection = true
return b return b
} }
@ -71,6 +76,8 @@ func (b *Birc) Connect() error {
i.SASLLogin = b.Config.NickServNick i.SASLLogin = b.Config.NickServNick
i.SASLPassword = b.Config.NickServPassword i.SASLPassword = b.Config.NickServPassword
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify} i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify}
i.KeepAlive = time.Minute
i.PingFreq = time.Minute
if b.Config.Password != "" { if b.Config.Password != "" {
i.Password = b.Config.Password i.Password = b.Config.Password
} }
@ -87,6 +94,14 @@ func (b *Birc) Connect() error {
return fmt.Errorf("connection timed out") return fmt.Errorf("connection timed out")
} }
i.Debug = false i.Debug = false
// clear on reconnects
i.ClearCallback(ircm.RPL_WELCOME)
i.AddCallback(ircm.RPL_WELCOME, func(event *irc.Event) {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
// set our correct nick on reconnect if necessary
b.Nick = event.Nick
})
go i.Loop()
go b.doSend() go b.doSend()
return nil return nil
} }
@ -109,9 +124,11 @@ func (b *Birc) Send(msg config.Message) error {
} }
if strings.HasPrefix(msg.Text, "!") { if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg) b.Command(&msg)
return nil
} }
for _, text := range strings.Split(msg.Text, "\n") { for _, text := range strings.Split(msg.Text, "\n") {
if len(text) > b.Config.MessageLength {
text = text[:b.Config.MessageLength] + " <message clipped>"
}
if len(b.Local) < b.Config.MessageQueue { if len(b.Local) < b.Config.MessageQueue {
if len(b.Local) == b.Config.MessageQueue-1 { if len(b.Local) == b.Config.MessageQueue-1 {
text = text + " <message clipped>" text = text + " <message clipped>"
@ -187,8 +204,11 @@ func (b *Birc) handleJoinPart(event *irc.Event) {
return return
} }
} }
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account) if event.Nick != b.Nick {
b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE} flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
return
}
flog.Debugf("handle %#v", event) flog.Debugf("handle %#v", event)
} }
@ -209,6 +229,11 @@ func (b *Birc) handleOther(event *irc.Event) {
} }
func (b *Birc) handlePrivMsg(event *irc.Event) { func (b *Birc) handlePrivMsg(event *irc.Event) {
b.Nick = b.i.GetNick()
// freenode doesn't send 001 as first reply
if event.Code == "NOTICE" {
return
}
// don't forward queries to the bot // don't forward queries to the bot
if event.Arguments[0] == b.Nick { if event.Arguments[0] == b.Nick {
return return
@ -227,7 +252,7 @@ func (b *Birc) handlePrivMsg(event *irc.Event) {
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`) re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
msg = re.ReplaceAllString(msg, "") msg = re.ReplaceAllString(msg, "")
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.Account) flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.Account)
b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Account: b.Account} b.Remote <- config.Message{Username: event.Nick, Text: msg, Channel: event.Arguments[0], Account: b.Account, UserID: event.User + "@" + event.Host}
} }
func (b *Birc) handleTopicWhoTime(event *irc.Event) { func (b *Birc) handleTopicWhoTime(event *irc.Event) {

View File

@ -1,10 +1,12 @@
package bmatrix package bmatrix
import ( import (
"regexp"
"sync"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
matrix "github.com/matrix-org/gomatrix" matrix "github.com/matrix-org/gomatrix"
"sync"
) )
type Bmatrix struct { type Bmatrix struct {
@ -101,8 +103,13 @@ func (b *Bmatrix) handlematrix() error {
flog.Debugf("Unknown room %s", ev.RoomID) flog.Debugf("Unknown room %s", ev.RoomID)
return return
} }
username := ev.Sender[1:]
if b.Config.NoHomeServerSuffix {
re := regexp.MustCompile("(.*?):.*")
username = re.ReplaceAllString(username, `$1`)
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account) flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- config.Message{Username: ev.Sender, Text: ev.Content["body"].(string), Channel: channel, Account: b.Account} b.Remote <- config.Message{Username: username, Text: ev.Content["body"].(string), Channel: channel, Account: b.Account, UserID: ev.Sender}
} }
flog.Debugf("Received: %#v", ev) flog.Debugf("Received: %#v", ev)
}) })

View File

@ -21,6 +21,7 @@ type MMMessage struct {
Text string Text string
Channel string Channel string
Username string Username string
UserID string
} }
type Bmattermost struct { type Bmattermost struct {
@ -127,7 +128,7 @@ func (b *Bmattermost) handleMatter() {
} }
for message := range mchan { for message := range mchan {
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.Username, Channel: message.Channel, Account: b.Account} b.Remote <- config.Message{Text: message.Text, Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID}
} }
} }
@ -141,14 +142,22 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE} b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
continue continue
} }
if (message.Raw.Event == "post_edited") && b.Config.EditDisable {
continue
}
// do not post our own messages back to irc // do not post our own messages back to irc
// only listen to message from our team // only listen to message from our team
if message.Raw.Event == "posted" && b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId { if (message.Raw.Event == "posted" || message.Raw.Event == "post_edited") &&
b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
flog.Debugf("Receiving from matterclient %#v", message) flog.Debugf("Receiving from matterclient %#v", message)
m := &MMMessage{} m := &MMMessage{}
m.UserID = message.UserID
m.Username = message.Username m.Username = message.Username
m.Channel = message.Channel m.Channel = message.Channel
m.Text = message.Text m.Text = message.Text
if message.Raw.Event == "post_edited" && !b.Config.EditDisable {
m.Text = message.Text + b.Config.EditSuffix
}
if len(message.Post.FileIds) > 0 { if len(message.Post.FileIds) > 0 {
for _, link := range b.mc.GetPublicLinks(message.Post.FileIds) { for _, link := range b.mc.GetPublicLinks(message.Post.FileIds) {
m.Text = m.Text + "\n" + link m.Text = m.Text + "\n" + link
@ -164,6 +173,7 @@ func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
message := b.mh.Receive() message := b.mh.Receive()
flog.Debugf("Receiving from matterhook %#v", message) flog.Debugf("Receiving from matterhook %#v", message)
m := &MMMessage{} m := &MMMessage{}
m.UserID = message.UserID
m.Username = message.UserName m.Username = message.UserName
m.Text = message.Text m.Text = message.Text
m.Channel = message.ChannelName m.Channel = message.ChannelName

View File

@ -82,6 +82,6 @@ func (b *Brocketchat) handleRocketHook() {
continue continue
} }
flog.Debugf("Sending message from %s on %s to gateway", message.UserName, b.Account) flog.Debugf("Sending message from %s on %s to gateway", message.UserName, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account} b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account, UserID: message.UserID}
} }
} }

View File

@ -15,6 +15,7 @@ type MMMessage struct {
Text string Text string
Channel string Channel string
Username string Username string
UserID string
Raw *slack.MessageEvent Raw *slack.MessageEvent
} }
@ -79,7 +80,9 @@ func (b *Bslack) JoinChannel(channel string) error {
} }
_, err := b.sc.JoinChannel(channel) _, err := b.sc.JoinChannel(channel)
if err != nil { if err != nil {
return err if err.Error() != "name_taken" {
return err
}
} }
} }
return nil return nil
@ -87,9 +90,6 @@ func (b *Bslack) JoinChannel(channel string) error {
func (b *Bslack) Send(msg config.Message) error { func (b *Bslack) Send(msg config.Message) error {
flog.Debugf("Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Account == b.Account {
return nil
}
nick := msg.Username nick := msg.Username
message := msg.Text message := msg.Text
channel := msg.Channel channel := msg.Channel
@ -186,7 +186,7 @@ func (b *Bslack) handleSlack() {
texts := strings.Split(message.Text, "\n") texts := strings.Split(message.Text, "\n")
for _, text := range texts { for _, text := range texts {
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username)} b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username), UserID: message.UserID}
} }
} }
} }
@ -199,6 +199,11 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
// ignore first message // ignore first message
if count > 0 { if count > 0 {
flog.Debugf("Receiving from slackclient %#v", ev) flog.Debugf("Receiving from slackclient %#v", ev)
if !b.Config.EditDisable && ev.SubMessage != nil {
flog.Debugf("SubMessage %#v", ev.SubMessage)
ev.User = ev.SubMessage.User
ev.Text = ev.SubMessage.Text + b.Config.EditSuffix
}
// use our own func because rtm.GetChannelInfo doesn't work for private channels // use our own func because rtm.GetChannelInfo doesn't work for private channels
channel, err := b.getChannelByID(ev.Channel) channel, err := b.getChannelByID(ev.Channel)
if err != nil { if err != nil {
@ -209,6 +214,7 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
continue continue
} }
m := &MMMessage{} m := &MMMessage{}
m.UserID = user.ID
m.Username = user.Name m.Username = user.Name
m.Channel = channel.Name m.Channel = channel.Name
m.Text = ev.Text m.Text = ev.Text

View File

@ -76,29 +76,36 @@ func (b *Btelegram) Send(msg config.Message) error {
} }
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
username := ""
text := ""
channel := ""
for update := range updates { for update := range updates {
var message *tgbotapi.Message var message *tgbotapi.Message
username := ""
channel := ""
text := ""
// handle channels // handle channels
if update.ChannelPost != nil { if update.ChannelPost != nil {
message = update.ChannelPost message = update.ChannelPost
} }
if update.EditedChannelPost != nil { if update.EditedChannelPost != nil && !b.Config.EditDisable {
message = update.EditedChannelPost message = update.EditedChannelPost
message.Text = message.Text + b.Config.EditSuffix
} }
// handle groups // handle groups
if update.Message != nil { if update.Message != nil {
message = update.Message message = update.Message
} }
if update.EditedMessage != nil { if update.EditedMessage != nil && !b.Config.EditDisable {
message = update.EditedMessage message = update.EditedMessage
message.Text = message.Text + b.Config.EditSuffix
} }
if message.From != nil { if message.From != nil {
username = message.From.FirstName if b.Config.UseFirstName {
username = message.From.FirstName
}
if username == "" { if username == "" {
username = message.From.UserName username = message.From.UserName
if username == "" {
username = message.From.FirstName
}
} }
text = message.Text text = message.Text
channel = strconv.FormatInt(message.Chat.ID, 10) channel = strconv.FormatInt(message.Chat.ID, 10)
@ -107,9 +114,31 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
if username == "" { if username == "" {
username = "unknown" username = "unknown"
} }
if message.Sticker != nil {
text = text + " " + b.getFileDirectURL(message.Sticker.FileID)
}
if message.Video != nil {
text = text + " " + b.getFileDirectURL(message.Video.FileID)
}
if message.Photo != nil {
photos := *message.Photo
// last photo is the biggest
text = text + " " + b.getFileDirectURL(photos[len(photos)-1].FileID)
}
if message.Document != nil {
text = text + " " + message.Document.FileName + " : " + b.getFileDirectURL(message.Document.FileID)
}
if text != "" { if text != "" {
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
b.Remote <- config.Message{Username: username, Text: text, Channel: channel, Account: b.Account} b.Remote <- config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID)}
} }
} }
} }
func (b *Btelegram) getFileDirectURL(id string) string {
res, err := b.c.GetFileDirectURL(id)
if err != nil {
return ""
}
return res
}

View File

@ -119,7 +119,7 @@ func (b *Bxmpp) handleXmpp() error {
var channel, nick string var channel, nick string
if v.Type == "groupchat" { if v.Type == "groupchat" {
s := strings.Split(v.Remote, "@") s := strings.Split(v.Remote, "@")
if len(s) == 2 { if len(s) >= 2 {
channel = s[0] channel = s[0]
} }
s = strings.Split(s[1], "/") s = strings.Split(s[1], "/")
@ -128,7 +128,7 @@ func (b *Bxmpp) handleXmpp() error {
} }
if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" { if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" {
flog.Debugf("Sending message from %s on %s to gateway", nick, b.Account) flog.Debugf("Sending message from %s on %s to gateway", nick, b.Account)
b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account} b.Remote <- config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account, UserID: v.Remote}
} }
} }
case xmpp.Presence: case xmpp.Presence:

View File

@ -1,4 +1,80 @@
# v0.11.0-dev # v0.15.0
## New features
* general: add option IgnoreMessages for all protocols (see mattebridge.toml.sample)
Messages matching these regexp will be ignored and not sent to other bridges
e.g. IgnoreMessages="^~~ badword"
* telegram: add support for sticker/video/photo/document #184
## Changes
* api: add userid to each message #200
## Bugfix
* discord: fix crash in memberupdate #198
* mattermost: Fix incorrect behaviour of EditDisable (mattermost). Fixes #197
* irc: Do not relay join/part of ourselves (irc). Closes #190
* irc: make reconnections more robust. #153
* gitter: update library, fixes possible crash
# v0.14.0
## New features
* api: add token authentication
* mattermost: add support for mattermost 3.10.0
## Changes
* api: gateway name is added in JSON messages
* api: lowercase JSON keys
* api: channel name isn't needed in config #195
## Bugfix
* discord: Add hashtag to channelname (when translating from id) (discord)
* mattermost: Fix a panic. #186
* mattermost: use teamid cache if possible. Fixes a panic
* api: post valid json. #185
* api: allow reuse of api in different gateways. #189
* general: Fix utf-8 issues for {NOPINGNICK}. #193
# v0.13.0
## New features
* irc: Limit message length. ```MessageLength=400```
Maximum length of message sent to irc server. If it exceeds <message clipped> will be add to the message.
* irc: Add NOPINGNICK option.
The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged.
See https://github.com/42wim/matterbridge/issues/175 for more information
## Bugfix
* slack: Fix sending to different channels on same account (slack). Closes #177
* telegram: Fix incorrect usernames being sent. Closes #181
# v0.12.1
## New features
* telegram: Add UseFirstName option (telegram). Closes #144
* matrix: Add NoHomeServerSuffix. Option to disable homeserver on username (matrix). Closes #160.
## Bugfix
* xmpp: Add Compatibility for Cisco Jabber (xmpp) (#166)
* irc: Fix JoinChannel argument to use IRC channel key (#172)
* discord: Fix possible crash on nil (discord)
* discord: Replace long ids in channel metions (discord). Fixes #174
# v0.12.0
## Changes
* general: edited messages are now being sent by default on discord/mattermost/telegram/slack. See "New Features"
## New features
* general: add support for edited messages.
Add new keyword EditDisable (false/true), default false. Which means by default edited messages will be sent to other bridges.
Add new keyword EditSuffix , default "". You can change this eg to "(edited)", this will be appended to every edit message.
* mattermost: support mattermost v3.9.x
* general: Add support for HTTP{S}_PROXY env variables (#162)
* discord: Strip custom emoji metadata (discord). Closes #148
## Bugfix
* slack: Ignore error on private channel join (slack) Fixes #150
* mattermost: fix crash on reconnects when server is down. Closes #163
* irc: Relay messages starting with ! (irc). Closes #164
# v0.11.0
## New features ## New features
* general: reusing the same account on multiple gateways now also reuses the connection. * general: reusing the same account on multiple gateways now also reuses the connection.
This is particuarly useful for irc. See #87 This is particuarly useful for irc. See #87
@ -6,6 +82,7 @@
* telegram: Support edited messages (telegram). See #141 * telegram: Support edited messages (telegram). See #141
* mattermost: Add support for showing/hiding join/leave messages from mattermost. Closes #147 * mattermost: Add support for showing/hiding join/leave messages from mattermost. Closes #147
* mattermost: Reconnect on session removal/timeout (mattermost) * mattermost: Reconnect on session removal/timeout (mattermost)
* mattermost: Support mattermost v3.8.x
* irc: Rejoin channel when kicked (irc). * irc: Rejoin channel when kicked (irc).
## Bugfix ## Bugfix

View File

@ -5,6 +5,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"regexp"
"strings" "strings"
"time" "time"
) )
@ -139,6 +140,9 @@ RECONNECT:
func (gw *Gateway) mapChannels() error { func (gw *Gateway) mapChannels() error {
for _, br := range append(gw.MyConfig.Out, gw.MyConfig.InOut...) { for _, br := range append(gw.MyConfig.Out, gw.MyConfig.InOut...) {
if isApi(br.Account) {
br.Channel = "api"
}
ID := br.Channel + br.Account ID := br.Channel + br.Account
_, ok := gw.Channels[ID] _, ok := gw.Channels[ID]
if !ok { if !ok {
@ -153,6 +157,9 @@ func (gw *Gateway) mapChannels() error {
} }
for _, br := range append(gw.MyConfig.In, gw.MyConfig.InOut...) { for _, br := range append(gw.MyConfig.In, gw.MyConfig.InOut...) {
if isApi(br.Account) {
br.Channel = "api"
}
ID := br.Channel + br.Account ID := br.Channel + br.Account
_, ok := gw.Channels[ID] _, ok := gw.Channels[ID]
if !ok { if !ok {
@ -174,7 +181,7 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
if _, ok := gw.Channels[getChannelID(*msg)]; !ok { if _, ok := gw.Channels[getChannelID(*msg)]; !ok {
continue continue
} }
if channel.Direction == "out" && channel.Account == dest.Account && gw.validGatewayDest(*msg, channel) { if channel.Direction == "out" && channel.Account == dest.Account && gw.validGatewayDest(msg, channel) {
channels = append(channels, *channel) channels = append(channels, *channel)
} }
} }
@ -192,9 +199,10 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
return return
} }
originchannel := msg.Channel originchannel := msg.Channel
origmsg := msg
for _, channel := range gw.DestChannelFunc(&msg, *dest) { for _, channel := range gw.DestChannelFunc(&msg, *dest) {
// do not send to ourself // do not send to ourself
if channel.ID == getChannelID(msg) { if channel.ID == getChannelID(origmsg) {
continue continue
} }
log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name) log.Debugf("Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
@ -223,6 +231,20 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
return true return true
} }
} }
// TODO do not compile regexps everytime
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreMessages) {
if entry != "" {
re, err := regexp.Compile(entry)
if err != nil {
log.Errorf("incorrect regexp %s for %s", entry, msg.Account)
continue
}
if re.MatchString(msg.Text) {
log.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
return true
}
}
}
return false return false
} }
@ -233,6 +255,18 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) {
if nick == "" { if nick == "" {
nick = dest.Config.RemoteNickFormat nick = dest.Config.RemoteNickFormat
} }
if len(msg.Username) > 0 {
// fix utf-8 issue #193
i := 0
for index := range msg.Username {
if i == 1 {
i = index
break
}
i++
}
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
}
nick = strings.Replace(nick, "{NICK}", msg.Username, -1) nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1) nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
@ -254,13 +288,21 @@ func getChannelID(msg config.Message) string {
return msg.Channel + msg.Account return msg.Channel + msg.Account
} }
func (gw *Gateway) validGatewayDest(msg config.Message, channel *config.ChannelInfo) bool { func (gw *Gateway) validGatewayDest(msg *config.Message, channel *config.ChannelInfo) bool {
GIDmap := gw.Channels[getChannelID(msg)].GID GIDmap := gw.Channels[getChannelID(*msg)].GID
// gateway is specified in message (probably from api)
if msg.Gateway != "" {
return channel.GID[msg.Gateway]
}
// check if we are running a samechannelgateway. // check if we are running a samechannelgateway.
// if it is and the channel name matches it's ok, otherwise we shouldn't use this channel. // if it is and the channel name matches it's ok, otherwise we shouldn't use this channel.
for k, _ := range GIDmap { for k, _ := range GIDmap {
if channel.SameChannel[k] == true { if channel.SameChannel[k] == true {
if msg.Channel == channel.Name { if msg.Channel == channel.Name {
// add the gateway to our message
msg.Gateway = k
return true return true
} else { } else {
return false return false
@ -270,8 +312,17 @@ func (gw *Gateway) validGatewayDest(msg config.Message, channel *config.ChannelI
// check if we are in the correct gateway // check if we are in the correct gateway
for k, _ := range GIDmap { for k, _ := range GIDmap {
if channel.GID[k] == true { if channel.GID[k] == true {
// add the gateway to our message
msg.Gateway = k
return true return true
} }
} }
return false return false
} }
func isApi(account string) bool {
if strings.HasPrefix(account, "api.") {
return true
}
return false
}

View File

@ -12,7 +12,7 @@ import (
) )
var ( var (
version = "0.11.0-dev" version = "0.15.0"
githash string githash string
) )
@ -34,7 +34,6 @@ func main() {
fmt.Printf("version: %s %s\n", version, githash) fmt.Printf("version: %s %s\n", version, githash)
return return
} }
flag.Parse()
if *flagDebug { if *flagDebug {
log.Info("Enabling debug") log.Info("Enabling debug")
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)

View File

@ -1,4 +1,5 @@
#This is configuration for matterbridge. #This is configuration for matterbridge.
#WARNING: as this file contains credentials, be sure to set correct file permissions
################################################################### ###################################################################
#IRC section #IRC section
################################################################### ###################################################################
@ -48,19 +49,31 @@ MessageDelay=1300
#Maximum amount of messages to hold in queue. If queue is full #Maximum amount of messages to hold in queue. If queue is full
#messages will be dropped. #messages will be dropped.
#<clipped> will be add to the message that fills the queue. #<message clipped> will be add to the message that fills the queue.
#OPTIONAL (default 30) #OPTIONAL (default 30)
MessageQueue=30 MessageQueue=30
#Maximum length of message sent to irc server. If it exceeds
#<message clipped> will be add to the message.
#OPTIONAL (default 400)
MessageLength=400
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
@ -108,6 +121,12 @@ SkipTLSVerify=true
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -152,6 +171,12 @@ Nick="yourlogin"
#OPTIONAL #OPTIONAL
IgnoreNicks="spammer1 spammer2" IgnoreNicks="spammer1 spammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -200,9 +225,9 @@ IconURL="http://youricon.png"
#OPTIONAL #OPTIONAL
useAPI=false useAPI=false
#The mattermost hostname. #The mattermost hostname. (do not prefix it with http or https)
#REQUIRED (when useAPI=true) #REQUIRED (when useAPI=true)
Server="yourmattermostserver.domain" Server="yourmattermostserver.domain"
#Your team on mattermost. #Your team on mattermost.
#REQUIRED (when useAPI=true) #REQUIRED (when useAPI=true)
@ -241,11 +266,25 @@ NicksPerRow=4
#OPTIONAL (default false) #OPTIONAL (default false)
PrefixMessagesWithNick=false PrefixMessagesWithNick=false
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false
#Message to be appended to every edited message
#OPTIONAL (default empty)
EditSuffix=" (edited)"
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -279,6 +318,12 @@ Token="Yourtokenhere"
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -347,6 +392,14 @@ NickFormatter="plain"
#OPTIONAL (default 4) #OPTIONAL (default 4)
NicksPerRow=4 NicksPerRow=4
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=true
#Message to be appended to every edited message
#OPTIONAL (default empty)
EditSuffix=" (edited)"
#Whether to prefix messages from other bridges to mattermost with RemoteNickFormat #Whether to prefix messages from other bridges to mattermost with RemoteNickFormat
#Useful if username overrides for incoming webhooks isn't enabled on the #Useful if username overrides for incoming webhooks isn't enabled on the
#slack server. If you set PrefixMessagesWithNick to true, each message #slack server. If you set PrefixMessagesWithNick to true, each message
@ -360,6 +413,12 @@ PrefixMessagesWithNick=false
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -391,11 +450,25 @@ Token="Yourtokenhere"
#REQUIRED #REQUIRED
Server="yourservername" Server="yourservername"
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false
#Message to be appended to every edited message
#OPTIONAL (default empty)
EditSuffix=" (edited)"
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -427,11 +500,31 @@ Token="Yourtokenhere"
#See https://core.telegram.org/bots/api#html-style #See https://core.telegram.org/bots/api#html-style
MessageFormat="" MessageFormat=""
#If enabled use the "First Name" as username. If this is empty use the Username
#If disabled use the "Username" as username. If this is empty use the First Name
#If all names are empty, username will be "unknown"
#OPTIONAL (default false)
UseFirstName=false
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false
#Message to be appended to every edited message
#OPTIONAL (default empty)
EditSuffix=" (edited)"
#Nicks you want to ignore. #Nicks you want to ignore.
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
#OPTIONAL #OPTIONAL
IgnoreNicks="spammer1 spammer2" IgnoreNicks="spammer1 spammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -490,6 +583,12 @@ PrefixMessagesWithNick=false
#OPTIONAL #OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2" IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -522,6 +621,11 @@ Server="https://matrix.org"
Login="yourlogin" Login="yourlogin"
Password="yourpass" Password="yourpass"
#Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org
#to other bridges, or only send "username".(true only sends username)
#OPTIONAL (default false)
NoHomeServerSuffix=false
#Whether to prefix messages from other bridges to matrix with the sender's nick. #Whether to prefix messages from other bridges to matrix with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the #Useful if username overrides for incoming webhooks isn't enabled on the
#matrix server. If you set PrefixMessagesWithNick to true, each message #matrix server. If you set PrefixMessagesWithNick to true, each message
@ -534,6 +638,12 @@ PrefixMessagesWithNick=false
#OPTIONAL #OPTIONAL
IgnoreNicks="spammer1 spammer2" IgnoreNicks="spammer1 spammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -563,6 +673,11 @@ BindAddress="127.0.0.1:4242"
#Amount of messages to keep in memory #Amount of messages to keep in memory
Buffer=1000 Buffer=1000
#Bearer token used for authentication
#curl -H "Authorization: Bearer token" http://localhost:4242/api/messages
#OPTIONAL (no authorization if token is empty)
Token="mytoken"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
@ -570,6 +685,8 @@ Buffer=1000
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="{NICK}" RemoteNickFormat="{NICK}"
################################################################### ###################################################################
#General configuration #General configuration
################################################################### ###################################################################
@ -613,7 +730,7 @@ enable=true
#channel to connect on that account #channel to connect on that account
#How to specify them for the different bridges: #How to specify them for the different bridges:
# #
#irc - #channel (# is required) #irc - #channel (# is required) (this needs to be lowercase!)
#mattermost - channel (the channel name as seen in the URL, not the displayname) #mattermost - channel (the channel name as seen in the URL, not the displayname)
#gitter - username/room #gitter - username/room
#xmpp - channel #xmpp - channel
@ -624,8 +741,9 @@ enable=true
#telegram - chatid (a large negative number, eg -123456789) #telegram - chatid (a large negative number, eg -123456789)
# see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau) # see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau)
#hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel) #hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
#rocketchat - #channel (# is required) #rocketchat - #channel (# is required (also needed for private channels!)
#matrix - #channel:server (eg #yourchannel:matrix.org) #matrix - #channel:server (eg #yourchannel:matrix.org)
# - encrypted rooms are not supported in matrix
#REQUIRED #REQUIRED
channel="#testing" channel="#testing"
@ -661,7 +779,9 @@ enable=true
#account="api.local" #account="api.local"
#channel="api" #channel="api"
#To send data to the api: #To send data to the api:
#curl -XPOST -H 'Content-Type: application/json' -d '{"text":"test","username":"randomuser"}' http://localhost:4242/api/message #curl -XPOST -H 'Content-Type: application/json' -d '{"text":"test","username":"randomuser","gateway":"gateway1"}' http://localhost:4242/api/message
#To read from the api:
#curl http://localhost:4242/api/messages
#If you want to do a 1:1 mapping between protocols where the channelnames are the same #If you want to do a 1:1 mapping between protocols where the channelnames are the same
#e.g. slack and mattermost you can use the samechannelgateway configuration #e.g. slack and mattermost you can use the samechannelgateway configuration

View File

@ -1,3 +1,4 @@
#WARNING: as this file contains credentials, be sure to set correct file permissions
[irc] [irc]
[irc.freenode] [irc.freenode]
Server="irc.freenode.net:6667" Server="irc.freenode.net:6667"
@ -6,7 +7,8 @@
[mattermost] [mattermost]
[mattermost.work] [mattermost.work]
useAPI=true useAPI=true
Server="yourmattermostserver.domain" #do not prefix it wit http:// or https://
Server="yourmattermostserver.domain"
Team="yourteam" Team="yourteam"
Login="yourlogin" Login="yourlogin"
Password="yourpass" Password="yourpass"
@ -15,22 +17,14 @@
[[gateway]] [[gateway]]
name="gateway1" name="gateway1"
enable=true enable=true
[[gateway.in]] [[gateway.inout]]
account="irc.freenode" account="irc.freenode"
channel="#testing" channel="#testing"
[[gateway.out]] [[gateway.inout]]
account="irc.freenode"
channel="#testing"
[[gateway.in]]
account="mattermost.work" account="mattermost.work"
channel="off-topic" channel="off-topic"
[[gateway.out]]
account="mattermost.work"
channel="off-topic"
#simpler config possible since v0.10.2 #simpler config possible since v0.10.2
#[[gateway]] #[[gateway]]
#name="gateway2" #name="gateway2"

View File

@ -4,9 +4,11 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -35,6 +37,7 @@ type Message struct {
Username string Username string
Text string Text string
Type string Type string
UserID string
} }
type Team struct { type Team struct {
@ -48,19 +51,20 @@ type Team struct {
type MMClient struct { type MMClient struct {
sync.RWMutex sync.RWMutex
*Credentials *Credentials
Team *Team Team *Team
OtherTeams []*Team OtherTeams []*Team
Client *model.Client Client *model.Client
User *model.User User *model.User
Users map[string]*model.User Users map[string]*model.User
MessageChan chan *Message MessageChan chan *Message
log *log.Entry log *log.Entry
WsClient *websocket.Conn WsClient *websocket.Conn
WsQuit bool WsQuit bool
WsAway bool WsAway bool
WsConnected bool WsConnected bool
WsSequence int64 WsSequence int64
WsPingChan chan *model.WebSocketResponse WsPingChan chan *model.WebSocketResponse
ServerVersion string
} }
func New(login, pass, team, server string) *MMClient { func New(login, pass, team, server string) *MMClient {
@ -103,8 +107,27 @@ func (m *MMClient) Login() error {
} }
// login to mattermost // login to mattermost
m.Client = model.NewClient(uriScheme + m.Credentials.Server) m.Client = model.NewClient(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}} m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, Proxy: http.ProxyFromEnvironment}
m.Client.HttpClient.Timeout = time.Second * 10 m.Client.HttpClient.Timeout = time.Second * 10
for {
d := b.Duration()
// bogus call to get the serverversion
m.Client.GetClientProperties()
if firstConnection && !supportedVersion(m.Client.ServerVersion) {
return fmt.Errorf("unsupported mattermost version: %s", m.Client.ServerVersion)
}
m.ServerVersion = m.Client.ServerVersion
if m.ServerVersion == "" {
m.log.Debugf("Server not up yet, reconnecting in %s", d)
time.Sleep(d)
} else {
m.log.Infof("Found version %s", m.ServerVersion)
break
}
}
b.Reset()
var myinfo *model.Result var myinfo *model.Result
var appErr *model.AppError var appErr *model.AppError
var logmsg = "trying login" var logmsg = "trying login"
@ -240,7 +263,7 @@ func (m *MMClient) WsReceiver() {
func (m *MMClient) parseMessage(rmsg *Message) { func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Event { switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED: case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED:
m.parseActionPost(rmsg) m.parseActionPost(rmsg)
/* /*
case model.ACTION_USER_REMOVED: case model.ACTION_USER_REMOVED:
@ -266,10 +289,20 @@ func (m *MMClient) parseActionPost(rmsg *Message) {
if m.GetUser(data.UserId) == nil { if m.GetUser(data.UserId) == nil {
m.UpdateUsers() m.UpdateUsers()
} }
rmsg.Username = m.GetUser(data.UserId).Username rmsg.Username = m.GetUserName(data.UserId)
rmsg.Channel = m.GetChannelName(data.ChannelId) rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.UserID = data.UserId
rmsg.Type = data.Type rmsg.Type = data.Type
rmsg.Team = m.GetTeamName(rmsg.Raw.Data["team_id"].(string)) teamid, _ := rmsg.Raw.Data["team_id"].(string)
// edit messsages have no team_id for some reason
if teamid == "" {
// we can find the team_id from the channelid
teamid = m.GetChannelTeamId(data.ChannelId)
rmsg.Raw.Data["team_id"] = teamid
}
if teamid != "" {
rmsg.Team = m.GetTeamName(teamid)
}
// direct message // direct message
if rmsg.Raw.Data["channel_type"] == "D" { if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username rmsg.Channel = m.GetUser(data.UserId).Username
@ -295,7 +328,12 @@ func (m *MMClient) UpdateChannels() error {
if err != nil { if err != nil {
return errors.New(err.DetailedError) return errors.New(err.DetailedError)
} }
mmchannels2, err := m.Client.GetMoreChannels("") var mmchannels2 *model.Result
if m.mmVersion() >= 3.08 {
mmchannels2, err = m.Client.GetMoreChannelsPage(0, 5000)
} else {
mmchannels2, err = m.Client.GetMoreChannels("")
}
if err != nil { if err != nil {
return errors.New(err.DetailedError) return errors.New(err.DetailedError)
} }
@ -337,6 +375,19 @@ func (m *MMClient) GetChannelId(name string, teamId string) string {
return "" return ""
} }
func (m *MMClient) GetChannelTeamId(id string) string {
m.RLock()
defer m.RUnlock()
for _, t := range append(m.OtherTeams, m.Team) {
for _, channel := range append(*t.Channels, *t.MoreChannels...) {
if channel.Id == id {
return channel.TeamId
}
}
}
return ""
}
func (m *MMClient) GetChannelHeader(channelId string) string { func (m *MMClient) GetChannelHeader(channelId string) string {
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
@ -430,6 +481,14 @@ func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
func (m *MMClient) UpdateLastViewed(channelId string) { func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId) m.log.Debugf("posting lastview %#v", channelId)
if m.mmVersion() >= 3.08 {
view := model.ChannelView{ChannelId: channelId}
res, _ := m.Client.ViewChannel(view)
if res == false {
m.log.Errorf("ChannelView update for %s failed", channelId)
}
return
}
_, err := m.Client.UpdateLastViewedAt(channelId, true) _, err := m.Client.UpdateLastViewedAt(channelId, true)
if err != nil { if err != nil {
m.log.Error(err) m.log.Error(err)
@ -576,6 +635,14 @@ func (m *MMClient) GetUser(userId string) *model.User {
return m.Users[userId] return m.Users[userId]
} }
func (m *MMClient) GetUserName(userId string) string {
user := m.GetUser(userId)
if user != nil {
return user.Username
}
return ""
}
func (m *MMClient) GetStatus(userId string) string { func (m *MMClient) GetStatus(userId string) string {
res, err := m.Client.GetStatuses() res, err := m.Client.GetStatuses()
if err != nil { if err != nil {
@ -663,7 +730,11 @@ func (m *MMClient) initUser() error {
return errors.New(err.DetailedError) return errors.New(err.DetailedError)
} }
t.Channels = mmchannels.Data.(*model.ChannelList) t.Channels = mmchannels.Data.(*model.ChannelList)
mmchannels, err = m.Client.GetMoreChannels("") if m.mmVersion() >= 3.08 {
mmchannels, err = m.Client.GetMoreChannelsPage(0, 5000)
} else {
mmchannels, err = m.Client.GetMoreChannels("")
}
if err != nil { if err != nil {
return errors.New(err.DetailedError) return errors.New(err.DetailedError)
} }
@ -691,3 +762,23 @@ func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) err
m.WsClient.WriteJSON(req) m.WsClient.WriteJSON(req)
return nil return nil
} }
func (m *MMClient) mmVersion() float64 {
v, _ := strconv.ParseFloat(string(m.ServerVersion[0:2])+"0"+string(m.ServerVersion[2]), 64)
if string(m.ServerVersion[4]) == "." {
v, _ = strconv.ParseFloat(m.ServerVersion[0:4], 64)
}
return v
}
func supportedVersion(version string) bool {
if strings.HasPrefix(version, "3.5.0") ||
strings.HasPrefix(version, "3.6.0") ||
strings.HasPrefix(version, "3.7.0") ||
strings.HasPrefix(version, "3.8.0") ||
strings.HasPrefix(version, "3.9.0") ||
strings.HasPrefix(version, "3.10.0") {
return true
}
return false
}

View File

@ -30,16 +30,16 @@ type (
// Bind implements the `Binder#Bind` function. // Bind implements the `Binder#Bind` function.
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) { func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
req := c.Request() req := c.Request()
if req.Method == GET {
if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error())
}
return
}
ctype := req.Header.Get(HeaderContentType)
if req.ContentLength == 0 { if req.ContentLength == 0 {
if req.Method == GET || req.Method == DELETE {
if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error())
}
return
}
return NewHTTPError(http.StatusBadRequest, "Request body can't be empty") return NewHTTPError(http.StatusBadRequest, "Request body can't be empty")
} }
ctype := req.Header.Get(HeaderContentType)
switch { switch {
case strings.HasPrefix(ctype, MIMEApplicationJSON): case strings.HasPrefix(ctype, MIMEApplicationJSON):
if err = json.NewDecoder(req.Body).Decode(i); err != nil { if err = json.NewDecoder(req.Body).Decode(i); err != nil {
@ -51,7 +51,7 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
return NewHTTPError(http.StatusBadRequest, err.Error()) return NewHTTPError(http.StatusBadRequest, err.Error())
} }
} }
case strings.HasPrefix(ctype, MIMEApplicationXML): case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
if err = xml.NewDecoder(req.Body).Decode(i); err != nil { if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
if ute, ok := err.(*xml.UnsupportedTypeError); ok { if ute, ok := err.(*xml.UnsupportedTypeError); ok {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())) return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error()))
@ -142,6 +142,8 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V
} }
switch valueKind { switch valueKind {
case reflect.Ptr:
return setWithProperType(structField.Elem().Kind(), val, structField.Elem())
case reflect.Int: case reflect.Int:
return setIntField(val, 0, structField) return setIntField(val, 0, structField)
case reflect.Int8: case reflect.Int8:

View File

@ -31,6 +31,9 @@ type (
// IsTLS returns true if HTTP connection is TLS otherwise false. // IsTLS returns true if HTTP connection is TLS otherwise false.
IsTLS() bool IsTLS() bool
// IsWebSocket returns true if HTTP connection is WebSocket otherwise false.
IsWebSocket() bool
// Scheme returns the HTTP protocol scheme, `http` or `https`. // Scheme returns the HTTP protocol scheme, `http` or `https`.
Scheme() string Scheme() string
@ -219,19 +222,36 @@ func (c *context) IsTLS() bool {
return c.request.TLS != nil return c.request.TLS != nil
} }
func (c *context) IsWebSocket() bool {
upgrade := c.request.Header.Get(HeaderUpgrade)
return upgrade == "websocket" || upgrade == "Websocket"
}
func (c *context) Scheme() string { func (c *context) Scheme() string {
// Can't use `r.Request.URL.Scheme` // Can't use `r.Request.URL.Scheme`
// See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0 // See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0
if c.IsTLS() { if c.IsTLS() {
return "https" return "https"
} }
if scheme := c.request.Header.Get(HeaderXForwardedProto); scheme != "" {
return scheme
}
if scheme := c.request.Header.Get(HeaderXForwardedProtocol); scheme != "" {
return scheme
}
if ssl := c.request.Header.Get(HeaderXForwardedSsl); ssl == "on" {
return "https"
}
if scheme := c.request.Header.Get(HeaderXUrlScheme); scheme != "" {
return scheme
}
return "http" return "http"
} }
func (c *context) RealIP() string { func (c *context) RealIP() string {
ra := c.request.RemoteAddr ra := c.request.RemoteAddr
if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" { if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" {
ra = ip ra = strings.Split(ip, ", ")[0]
} else if ip := c.request.Header.Get(HeaderXRealIP); ip != "" { } else if ip := c.request.Header.Get(HeaderXRealIP); ip != "" {
ra = ip ra = ip
} else { } else {
@ -275,7 +295,7 @@ func (c *context) SetParamNames(names ...string) {
} }
func (c *context) ParamValues() []string { func (c *context) ParamValues() []string {
return c.pvalues return c.pvalues[:len(c.pnames)]
} }
func (c *context) SetParamValues(values ...string) { func (c *context) SetParamValues(values ...string) {
@ -385,7 +405,8 @@ func (c *context) String(code int, s string) (err error) {
} }
func (c *context) JSON(code int, i interface{}) (err error) { func (c *context) JSON(code int, i interface{}) (err error) {
if c.echo.Debug { _, pretty := c.QueryParams()["pretty"]
if c.echo.Debug || pretty {
return c.JSONPretty(code, i, " ") return c.JSONPretty(code, i, " ")
} }
b, err := json.Marshal(i) b, err := json.Marshal(i)
@ -429,7 +450,8 @@ func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) {
} }
func (c *context) XML(code int, i interface{}) (err error) { func (c *context) XML(code int, i interface{}) (err error) {
if c.echo.Debug { _, pretty := c.QueryParams()["pretty"]
if c.echo.Debug || pretty {
return c.XMLPretty(code, i, " ") return c.XMLPretty(code, i, " ")
} }
b, err := xml.Marshal(i) b, err := xml.Marshal(i)
@ -471,7 +493,12 @@ func (c *context) Stream(code int, contentType string, r io.Reader) (err error)
return return
} }
func (c *context) File(file string) error { func (c *context) File(file string) (err error) {
file, err = url.QueryUnescape(file) // Issue #839
if err != nil {
return
}
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return ErrNotFound return ErrNotFound
@ -487,11 +514,11 @@ func (c *context) File(file string) error {
} }
defer f.Close() defer f.Close()
if fi, err = f.Stat(); err != nil { if fi, err = f.Stat(); err != nil {
return err return
} }
} }
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f) http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f)
return nil return
} }
func (c *context) Attachment(file, name string) (err error) { func (c *context) Attachment(file, name string) (err error) {
@ -514,7 +541,7 @@ func (c *context) NoContent(code int) error {
} }
func (c *context) Redirect(code int, url string) error { func (c *context) Redirect(code int, url string) error {
if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect { if code < 300 || code > 308 {
return ErrInvalidRedirectCode return ErrInvalidRedirectCode
} }
c.response.Header().Set(HeaderLocation, url) c.response.Header().Set(HeaderLocation, url)
@ -548,4 +575,8 @@ func (c *context) Reset(r *http.Request, w http.ResponseWriter) {
c.query = nil c.query = nil
c.handler = NotFoundHandler c.handler = NotFoundHandler
c.store = nil c.store = nil
c.path = ""
c.pnames = nil
// NOTE: Don't reset because it has to have length c.echo.maxParam at all times
// c.pvalues = nil
} }

View File

@ -1,26 +0,0 @@
package main
import (
"net/http"
"golang.org/x/crypto/acme/autocert"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
e := echo.New()
// e.AutoTLSManager.HostPolicy = autocert.HostWhitelist("<DOMAIN>")
// Cache certificates
e.AutoTLSManager.Cache = autocert.DirCache("/var/www/.cache")
e.Use(middleware.Recover())
e.Use(middleware.Logger())
e.GET("/", func(c echo.Context) error {
return c.HTML(http.StatusOK, `
<h1>Welcome to Echo!</h1>
<h3>TLS certificates automatically installed from Let's Encrypt :)</h3>
`)
})
e.Logger.Fatal(e.StartAutoTLS(":443"))
}

View File

@ -1,38 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
var (
users = []string{"Joe", "Veer", "Zion"}
)
func getUsers(c echo.Context) error {
return c.JSON(http.StatusOK, users)
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// CORS default
// Allows requests from any origin wth GET, HEAD, PUT, POST or DELETE method.
// e.Use(middleware.CORS())
// CORS restricted
// Allows requests from any `https://labstack.com` or `https://labstack.net` origin
// wth GET, PUT, POST or DELETE method.
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://labstack.com", "https://labstack.net"},
AllowMethods: []string{echo.GET, echo.PUT, echo.POST, echo.DELETE},
}))
e.GET("/api/users", getUsers)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,75 +0,0 @@
package main
import (
"net/http"
"strconv"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
type (
user struct {
ID int `json:"id"`
Name string `json:"name"`
}
)
var (
users = map[int]*user{}
seq = 1
)
//----------
// Handlers
//----------
func createUser(c echo.Context) error {
u := &user{
ID: seq,
}
if err := c.Bind(u); err != nil {
return err
}
users[u.ID] = u
seq++
return c.JSON(http.StatusCreated, u)
}
func getUser(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
return c.JSON(http.StatusOK, users[id])
}
func updateUser(c echo.Context) error {
u := new(user)
if err := c.Bind(u); err != nil {
return err
}
id, _ := strconv.Atoi(c.Param("id"))
users[id].Name = u.Name
return c.JSON(http.StatusOK, users[id])
}
func deleteUser(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
delete(users, id)
return c.NoContent(http.StatusNoContent)
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.POST("/users", createUser)
e.GET("/users/:id", getUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,21 +0,0 @@
package main
import (
"net/http"
rice "github.com/GeertJohan/go.rice"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
// the file server for rice. "app" is the folder where the files come from.
assetHandler := http.FileServer(rice.MustFindBox("app").HTTPBox())
// serves the index.html from rice
e.GET("/", echo.WrapHandler(assetHandler))
// servers other static files
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,65 +0,0 @@
package main
import (
"fmt"
"io"
"os"
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func upload(c echo.Context) error {
// Read form fields
name := c.FormValue("name")
email := c.FormValue("email")
//------------
// Read files
//------------
// Multipart form
form, err := c.MultipartForm()
if err != nil {
return err
}
files := form.File["files"]
for _, file := range files {
// Source
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// Destination
dst, err := os.Create(file.Filename)
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err
}
}
return c.HTML(http.StatusOK, fmt.Sprintf("<p>Uploaded successfully %d files with fields name=%s and email=%s.</p>", len(files), name, email))
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "public")
e.POST("/upload", upload)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,59 +0,0 @@
package main
import (
"fmt"
"io"
"os"
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func upload(c echo.Context) error {
// Read form fields
name := c.FormValue("name")
email := c.FormValue("email")
//-----------
// Read file
//-----------
// Source
file, err := c.FormFile("file")
if err != nil {
return err
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// Destination
dst, err := os.Create(file.Filename)
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err
}
return c.HTML(http.StatusOK, fmt.Sprintf("<p>File %s uploaded successfully with fields name=%s and email=%s.</p>", file.Filename, name, email))
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "public")
e.POST("/upload", upload)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,17 +0,0 @@
// +build appengine
package main
import (
"net/http"
"github.com/labstack/echo"
)
func createMux() *echo.Echo {
e := echo.New()
// note: we don't need to provide the middleware or static handlers, that's taken care of by the platform
// app engine has it's own "main" wrapper - we just need to hook echo into the default handler
http.Handle("/", e)
return e
}

View File

@ -1,25 +0,0 @@
// +build appenginevm
package main
import (
"net/http"
"github.com/labstack/echo"
"google.golang.org/appengine"
)
func createMux() *echo.Echo {
e := echo.New()
// note: we don't need to provide the middleware or static handlers
// for the appengine vm version - that's taken care of by the platform
return e
}
func main() {
// the appengine package provides a convenient method to handle the health-check requests
// and also run the app on the correct port. We just need to add Echo to the default handler
e := echo.New(":8080")
http.Handle("/", e)
appengine.Main()
}

View File

@ -1,24 +0,0 @@
// +build !appengine,!appenginevm
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func createMux() *echo.Echo {
e := echo.New()
e.Use(middleware.Recover())
e.Use(middleware.Logger())
e.Use(middleware.Gzip())
e.Static("/", "public")
return e
}
func main() {
e.Logger.Fatal(e.Start(":8080"))
}

View File

@ -1,4 +0,0 @@
package main
// reference our echo instance and create it early
var e = createMux()

View File

@ -1,54 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
type (
user struct {
ID string `json:"id"`
Name string `json:"name"`
}
)
var (
users map[string]user
)
func init() {
users = map[string]user{
"1": user{
ID: "1",
Name: "Wreck-It Ralph",
},
}
// hook into the echo instance to create an endpoint group
// and add specific middleware to it plus handlers
g := e.Group("/users")
g.Use(middleware.CORS())
g.POST("", createUser)
g.GET("", getUsers)
g.GET("/:id", getUser)
}
func createUser(c echo.Context) error {
u := new(user)
if err := c.Bind(u); err != nil {
return err
}
users[u.ID] = *u
return c.JSON(http.StatusCreated, u)
}
func getUsers(c echo.Context) error {
return c.JSON(http.StatusOK, users)
}
func getUser(c echo.Context) error {
return c.JSON(http.StatusOK, users[c.Param("id")])
}

View File

@ -1,31 +0,0 @@
package main
import (
"html/template"
"io"
"net/http"
"github.com/labstack/echo"
)
type (
Template struct {
templates *template.Template
}
)
func init() {
t := &Template{
templates: template.Must(template.ParseFiles("templates/welcome.html")),
}
e.Renderer = t
e.GET("/welcome", welcome)
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func welcome(c echo.Context) error {
return c.Render(http.StatusOK, "welcome", "Joe")
}

View File

@ -1,20 +0,0 @@
package main
import (
"net/http"
"github.com/facebookgo/grace/gracehttp"
"github.com/labstack/echo"
)
func main() {
// Setup
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Six sick bricks tick")
})
e.Server.Addr = ":1323"
// Serve it like a boss
e.Logger.Fatal(gracehttp.Serve(e.Server))
}

View File

@ -1,21 +0,0 @@
package main
import (
"net/http"
"time"
"github.com/labstack/echo"
"github.com/tylerb/graceful"
)
func main() {
// Setup
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Sue sews rose on slow joe crows nose")
})
e.Server.Addr = ":1323"
// Serve it like a boss
graceful.ListenAndServe(e.Server, 5*time.Second)
}

View File

@ -1,25 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
// Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Route => handler
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!\n")
})
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,42 +0,0 @@
package main
import (
"fmt"
"net/http"
"time"
"github.com/labstack/echo"
)
func request(c echo.Context) error {
req := c.Request()
format := "<pre><strong>Request Information</strong>\n\n<code>Protocol: %s\nHost: %s\nRemote Address: %s\nMethod: %s\nPath: %s\n</code></pre>"
return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path))
}
func stream(c echo.Context) error {
res := c.Response()
gone := res.CloseNotify()
res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
res.WriteHeader(http.StatusOK)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
fmt.Fprint(res, "<pre><strong>Clock Stream</strong>\n\n<code>")
for {
fmt.Fprintf(res, "%v\n", time.Now())
res.Flush()
select {
case <-ticker.C:
case <-gone:
break
}
}
}
func main() {
e := echo.New()
e.GET("/request", request)
e.GET("/stream", stream)
e.Logger.Fatal(e.StartTLS(":1323", "cert.pem", "key.pem"))
}

View File

@ -1,35 +0,0 @@
package main
import (
"math/rand"
"net/http"
"time"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "public")
// JSONP
e.GET("/jsonp", func(c echo.Context) error {
callback := c.QueryParam("callback")
var content struct {
Response string `json:"response"`
Timestamp time.Time `json:"timestamp"`
Random int `json:"random"`
}
content.Response = "Sent via JSONP"
content.Timestamp = time.Now().UTC()
content.Random = rand.Intn(1000)
return c.JSONP(http.StatusOK, callback, &content)
})
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,86 +0,0 @@
package main
import (
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
// jwtCustomClaims are custom claims extending default ones.
type jwtCustomClaims struct {
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.StandardClaims
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
if username == "jon" && password == "shhh!" {
// Set custom claims
claims := &jwtCustomClaims{
"Jon Snow",
true,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 72).Unix(),
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}
return echo.ErrUnauthorized
}
func accessible(c echo.Context) error {
return c.String(http.StatusOK, "Accessible")
}
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
name := claims.Name
return c.String(http.StatusOK, "Welcome "+name+"!")
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Login route
e.POST("/login", login)
// Unauthenticated route
e.GET("/", accessible)
// Restricted group
r := e.Group("/restricted")
// Configure middleware with the custom claims type
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte("secret"),
}
r.Use(middleware.JWTWithConfig(config))
r.GET("", restricted)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,69 +0,0 @@
package main
import (
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
if username == "jon" && password == "shhh!" {
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["name"] = "Jon Snow"
claims["admin"] = true
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
return echo.ErrUnauthorized
}
func accessible(c echo.Context) error {
return c.String(http.StatusOK, "Accessible")
}
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.String(http.StatusOK, "Welcome "+name+"!")
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Login route
e.POST("/login", login)
// Unauthenticated route
e.GET("/", accessible)
// Restricted group
r := e.Group("/restricted")
r.Use(middleware.JWT([]byte("secret")))
r.GET("", restricted)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,82 +0,0 @@
package main
import (
"net/http"
"strconv"
"sync"
"time"
"github.com/labstack/echo"
)
type (
Stats struct {
Uptime time.Time `json:"uptime"`
RequestCount uint64 `json:"requestCount"`
Statuses map[string]int `json:"statuses"`
mutex sync.RWMutex
}
)
func NewStats() *Stats {
return &Stats{
Uptime: time.Now(),
Statuses: make(map[string]int),
}
}
// Process is the middleware function.
func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if err := next(c); err != nil {
c.Error(err)
}
s.mutex.Lock()
defer s.mutex.Unlock()
s.RequestCount++
status := strconv.Itoa(c.Response().Status)
s.Statuses[status]++
return nil
}
}
// Handle is the endpoint to get stats.
func (s *Stats) Handle(c echo.Context) error {
s.mutex.RLock()
defer s.mutex.RUnlock()
return c.JSON(http.StatusOK, s)
}
// ServerHeader middleware adds a `Server` header to the response.
func ServerHeader(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderServer, "Echo/3.0")
return next(c)
}
}
func main() {
e := echo.New()
// Debug mode
e.Debug = true
//-------------------
// Custom middleware
//-------------------
// Stats
s := NewStats()
e.Use(s.Process)
e.GET("/stats", s.Handle) // Endpoint to get stats
// Server header
e.Use(ServerHeader)
// Handler
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,45 +0,0 @@
package main
import (
"net/http"
"time"
"encoding/json"
"github.com/labstack/echo"
)
type (
Geolocation struct {
Altitude float64
Latitude float64
Longitude float64
}
)
var (
locations = []Geolocation{
{-97, 37.819929, -122.478255},
{1899, 39.096849, -120.032351},
{2619, 37.865101, -119.538329},
{42, 33.812092, -117.918974},
{15, 37.77493, -122.419416},
}
)
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
for _, l := range locations {
if err := json.NewEncoder(c.Response()).Encode(l); err != nil {
return err
}
c.Response().Flush()
time.Sleep(1 * time.Second)
}
return nil
})
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,78 +0,0 @@
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
type (
Host struct {
Echo *echo.Echo
}
)
func main() {
// Hosts
hosts := make(map[string]*Host)
//-----
// API
//-----
api := echo.New()
api.Use(middleware.Logger())
api.Use(middleware.Recover())
hosts["api.localhost:1323"] = &Host{api}
api.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "API")
})
//------
// Blog
//------
blog := echo.New()
blog.Use(middleware.Logger())
blog.Use(middleware.Recover())
hosts["blog.localhost:1323"] = &Host{blog}
blog.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Blog")
})
//---------
// Website
//---------
site := echo.New()
site.Use(middleware.Logger())
site.Use(middleware.Recover())
hosts["localhost:1323"] = &Host{site}
site.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Website")
})
// Server
e := echo.New()
e.Any("/*", func(c echo.Context) (err error) {
req := c.Request()
res := c.Response()
host := hosts[req.Host]
if host == nil {
err = echo.ErrNotFound
} else {
host.Echo.ServeHTTP(res, req)
}
return
})
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,14 +0,0 @@
package handler
import mgo "gopkg.in/mgo.v2"
type (
Handler struct {
DB *mgo.Session
}
)
const (
// Key (Should come from somewhere else).
Key = "secret"
)

View File

@ -1,73 +0,0 @@
package handler
import (
"net/http"
"strconv"
"github.com/labstack/echo"
"github.com/labstack/echo/cookbook/twitter/model"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func (h *Handler) CreatePost(c echo.Context) (err error) {
u := &model.User{
ID: bson.ObjectIdHex(userIDFromToken(c)),
}
p := &model.Post{
ID: bson.NewObjectId(),
From: u.ID.Hex(),
}
if err = c.Bind(p); err != nil {
return
}
// Validation
if p.To == "" || p.Message == "" {
return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid to or message fields"}
}
// Find user from database
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").FindId(u.ID).One(u); err != nil {
if err == mgo.ErrNotFound {
return echo.ErrNotFound
}
return
}
// Save post in database
if err = db.DB("twitter").C("posts").Insert(p); err != nil {
return
}
return c.JSON(http.StatusCreated, p)
}
func (h *Handler) FetchPost(c echo.Context) (err error) {
userID := userIDFromToken(c)
page, _ := strconv.Atoi(c.QueryParam("page"))
limit, _ := strconv.Atoi(c.QueryParam("limit"))
// Defaults
if page == 0 {
page = 1
}
if limit == 0 {
limit = 100
}
// Retrieve posts from database
posts := []*model.Post{}
db := h.DB.Clone()
if err = db.DB("twitter").C("posts").
Find(bson.M{"to": userID}).
Skip((page - 1) * limit).
Limit(limit).
All(&posts); err != nil {
return
}
defer db.Close()
return c.JSON(http.StatusOK, posts)
}

View File

@ -1,97 +0,0 @@
package handler
import (
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/labstack/echo/cookbook/twitter/model"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func (h *Handler) Signup(c echo.Context) (err error) {
// Bind
u := &model.User{ID: bson.NewObjectId()}
if err = c.Bind(u); err != nil {
return
}
// Validate
if u.Email == "" || u.Password == "" {
return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid email or password"}
}
// Save user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").Insert(u); err != nil {
return
}
return c.JSON(http.StatusCreated, u)
}
func (h *Handler) Login(c echo.Context) (err error) {
// Bind
u := new(model.User)
if err = c.Bind(u); err != nil {
return
}
// Find user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").
Find(bson.M{"email": u.Email, "password": u.Password}).One(u); err != nil {
if err == mgo.ErrNotFound {
return &echo.HTTPError{Code: http.StatusUnauthorized, Message: "invalid email or password"}
}
return
}
//-----
// JWT
//-----
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["id"] = u.ID
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response
u.Token, err = token.SignedString([]byte(Key))
if err != nil {
return err
}
u.Password = "" // Don't send password
return c.JSON(http.StatusOK, u)
}
func (h *Handler) Follow(c echo.Context) (err error) {
userID := userIDFromToken(c)
id := c.Param("id")
// Add a follower to user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").
UpdateId(bson.ObjectIdHex(id), bson.M{"$addToSet": bson.M{"followers": userID}}); err != nil {
if err == mgo.ErrNotFound {
return echo.ErrNotFound
}
}
return
}
func userIDFromToken(c echo.Context) string {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
return claims["id"].(string)
}

View File

@ -1,12 +0,0 @@
package model
import "gopkg.in/mgo.v2/bson"
type (
Post struct {
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
To string `json:"to" bson:"to"`
From string `json:"from" bson:"from"`
Message string `json:"message" bson:"message"`
}
)

View File

@ -1,13 +0,0 @@
package model
import "gopkg.in/mgo.v2/bson"
type (
User struct {
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
Email string `json:"email" bson:"email"`
Password string `json:"password,omitempty" bson:"password"`
Token string `json:"token,omitempty" bson:"-"`
Followers []string `json:"followers,omitempty" bson:"followers,omitempty"`
}
)

View File

@ -1,52 +0,0 @@
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/cookbook/twitter/handler"
"github.com/labstack/echo/middleware"
"github.com/labstack/gommon/log"
mgo "gopkg.in/mgo.v2"
)
func main() {
e := echo.New()
e.Logger.SetLevel(log.ERROR)
e.Use(middleware.Logger())
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte(handler.Key),
Skipper: func(c echo.Context) bool {
// Skip authentication for and signup login requests
if c.Path() == "/login" || c.Path() == "/signup" {
return true
}
return false
},
}))
// Database connection
db, err := mgo.Dial("localhost")
if err != nil {
e.Logger.Fatal(err)
}
// Create indices
if err = db.Copy().DB("twitter").C("users").EnsureIndex(mgo.Index{
Key: []string{"email"},
Unique: true,
}); err != nil {
log.Fatal(err)
}
// Initialize handler
h := &handler.Handler{DB: db}
// Routes
e.POST("/signup", h.Signup)
e.POST("/login", h.Login)
e.POST("/follow/:id", h.Follow)
e.POST("/posts", h.CreatePost)
e.GET("/feed", h.FetchPost)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,47 +0,0 @@
package main
import (
"fmt"
"log"
"github.com/labstack/echo"
"github.com/gorilla/websocket"
"github.com/labstack/echo/middleware"
)
var (
upgrader = websocket.Upgrader{}
)
func hello(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
for {
// Write
err := ws.WriteMessage(websocket.TextMessage, []byte("Hello, Client!"))
if err != nil {
log.Fatal(err)
}
// Read
_, msg, err := ws.ReadMessage()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", msg)
}
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "../public")
e.GET("/ws", hello)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -1,41 +0,0 @@
package main
import (
"fmt"
"log"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"golang.org/x/net/websocket"
)
func hello(c echo.Context) error {
websocket.Handler(func(ws *websocket.Conn) {
defer ws.Close()
for {
// Write
err := websocket.Message.Send(ws, "Hello, Client!")
if err != nil {
log.Fatal(err)
}
// Read
msg := ""
err = websocket.Message.Receive(ws, &msg)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", msg)
}
}).ServeHTTP(c.Response(), c.Request())
return nil
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/", "../public")
e.GET("/ws", hello)
e.Logger.Fatal(e.Start(":1323"))
}

View File

@ -42,10 +42,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
slog "log" stdLog "log"
"net" "net"
"net/http" "net/http"
"path" "path"
"path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"sync" "sync"
@ -59,7 +60,7 @@ import (
type ( type (
// Echo is the top-level framework instance. // Echo is the top-level framework instance.
Echo struct { Echo struct {
stdLogger *slog.Logger stdLogger *stdLog.Logger
colorer *color.Color colorer *color.Color
premiddleware []MiddlewareFunc premiddleware []MiddlewareFunc
middleware []MiddlewareFunc middleware []MiddlewareFunc
@ -73,20 +74,21 @@ type (
TLSListener net.Listener TLSListener net.Listener
DisableHTTP2 bool DisableHTTP2 bool
Debug bool Debug bool
HideBanner bool
HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler
Binder Binder Binder Binder
Validator Validator Validator Validator
Renderer Renderer Renderer Renderer
AutoTLSManager autocert.Manager AutoTLSManager autocert.Manager
Mutex sync.RWMutex // Mutex sync.RWMutex
Logger Logger Logger Logger
} }
// Route contains a handler and information for matching against requests. // Route contains a handler and information for matching against requests.
Route struct { Route struct {
Method string Method string `json:"method"`
Path string Path string `json:"path"`
Handler string Handler string `json:"handler"`
} }
// HTTPError represents an error that occurred while handling a request. // HTTPError represents an error that occurred while handling a request.
@ -144,6 +146,8 @@ const (
MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8 MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8
MIMEApplicationXML = "application/xml" MIMEApplicationXML = "application/xml"
MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + charsetUTF8 MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + charsetUTF8
MIMETextXML = "text/xml"
MIMETextXMLCharsetUTF8 = MIMETextXML + "; " + charsetUTF8
MIMEApplicationForm = "application/x-www-form-urlencoded" MIMEApplicationForm = "application/x-www-form-urlencoded"
MIMEApplicationProtobuf = "application/protobuf" MIMEApplicationProtobuf = "application/protobuf"
MIMEApplicationMsgpack = "application/msgpack" MIMEApplicationMsgpack = "application/msgpack"
@ -161,27 +165,34 @@ const (
// Headers // Headers
const ( const (
HeaderAcceptEncoding = "Accept-Encoding" HeaderAccept = "Accept"
HeaderAllow = "Allow" HeaderAcceptEncoding = "Accept-Encoding"
HeaderAuthorization = "Authorization" HeaderAllow = "Allow"
HeaderContentDisposition = "Content-Disposition" HeaderAuthorization = "Authorization"
HeaderContentEncoding = "Content-Encoding" HeaderContentDisposition = "Content-Disposition"
HeaderContentLength = "Content-Length" HeaderContentEncoding = "Content-Encoding"
HeaderContentType = "Content-Type" HeaderContentLength = "Content-Length"
HeaderCookie = "Cookie" HeaderContentType = "Content-Type"
HeaderSetCookie = "Set-Cookie" HeaderCookie = "Cookie"
HeaderIfModifiedSince = "If-Modified-Since" HeaderSetCookie = "Set-Cookie"
HeaderLastModified = "Last-Modified" HeaderIfModifiedSince = "If-Modified-Since"
HeaderLocation = "Location" HeaderLastModified = "Last-Modified"
HeaderUpgrade = "Upgrade" HeaderLocation = "Location"
HeaderVary = "Vary" HeaderUpgrade = "Upgrade"
HeaderWWWAuthenticate = "WWW-Authenticate" HeaderVary = "Vary"
HeaderXForwardedProto = "X-Forwarded-Proto" HeaderWWWAuthenticate = "WWW-Authenticate"
HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" HeaderXForwardedFor = "X-Forwarded-For"
HeaderXForwardedFor = "X-Forwarded-For" HeaderXForwardedProto = "X-Forwarded-Proto"
HeaderXRealIP = "X-Real-IP" HeaderXForwardedProtocol = "X-Forwarded-Protocol"
HeaderServer = "Server" HeaderXForwardedSsl = "X-Forwarded-Ssl"
HeaderOrigin = "Origin" HeaderXUrlScheme = "X-Url-Scheme"
HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
HeaderXRealIP = "X-Real-IP"
HeaderXRequestID = "X-Request-ID"
HeaderServer = "Server"
HeaderOrigin = "Origin"
// Access control
HeaderAccessControlRequestMethod = "Access-Control-Request-Method" HeaderAccessControlRequestMethod = "Access-Control-Request-Method"
HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers"
HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin"
@ -200,6 +211,22 @@ const (
HeaderXCSRFToken = "X-CSRF-Token" HeaderXCSRFToken = "X-CSRF-Token"
) )
const (
version = "3.1.0"
website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = `
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ %s
High performance, minimalist Go web framework
%s
____________________________________O/_______
O\
`
)
var ( var (
methods = [...]string{ methods = [...]string{
CONNECT, CONNECT,
@ -219,6 +246,7 @@ var (
ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType) ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType)
ErrNotFound = NewHTTPError(http.StatusNotFound) ErrNotFound = NewHTTPError(http.StatusNotFound)
ErrUnauthorized = NewHTTPError(http.StatusUnauthorized) ErrUnauthorized = NewHTTPError(http.StatusUnauthorized)
ErrForbidden = NewHTTPError(http.StatusForbidden)
ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed) ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed)
ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge) ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge)
ErrValidatorNotRegistered = errors.New("Validator not registered") ErrValidatorNotRegistered = errors.New("Validator not registered")
@ -255,7 +283,7 @@ func New() (e *Echo) {
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
e.Binder = &DefaultBinder{} e.Binder = &DefaultBinder{}
e.Logger.SetLevel(log.OFF) e.Logger.SetLevel(log.OFF)
e.stdLogger = slog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
e.pool.New = func() interface{} { e.pool.New = func() interface{} {
return e.NewContext(nil, nil) return e.NewContext(nil, nil)
} }
@ -398,12 +426,16 @@ func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middlew
// Static registers a new route with path prefix to serve static files from the // Static registers a new route with path prefix to serve static files from the
// provided root directory. // provided root directory.
func (e *Echo) Static(prefix, root string) { func (e *Echo) Static(prefix, root string) {
if root == "" {
root = "." // For security we want to restrict to CWD.
}
static(e, prefix, root) static(e, prefix, root)
} }
func static(i i, prefix, root string) { func static(i i, prefix, root string) {
h := func(c Context) error { h := func(c Context) error {
return c.File(path.Join(root, c.Param("*"))) name := filepath.Join(root, path.Clean("/"+c.Param("*"))) // "/"+ for security
return c.File(name)
} }
i.GET(prefix, h) i.GET(prefix, h)
if prefix == "/" { if prefix == "/" {
@ -430,7 +462,7 @@ func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...Middl
} }
return h(c) return h(c)
}) })
r := Route{ r := &Route{
Method: method, Method: method,
Path: path, Path: path,
Handler: name, Handler: name,
@ -476,8 +508,8 @@ func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
} }
// Routes returns the registered routes. // Routes returns the registered routes.
func (e *Echo) Routes() []Route { func (e *Echo) Routes() []*Route {
routes := []Route{} routes := []*Route{}
for _, v := range e.router.routes { for _, v := range e.router.routes {
routes = append(routes, v) routes = append(routes, v)
} }
@ -499,8 +531,8 @@ func (e *Echo) ReleaseContext(c Context) {
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests. // ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Acquire lock // Acquire lock
e.Mutex.RLock() // e.Mutex.RLock()
defer e.Mutex.RUnlock() // defer e.Mutex.RUnlock()
// Acquire context // Acquire context
c := e.pool.Get().(*context) c := e.pool.Get().(*context)
@ -510,7 +542,10 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Middleware // Middleware
h := func(c Context) error { h := func(c Context) error {
method := r.Method method := r.Method
path := r.URL.EscapedPath() path := r.URL.RawPath
if path == "" {
path = r.URL.Path
}
e.router.Find(method, path, c) e.router.Find(method, path, c)
h := c.Handler() h := c.Handler()
for i := len(e.middleware) - 1; i >= 0; i-- { for i := len(e.middleware) - 1; i >= 0; i-- {
@ -572,8 +607,15 @@ func (e *Echo) startTLS(address string) error {
func (e *Echo) StartServer(s *http.Server) (err error) { func (e *Echo) StartServer(s *http.Server) (err error) {
// Setup // Setup
e.colorer.SetOutput(e.Logger.Output()) e.colorer.SetOutput(e.Logger.Output())
s.Handler = e
s.ErrorLog = e.stdLogger s.ErrorLog = e.stdLogger
s.Handler = e
if e.Debug {
e.Logger.SetLevel(log.DEBUG)
}
if !e.HideBanner {
e.colorer.Printf(banner, e.colorer.Red("v"+version), e.colorer.Blue(website))
}
if s.TLSConfig == nil { if s.TLSConfig == nil {
if e.Listener == nil { if e.Listener == nil {
@ -582,7 +624,9 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
return err return err
} }
} }
e.colorer.Printf("⇛ http server started on %s\n", e.colorer.Green(e.Listener.Addr())) if !e.HideBanner {
e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr()))
}
return s.Serve(e.Listener) return s.Serve(e.Listener)
} }
if e.TLSListener == nil { if e.TLSListener == nil {
@ -592,7 +636,9 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
} }
e.TLSListener = tls.NewListener(l, s.TLSConfig) e.TLSListener = tls.NewListener(l, s.TLSConfig)
} }
e.colorer.Printf("⇛ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr())) if !e.HideBanner {
e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr()))
}
return s.Serve(e.TLSListener) return s.Serve(e.TLSListener)
} }

25
vendor/github.com/labstack/echo/echo_go1.8.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
// +build go1.8
package echo
import (
stdContext "context"
)
// Close immediately stops the server.
// It internally calls `http.Server#Close()`.
func (e *Echo) Close() error {
if err := e.TLSServer.Close(); err != nil {
return err
}
return e.Server.Close()
}
// Shutdown stops server the gracefully.
// It internally calls `http.Server#Shutdown()`.
func (e *Echo) Shutdown(ctx stdContext.Context) error {
if err := e.TLSServer.Shutdown(ctx); err != nil {
return err
}
return e.Server.Shutdown(ctx)
}

View File

@ -1,8 +1,12 @@
package echo package echo
import (
"path"
)
type ( type (
// Group is a set of sub-routes for a specified route. It can be used for inner // Group is a set of sub-routes for a specified route. It can be used for inner
// routes that share a common middlware or functionality that should be separate // routes that share a common middleware or functionality that should be separate
// from the parent echo instance while still inheriting from it. // from the parent echo instance while still inheriting from it.
Group struct { Group struct {
prefix string prefix string
@ -14,6 +18,11 @@ type (
// Use implements `Echo#Use()` for sub-routes within the Group. // Use implements `Echo#Use()` for sub-routes within the Group.
func (g *Group) Use(middleware ...MiddlewareFunc) { func (g *Group) Use(middleware ...MiddlewareFunc) {
g.middleware = append(g.middleware, middleware...) g.middleware = append(g.middleware, middleware...)
// Allow all requests to reach the group as they might get dropped if router
// 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
}, g.middleware...)
} }
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group.

View File

@ -2,6 +2,7 @@ package middleware
import ( import (
"encoding/base64" "encoding/base64"
"strconv"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
@ -15,20 +16,26 @@ type (
// Validator is a function to validate BasicAuth credentials. // Validator is a function to validate BasicAuth credentials.
// Required. // Required.
Validator BasicAuthValidator Validator BasicAuthValidator
// Realm is a string to define realm attribute of BasicAuth.
// Default value "Restricted".
Realm string
} }
// BasicAuthValidator defines a function to validate BasicAuth credentials. // BasicAuthValidator defines a function to validate BasicAuth credentials.
BasicAuthValidator func(string, string, echo.Context) bool BasicAuthValidator func(string, string, echo.Context) (bool, error)
) )
const ( const (
basic = "Basic" basic = "Basic"
defaultRealm = "Restricted"
) )
var ( var (
// DefaultBasicAuthConfig is the default BasicAuth middleware config. // DefaultBasicAuthConfig is the default BasicAuth middleware config.
DefaultBasicAuthConfig = BasicAuthConfig{ DefaultBasicAuthConfig = BasicAuthConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
Realm: defaultRealm,
} }
) )
@ -52,6 +59,9 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultBasicAuthConfig.Skipper config.Skipper = DefaultBasicAuthConfig.Skipper
} }
if config.Realm == "" {
config.Realm = defaultRealm
}
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -71,15 +81,25 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
for i := 0; i < len(cred); i++ { for i := 0; i < len(cred); i++ {
if cred[i] == ':' { if cred[i] == ':' {
// Verify credentials // Verify credentials
if config.Validator(cred[:i], cred[i+1:], c) { valid, err := config.Validator(cred[:i], cred[i+1:], c)
if err != nil {
return err
} else if valid {
return next(c) return next(c)
} }
} }
} }
} }
realm := ""
if config.Realm == defaultRealm {
realm = defaultRealm
} else {
realm = strconv.Quote(config.Realm)
}
// Need to return `401` for browsers to pop-up login box. // Need to return `401` for browsers to pop-up login box.
c.Response().Header().Set(echo.HeaderWWWAuthenticate, basic+" realm=Restricted") c.Response().Header().Set(echo.HeaderWWWAuthenticate, basic+" realm="+realm)
return echo.ErrUnauthorized return echo.ErrUnauthorized
} }
} }

View File

@ -108,8 +108,8 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b) return w.Writer.Write(b)
} }
func (w *gzipResponseWriter) Flush() error { func (w *gzipResponseWriter) Flush() {
return w.Writer.(*gzip.Writer).Flush() w.Writer.(*gzip.Writer).Flush()
} }
func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {

View File

@ -91,7 +91,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
config.Skipper = DefaultJWTConfig.Skipper config.Skipper = DefaultJWTConfig.Skipper
} }
if config.SigningKey == nil { if config.SigningKey == nil {
panic("jwt middleware requires signing key") panic("echo: jwt middleware requires signing key")
} }
if config.SigningMethod == "" { if config.SigningMethod == "" {
config.SigningMethod = DefaultJWTConfig.SigningMethod config.SigningMethod = DefaultJWTConfig.SigningMethod

View File

@ -32,7 +32,7 @@ type (
} }
// KeyAuthValidator defines a function to validate KeyAuth credentials. // KeyAuthValidator defines a function to validate KeyAuth credentials.
KeyAuthValidator func(string, echo.Context) bool KeyAuthValidator func(string, echo.Context) (bool, error)
keyExtractor func(echo.Context) (string, error) keyExtractor func(echo.Context) (string, error)
) )
@ -94,7 +94,10 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error())
} }
if config.Validator(key, c) { valid, err := config.Validator(key, c)
if err != nil {
return err
} else if valid {
return next(c) return next(c)
} }

View File

@ -26,7 +26,7 @@ type (
// - time_unix_nano // - time_unix_nano
// - time_rfc3339 // - time_rfc3339
// - time_rfc3339_nano // - time_rfc3339_nano
// - id (Request ID - Not implemented) // - id (Request ID)
// - remote_ip // - remote_ip
// - uri // - uri
// - host // - host
@ -62,7 +62,7 @@ var (
// DefaultLoggerConfig is the default Logger middleware config. // DefaultLoggerConfig is the default Logger middleware config.
DefaultLoggerConfig = LoggerConfig{ DefaultLoggerConfig = LoggerConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
Format: `{"time":"${time_rfc3339_nano}","remote_ip":"${remote_ip}","host":"${host}",` + Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}",` +
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
`"latency_human":"${latency_human}","bytes_in":${bytes_in},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in},` +
`"bytes_out":${bytes_out}}` + "\n", `"bytes_out":${bytes_out}}` + "\n",
@ -126,6 +126,12 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return buf.WriteString(time.Now().Format(time.RFC3339)) return buf.WriteString(time.Now().Format(time.RFC3339))
case "time_rfc3339_nano": case "time_rfc3339_nano":
return buf.WriteString(time.Now().Format(time.RFC3339Nano)) return buf.WriteString(time.Now().Format(time.RFC3339Nano))
case "id":
id := req.Header.Get(echo.HeaderXRequestID)
if id == "" {
id = res.Header().Get(echo.HeaderXRequestID)
}
return buf.WriteString(id)
case "remote_ip": case "remote_ip":
return buf.WriteString(c.RealIP()) return buf.WriteString(c.RealIP())
case "host": case "host":
@ -177,6 +183,11 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return buf.Write([]byte(c.QueryParam(tag[6:]))) return buf.Write([]byte(c.QueryParam(tag[6:])))
case strings.HasPrefix(tag, "form:"): case strings.HasPrefix(tag, "form:"):
return buf.Write([]byte(c.FormValue(tag[5:]))) return buf.Write([]byte(c.FormValue(tag[5:])))
case strings.HasPrefix(tag, "cookie:"):
cookie, err := c.Cookie(tag[7:])
if err == nil {
return buf.Write([]byte(cookie.Value))
}
} }
} }
return 0, nil return 0, nil

View File

@ -9,6 +9,6 @@ type (
) )
// DefaultSkipper returns false which processes the middleware. // DefaultSkipper returns false which processes the middleware.
func DefaultSkipper(c echo.Context) bool { func DefaultSkipper(echo.Context) bool {
return false return false
} }

160
vendor/github.com/labstack/echo/middleware/proxy.go generated vendored Normal file
View File

@ -0,0 +1,160 @@
package middleware
import (
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync/atomic"
"time"
"github.com/labstack/echo"
)
// TODO: Handle TLS proxy
type (
// ProxyConfig defines the config for Proxy middleware.
ProxyConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Balancer defines a load balancing technique.
// Required.
// Possible values:
// - RandomBalancer
// - RoundRobinBalancer
Balancer ProxyBalancer
}
// ProxyTarget defines the upstream target.
ProxyTarget struct {
URL *url.URL
}
// RandomBalancer implements a random load balancing technique.
RandomBalancer struct {
Targets []*ProxyTarget
random *rand.Rand
}
// RoundRobinBalancer implements a round-robin load balancing technique.
RoundRobinBalancer struct {
Targets []*ProxyTarget
i uint32
}
// ProxyBalancer defines an interface to implement a load balancing technique.
ProxyBalancer interface {
Next() *ProxyTarget
}
)
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()
if err != nil {
c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", r.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))
c.Error(he)
return
}
defer out.Close()
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))
c.Error(he)
return
}
errc := make(chan error, 2)
cp := func(dst io.Writer, src io.Reader) {
_, err := io.Copy(dst, src)
errc <- err
}
go cp(out, in)
go cp(in, out)
err = <-errc
if err != nil && err != io.EOF {
c.Logger().Errorf("proxy raw, error=%v, url=%s", r.URL, err)
}
})
}
// Next randomly returns an upstream target.
func (r *RandomBalancer) Next() *ProxyTarget {
if r.random == nil {
r.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
}
return r.Targets[r.random.Intn(len(r.Targets))]
}
// Next returns an upstream target using round-robin technique.
func (r *RoundRobinBalancer) Next() *ProxyTarget {
r.i = r.i % uint32(len(r.Targets))
t := r.Targets[r.i]
atomic.AddUint32(&r.i, 1)
return t
}
// Proxy returns an HTTP/WebSocket reverse proxy middleware.
func Proxy(config ProxyConfig) echo.MiddlewareFunc {
// Defaults
if config.Skipper == nil {
config.Skipper = DefaultLoggerConfig.Skipper
}
if config.Balancer == nil {
panic("echo: proxy middleware requires balancer")
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
req := c.Request()
res := c.Response()
tgt := config.Balancer.Next()
// Fix header
if req.Header.Get(echo.HeaderXRealIP) == "" {
req.Header.Set(echo.HeaderXRealIP, c.RealIP())
}
if req.Header.Get(echo.HeaderXForwardedProto) == "" {
req.Header.Set(echo.HeaderXForwardedProto, c.Scheme())
}
if c.IsWebSocket() && req.Header.Get(echo.HeaderXForwardedFor) == "" { // For HTTP, it is automatically set by Go HTTP reverse proxy.
req.Header.Set(echo.HeaderXForwardedFor, c.RealIP())
}
// Proxy
switch {
case c.IsWebSocket():
proxyRaw(tgt, c).ServeHTTP(res, req)
case req.Header.Get(echo.HeaderAccept) == "text/event-stream":
default:
proxyHTTP(tgt).ServeHTTP(res, req)
}
return
}
}
}

View File

@ -0,0 +1,64 @@
package middleware
import (
"github.com/labstack/echo"
"github.com/labstack/gommon/random"
)
type (
// RequestIDConfig defines the config for RequestID middleware.
RequestIDConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Generator defines a function to generate an ID.
// Optional. Default value random.String(32).
Generator func() string
}
)
var (
// DefaultRequestIDConfig is the default RequestID middleware config.
DefaultRequestIDConfig = RequestIDConfig{
Skipper: DefaultSkipper,
Generator: generator,
}
)
// RequestID returns a X-Request-ID middleware.
func RequestID() echo.MiddlewareFunc {
return RequestIDWithConfig(DefaultRequestIDConfig)
}
// RequestIDWithConfig returns a X-Request-ID middleware with config.
func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc {
// Defaults
if config.Skipper == nil {
config.Skipper = DefaultRequestIDConfig.Skipper
}
if config.Generator == nil {
config.Generator = generator
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
res := c.Response()
rid := req.Header.Get(echo.HeaderXRequestID)
if rid == "" {
rid = config.Generator()
}
res.Header().Set(echo.HeaderXRequestID, rid)
return next(c)
}
}
}
func generator() string {
return random.String(32)
}

View File

@ -3,7 +3,9 @@ package middleware
import ( import (
"fmt" "fmt"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
@ -53,6 +55,9 @@ func Static(root string) echo.MiddlewareFunc {
// See `Static()`. // See `Static()`.
func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
// Defaults // Defaults
if config.Root == "" {
config.Root = "." // For security we want to restrict to CWD.
}
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultStaticConfig.Skipper config.Skipper = DefaultStaticConfig.Skipper
} }
@ -62,26 +67,44 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
p := c.Param("*") if config.Skipper(c) {
name := filepath.Join(config.Root, p) return next(c)
fi, err := os.Stat(name) }
p := c.Request().URL.Path
if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
p = c.Param("*")
}
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
fi, err := os.Stat(name)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if config.HTML5 { if config.HTML5 && path.Ext(p) == "" {
return c.File(filepath.Join(config.Root, config.Index)) return c.File(filepath.Join(config.Root, config.Index))
} }
return echo.ErrNotFound return next(c)
} }
return err return err
} }
if fi.IsDir() { if fi.IsDir() {
if config.Browse { index := filepath.Join(name, config.Index)
return listDir(name, c.Response()) fi, err = os.Stat(index)
if err != nil {
if config.Browse {
return listDir(name, c.Response())
}
if os.IsNotExist(err) {
return next(c)
}
return err
} }
return c.File(filepath.Join(name, config.Index))
return c.File(index)
} }
return c.File(name) return c.File(name)
} }
} }

View File

@ -7,7 +7,7 @@ type (
// request matching and URL path parameter parsing. // request matching and URL path parameter parsing.
Router struct { Router struct {
tree *node tree *node
routes map[string]Route routes map[string]*Route
echo *Echo echo *Echo
} }
node struct { node struct {
@ -47,7 +47,7 @@ func NewRouter(e *Echo) *Router {
tree: &node{ tree: &node{
methodHandler: new(methodHandler), methodHandler: new(methodHandler),
}, },
routes: make(map[string]Route), routes: map[string]*Route{},
echo: e, echo: e,
} }
} }
@ -101,7 +101,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
cn := r.tree // Current node as root cn := r.tree // Current node as root
if cn == nil { if cn == nil {
panic("echo invalid method") panic("echo: invalid method")
} }
search := path search := path
@ -296,18 +296,19 @@ func (n *node) checkMethodNotAllowed() HandlerFunc {
// - Get context from `Echo#AcquireContext()` // - Get context from `Echo#AcquireContext()`
// - Reset it `Context#Reset()` // - Reset it `Context#Reset()`
// - Return it `Echo#ReleaseContext()`. // - Return it `Echo#ReleaseContext()`.
func (r *Router) Find(method, path string, context Context) { func (r *Router) Find(method, path string, c Context) {
context.SetPath(path) ctx := c.(*context)
ctx.path = path
cn := r.tree // Current node as root cn := r.tree // Current node as root
var ( var (
search = path search = path
c *node // Child node child *node // Child node
n int // Param counter n int // Param counter
nk kind // Next kind nk kind // Next kind
nn *node // Next node nn *node // Next node
ns string // Next search ns string // Next search
pvalues = context.ParamValues() pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
) )
// Search order static > param > any // Search order static > param > any
@ -352,20 +353,20 @@ func (r *Router) Find(method, path string, context Context) {
} }
// Static node // Static node
if c = cn.findChild(search[0], skind); c != nil { if child = cn.findChild(search[0], skind); child != nil {
// Save next // Save next
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623 if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
nk = pkind nk = pkind
nn = cn nn = cn
ns = search ns = search
} }
cn = c cn = child
continue continue
} }
// Param node // Param node
Param: Param:
if c = cn.findChildByKind(pkind); c != nil { if child = cn.findChildByKind(pkind); child != nil {
// Issue #378 // Issue #378
if len(pvalues) == n { if len(pvalues) == n {
continue continue
@ -378,7 +379,7 @@ func (r *Router) Find(method, path string, context Context) {
ns = search ns = search
} }
cn = c cn = child
i, l := 0, len(search) i, l := 0, len(search)
for ; i < l && search[i] != '/'; i++ { for ; i < l && search[i] != '/'; i++ {
} }
@ -409,13 +410,13 @@ func (r *Router) Find(method, path string, context Context) {
} }
End: End:
context.SetHandler(cn.findHandler(method)) ctx.handler = cn.findHandler(method)
context.SetPath(cn.ppath) ctx.path = cn.ppath
context.SetParamNames(cn.pnames...) ctx.pnames = cn.pnames
// NOTE: Slow zone... // NOTE: Slow zone...
if context.Handler() == nil { if ctx.handler == nil {
context.SetHandler(cn.checkMethodNotAllowed()) ctx.handler = cn.checkMethodNotAllowed()
// Dig further for any, might have an empty value for *, e.g. // Dig further for any, might have an empty value for *, e.g.
// serving a directory. Issue #207. // serving a directory. Issue #207.
@ -423,12 +424,12 @@ End:
return return
} }
if h := cn.findHandler(method); h != nil { if h := cn.findHandler(method); h != nil {
context.SetHandler(h) ctx.handler = h
} else { } else {
context.SetHandler(cn.checkMethodNotAllowed()) ctx.handler = cn.checkMethodNotAllowed()
} }
context.SetPath(cn.ppath) ctx.path = cn.ppath
context.SetParamNames(cn.pnames...) ctx.pnames = cn.pnames
pvalues[len(cn.pnames)-1] = "" pvalues[len(cn.pnames)-1] = ""
} }

View File

@ -2,22 +2,24 @@ package random
import ( import (
"math/rand" "math/rand"
"strings"
"time" "time"
) )
type ( type (
Random struct { Random struct {
charset Charset
} }
Charset string
) )
// Charsets
const ( const (
Alphanumeric Charset = Alphabetic + Numeric Uppercase string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Alphabetic Charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" Lowercase = "abcdefghijklmnopqrstuvwxyz"
Numeric Charset = "0123456789" Alphabetic = Uppercase + Lowercase
Hex Charset = Numeric + "abcdef" Numeric = "0123456789"
Alphanumeric = Alphabetic + Numeric
Symbols = "`" + `~!@#$%^&*()-_+={}[]|\;:"<>,./?`
Hex = Numeric + "abcdef"
) )
var ( var (
@ -26,27 +28,21 @@ var (
func New() *Random { func New() *Random {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
return &Random{ return new(Random)
charset: Alphanumeric, }
func (r *Random) String(length uint8, charsets ...string) string {
charset := strings.Join(charsets, "")
if charset == "" {
charset = Alphanumeric
} }
}
func (r *Random) SetCharset(c Charset) {
r.charset = c
}
func (r *Random) String(length uint8) string {
b := make([]byte, length) b := make([]byte, length)
for i := range b { for i := range b {
b[i] = r.charset[rand.Int63()%int64(len(r.charset))] b[i] = charset[rand.Int63()%int64(len(charset))]
} }
return string(b) return string(b)
} }
func SetCharset(c Charset) { func String(length uint8, charsets ...string) string {
global.SetCharset(c) return global.String(length, charsets...)
}
func String(length uint8) string {
return global.String(length)
} }

View File

@ -1,14 +0,0 @@
package main
import (
"time"
)
import l4g "code.google.com/p/log4go"
func main() {
log := l4g.NewLogger()
defer log.Close()
log.AddFilter("stdout", l4g.DEBUG, l4g.NewConsoleLogWriter())
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
}

View File

@ -1,57 +0,0 @@
package main
import (
"bufio"
"fmt"
"io"
"os"
"time"
)
import l4g "code.google.com/p/log4go"
const (
filename = "flw.log"
)
func main() {
// Get a new logger instance
log := l4g.NewLogger()
// Create a default logger that is logging messages of FINE or higher
log.AddFilter("file", l4g.FINE, l4g.NewFileLogWriter(filename, false))
log.Close()
/* Can also specify manually via the following: (these are the defaults) */
flw := l4g.NewFileLogWriter(filename, false)
flw.SetFormat("[%D %T] [%L] (%S) %M")
flw.SetRotate(false)
flw.SetRotateSize(0)
flw.SetRotateLines(0)
flw.SetRotateDaily(false)
log.AddFilter("file", l4g.FINE, flw)
// Log some experimental messages
log.Finest("Everything is created now (notice that I will not be printing to the file)")
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
log.Critical("Time to close out!")
// Close the log
log.Close()
// Print what was logged to the file (yes, I know I'm skipping error checking)
fd, _ := os.Open(filename)
in := bufio.NewReader(fd)
fmt.Print("Messages logged to file were: (line numbers not included)\n")
for lineno := 1; ; lineno++ {
line, err := in.ReadString('\n')
if err == io.EOF {
break
}
fmt.Printf("%3d:\t%s", lineno, line)
}
fd.Close()
// Remove the file so it's not lying around
os.Remove(filename)
}

View File

@ -1,42 +0,0 @@
package main
import (
"flag"
"fmt"
"net"
"os"
)
var (
port = flag.String("p", "12124", "Port number to listen on")
)
func e(err error) {
if err != nil {
fmt.Printf("Erroring out: %s\n", err)
os.Exit(1)
}
}
func main() {
flag.Parse()
// Bind to the port
bind, err := net.ResolveUDPAddr("0.0.0.0:" + *port)
e(err)
// Create listener
listener, err := net.ListenUDP("udp", bind)
e(err)
fmt.Printf("Listening to port %s...\n", *port)
for {
// read into a new buffer
buffer := make([]byte, 1024)
_, _, err := listener.ReadFrom(buffer)
e(err)
// log to standard output
fmt.Println(string(buffer))
}
}

View File

@ -1,18 +0,0 @@
package main
import (
"time"
)
import l4g "code.google.com/p/log4go"
func main() {
log := l4g.NewLogger()
log.AddFilter("network", l4g.FINEST, l4g.NewSocketLogWriter("udp", "192.168.1.255:12124"))
// Run `nc -u -l -p 12124` or similar before you run this to see the following message
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
// This makes sure the output stream buffer is written
log.Close()
}

View File

@ -1,13 +0,0 @@
package main
import l4g "code.google.com/p/log4go"
func main() {
// Load the configuration (isn't this easy?)
l4g.LoadConfiguration("example.xml")
// And now we're ready!
l4g.Finest("This will only go to those of you really cool UDP kids! If you change enabled=true.")
l4g.Debug("Oh no! %d + %d = %d!", 2, 2, 2+2)
l4g.Info("About that time, eh chaps?")
}

View File

@ -137,7 +137,10 @@ func (stream *Stream) connect() {
res, err := stream.gitter.getResponse(stream.url, stream) res, err := stream.gitter.getResponse(stream.url, stream)
if err != nil || res.StatusCode != 200 { if err != nil || res.StatusCode != 200 {
stream.gitter.log(fmt.Sprintf("Failed to get response, trying reconnect (Status code: %v)", res.StatusCode)) stream.gitter.log("Failed to get response, trying reconnect")
if res != nil {
stream.gitter.log(fmt.Sprintf("Status code: %v", res.StatusCode))
}
stream.gitter.log(err) stream.gitter.log(err)
// sleep and wait // sleep and wait

View File

@ -1,3 +1,5 @@
// +build !appengine
package fasttemplate package fasttemplate
import ( import (

11
vendor/github.com/valyala/fasttemplate/unsafe_gae.go generated vendored Normal file
View File

@ -0,0 +1,11 @@
// +build appengine
package fasttemplate
func unsafeBytes2String(b []byte) string {
return string(b)
}
func unsafeString2Bytes(s string) []byte {
return []byte(s)
}

16
vendor/manifest vendored
View File

@ -53,7 +53,7 @@
"importpath": "github.com/dgrijalva/jwt-go", "importpath": "github.com/dgrijalva/jwt-go",
"repository": "https://github.com/dgrijalva/jwt-go", "repository": "https://github.com/dgrijalva/jwt-go",
"vcs": "git", "vcs": "git",
"revision": "2268707a8f0843315e2004ee4f1d021dc08baedf", "revision": "6c8dedd55f8a2e41f605de6d5d66e51ed1f299fc",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },
@ -170,7 +170,7 @@
"importpath": "github.com/labstack/echo", "importpath": "github.com/labstack/echo",
"repository": "https://github.com/labstack/echo", "repository": "https://github.com/labstack/echo",
"vcs": "git", "vcs": "git",
"revision": "0b53f397ad7709a27d37500a67735c0a639b5c38", "revision": "c3887ebb131d996411cf13a9688ab02c8dba599e",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },
@ -178,7 +178,7 @@
"importpath": "github.com/labstack/gommon/bytes", "importpath": "github.com/labstack/gommon/bytes",
"repository": "https://github.com/labstack/gommon", "repository": "https://github.com/labstack/gommon",
"vcs": "git", "vcs": "git",
"revision": "f72d3c883f8ea180da8f085dd320804c41332ad1", "revision": "1121fd3e243c202482226a7afe4dcd07ffc4139a",
"branch": "master", "branch": "master",
"path": "/bytes", "path": "/bytes",
"notests": true "notests": true
@ -187,7 +187,7 @@
"importpath": "github.com/labstack/gommon/color", "importpath": "github.com/labstack/gommon/color",
"repository": "https://github.com/labstack/gommon", "repository": "https://github.com/labstack/gommon",
"vcs": "git", "vcs": "git",
"revision": "f72d3c883f8ea180da8f085dd320804c41332ad1", "revision": "1121fd3e243c202482226a7afe4dcd07ffc4139a",
"branch": "master", "branch": "master",
"path": "/color", "path": "/color",
"notests": true "notests": true
@ -196,7 +196,7 @@
"importpath": "github.com/labstack/gommon/log", "importpath": "github.com/labstack/gommon/log",
"repository": "https://github.com/labstack/gommon", "repository": "https://github.com/labstack/gommon",
"vcs": "git", "vcs": "git",
"revision": "f72d3c883f8ea180da8f085dd320804c41332ad1", "revision": "1121fd3e243c202482226a7afe4dcd07ffc4139a",
"branch": "master", "branch": "master",
"path": "/log", "path": "/log",
"notests": true "notests": true
@ -205,7 +205,7 @@
"importpath": "github.com/labstack/gommon/random", "importpath": "github.com/labstack/gommon/random",
"repository": "https://github.com/labstack/gommon", "repository": "https://github.com/labstack/gommon",
"vcs": "git", "vcs": "git",
"revision": "f72d3c883f8ea180da8f085dd320804c41332ad1", "revision": "1121fd3e243c202482226a7afe4dcd07ffc4139a",
"branch": "master", "branch": "master",
"path": "/random", "path": "/random",
"notests": true "notests": true
@ -384,7 +384,7 @@
"importpath": "github.com/sromku/go-gitter", "importpath": "github.com/sromku/go-gitter",
"repository": "https://github.com/sromku/go-gitter", "repository": "https://github.com/sromku/go-gitter",
"vcs": "git", "vcs": "git",
"revision": "0b9f26da1844acd3c6c2fd4ee2bdd30eb1a40568", "revision": "16aadfbb65c6641501ea8eebb6bcf23b9011912b",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },
@ -424,7 +424,7 @@
"importpath": "github.com/valyala/fasttemplate", "importpath": "github.com/valyala/fasttemplate",
"repository": "https://github.com/valyala/fasttemplate", "repository": "https://github.com/valyala/fasttemplate",
"vcs": "git", "vcs": "git",
"revision": "d090d65668a286d9a180d43a19dfdc5dcad8fe88", "revision": "dcecefd839c4193db0d35b88ec65b4c12d360ab0",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },