From 6ebd5cbbd8a941e0bc5f99f0d8e99cfd1d8ac0d7 Mon Sep 17 00:00:00 2001 From: Wim Date: Sun, 10 Feb 2019 17:00:11 +0100 Subject: Refactor and update RocketChat bridge * Add support for editing/deleting messages * Add support for uploading files * Add support for avatars * Use the Rocket.Chat.Go.SDK * Use the rest and streaming api --- bridge/rocketchat/handlers.go | 69 +++ bridge/rocketchat/helpers.go | 198 +++++++ bridge/rocketchat/rocketchat.go | 150 +++-- go.mod | 4 + go.sum | 8 + matterbridge.toml.sample | 17 + vendor/github.com/Jeffail/gabs/LICENSE | 19 + vendor/github.com/Jeffail/gabs/README.md | 315 ++++++++++ vendor/github.com/Jeffail/gabs/gabs.go | 581 ++++++++++++++++++ vendor/github.com/Jeffail/gabs/gabs_logo.png | Bin 0 -> 167771 bytes vendor/github.com/gopackage/ddp/.gitignore | 24 + vendor/github.com/gopackage/ddp/LICENSE | 13 + vendor/github.com/gopackage/ddp/README.md | 3 + vendor/github.com/gopackage/ddp/ddp.go | 79 +++ vendor/github.com/gopackage/ddp/ddp_client.go | 654 +++++++++++++++++++++ vendor/github.com/gopackage/ddp/ddp_collection.go | 245 ++++++++ vendor/github.com/gopackage/ddp/ddp_ejson.go | 217 +++++++ vendor/github.com/gopackage/ddp/ddp_messages.go | 82 +++ vendor/github.com/gopackage/ddp/ddp_stats.go | 321 ++++++++++ .../Rocket.Chat.Go.SDK/models/channel.go | 39 ++ .../matterbridge/Rocket.Chat.Go.SDK/models/info.go | 133 +++++ .../Rocket.Chat.Go.SDK/models/message.go | 75 +++ .../Rocket.Chat.Go.SDK/models/permission.go | 7 + .../Rocket.Chat.Go.SDK/models/setting.go | 21 + .../matterbridge/Rocket.Chat.Go.SDK/models/user.go | 29 + .../Rocket.Chat.Go.SDK/models/userCredentials.go | 10 + .../Rocket.Chat.Go.SDK/realtime/channels.go | 263 +++++++++ .../Rocket.Chat.Go.SDK/realtime/client.go | 96 +++ .../Rocket.Chat.Go.SDK/realtime/emoji.go | 10 + .../Rocket.Chat.Go.SDK/realtime/events.go | 21 + .../Rocket.Chat.Go.SDK/realtime/messages.go | 240 ++++++++ .../Rocket.Chat.Go.SDK/realtime/permissions.go | 54 ++ .../Rocket.Chat.Go.SDK/realtime/settings.go | 53 ++ .../Rocket.Chat.Go.SDK/realtime/subscriptions.go | 41 ++ .../Rocket.Chat.Go.SDK/realtime/users.go | 103 ++++ .../Rocket.Chat.Go.SDK/rest/channels.go | 64 ++ .../matterbridge/Rocket.Chat.Go.SDK/rest/client.go | 176 ++++++ .../Rocket.Chat.Go.SDK/rest/information.go | 98 +++ .../Rocket.Chat.Go.SDK/rest/messages.go | 67 +++ .../matterbridge/Rocket.Chat.Go.SDK/rest/users.go | 145 +++++ vendor/github.com/nelsonken/gomf/README.md | 37 ++ vendor/github.com/nelsonken/gomf/form_builder.go | 89 +++ vendor/github.com/nelsonken/gomf/up.php | 33 ++ vendor/golang.org/x/net/AUTHORS | 3 + vendor/golang.org/x/net/CONTRIBUTORS | 3 + vendor/golang.org/x/net/LICENSE | 27 + vendor/golang.org/x/net/PATENTS | 22 + vendor/golang.org/x/net/websocket/client.go | 106 ++++ vendor/golang.org/x/net/websocket/dial.go | 24 + vendor/golang.org/x/net/websocket/hybi.go | 583 ++++++++++++++++++ vendor/golang.org/x/net/websocket/server.go | 113 ++++ vendor/golang.org/x/net/websocket/websocket.go | 448 ++++++++++++++ vendor/modules.txt | 12 + 53 files changed, 6201 insertions(+), 43 deletions(-) create mode 100644 bridge/rocketchat/handlers.go create mode 100644 bridge/rocketchat/helpers.go create mode 100644 vendor/github.com/Jeffail/gabs/LICENSE create mode 100644 vendor/github.com/Jeffail/gabs/README.md create mode 100644 vendor/github.com/Jeffail/gabs/gabs.go create mode 100644 vendor/github.com/Jeffail/gabs/gabs_logo.png create mode 100644 vendor/github.com/gopackage/ddp/.gitignore create mode 100644 vendor/github.com/gopackage/ddp/LICENSE create mode 100644 vendor/github.com/gopackage/ddp/README.md create mode 100644 vendor/github.com/gopackage/ddp/ddp.go create mode 100644 vendor/github.com/gopackage/ddp/ddp_client.go create mode 100644 vendor/github.com/gopackage/ddp/ddp_collection.go create mode 100644 vendor/github.com/gopackage/ddp/ddp_ejson.go create mode 100644 vendor/github.com/gopackage/ddp/ddp_messages.go create mode 100644 vendor/github.com/gopackage/ddp/ddp_stats.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/channel.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/info.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/message.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/permission.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/setting.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/user.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/userCredentials.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/channels.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/client.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/emoji.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/events.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/messages.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/permissions.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/settings.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/subscriptions.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/users.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/channels.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/client.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/information.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/messages.go create mode 100644 vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/users.go create mode 100644 vendor/github.com/nelsonken/gomf/README.md create mode 100644 vendor/github.com/nelsonken/gomf/form_builder.go create mode 100644 vendor/github.com/nelsonken/gomf/up.php create mode 100644 vendor/golang.org/x/net/AUTHORS create mode 100644 vendor/golang.org/x/net/CONTRIBUTORS create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/websocket/client.go create mode 100644 vendor/golang.org/x/net/websocket/dial.go create mode 100644 vendor/golang.org/x/net/websocket/hybi.go create mode 100644 vendor/golang.org/x/net/websocket/server.go create mode 100644 vendor/golang.org/x/net/websocket/websocket.go diff --git a/bridge/rocketchat/handlers.go b/bridge/rocketchat/handlers.go new file mode 100644 index 00000000..b44ea46f --- /dev/null +++ b/bridge/rocketchat/handlers.go @@ -0,0 +1,69 @@ +package brocketchat + +import ( + "github.com/42wim/matterbridge/bridge/config" +) + +func (b *Brocketchat) handleRocket() { + messages := make(chan *config.Message) + if b.GetString("WebhookBindAddress") != "" { + b.Log.Debugf("Choosing webhooks based receiving") + go b.handleRocketHook(messages) + } else { + b.Log.Debugf("Choosing login/password based receiving") + go b.handleRocketClient(messages) + } + for message := range messages { + message.Account = b.Account + b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) + b.Log.Debugf("<= Message is %#v", message) + b.Remote <- *message + } +} + +func (b *Brocketchat) handleRocketHook(messages chan *config.Message) { + for { + message := b.rh.Receive() + b.Log.Debugf("Receiving from rockethook %#v", message) + // do not loop + if message.UserName == b.GetString("Nick") { + continue + } + messages <- &config.Message{ + UserID: message.UserID, + Username: message.UserName, + Text: message.Text, + Channel: message.ChannelName, + } + } +} + +func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { + for message := range b.messageChan { + b.Log.Debugf("message %#v", message) + m := message + if b.skipMessage(&m) { + b.Log.Debugf("Skipped message: %#v", message) + continue + } + + rmsg := &config.Message{Text: message.Msg, + Username: message.User.UserName, + Channel: b.getChannelName(message.RoomID), + Account: b.Account, + UserID: message.User.ID, + ID: message.ID, + } + messages <- rmsg + } +} + +func (b *Brocketchat) handleUploadFile(msg *config.Message) error { + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + if err := b.uploadFile(&fi, b.getChannelID(msg.Channel)); err != nil { + return err + } + } + return nil +} diff --git a/bridge/rocketchat/helpers.go b/bridge/rocketchat/helpers.go new file mode 100644 index 00000000..c6bd2dd8 --- /dev/null +++ b/bridge/rocketchat/helpers.go @@ -0,0 +1,198 @@ +package brocketchat + +import ( + "context" + "io/ioutil" + "mime" + "net/http" + "net/url" + "strings" + "time" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/42wim/matterbridge/hook/rockethook" + "github.com/42wim/matterbridge/matterhook" + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" + "github.com/matterbridge/Rocket.Chat.Go.SDK/realtime" + "github.com/matterbridge/Rocket.Chat.Go.SDK/rest" + "github.com/nelsonken/gomf" +) + +func (b *Brocketchat) doConnectWebhookBind() error { + switch { + case b.GetString("WebhookURL") != "": + b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") + b.mh = matterhook.New(b.GetString("WebhookURL"), + matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), + DisableServer: true}) + b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")}) + case b.GetString("Login") != "": + b.Log.Info("Connecting using login/password (sending)") + err := b.apiLogin() + if err != nil { + return err + } + default: + b.Log.Info("Connecting using webhookbindaddress (receiving)") + b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")}) + } + return nil +} + +func (b *Brocketchat) doConnectWebhookURL() error { + b.Log.Info("Connecting using webhookurl (sending)") + b.mh = matterhook.New(b.GetString("WebhookURL"), + matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), + DisableServer: true}) + if b.GetString("Login") != "" { + b.Log.Info("Connecting using login/password (receiving)") + err := b.apiLogin() + if err != nil { + return err + } + } + return nil +} + +func (b *Brocketchat) apiLogin() error { + b.Log.Debugf("handling apiLogin()") + credentials := &models.UserCredentials{Email: b.GetString("login"), Password: b.GetString("password")} + myURL, err := url.Parse(b.GetString("server")) + if err != nil { + return err + } + client, err := realtime.NewClient(myURL, b.GetBool("debug")) + b.c = client + if err != nil { + return err + } + restclient := rest.NewClient(myURL, b.GetBool("debug")) + user, err := b.c.Login(credentials) + if err != nil { + return err + } + b.user = user + b.r = restclient + err = b.r.Login(credentials) + if err != nil { + return err + } + b.Log.Info("Connection succeeded") + return nil +} + +func (b *Brocketchat) getChannelName(id string) string { + b.RLock() + defer b.RUnlock() + if name, ok := b.channelMap[id]; ok { + return name + } + return "" +} + +func (b *Brocketchat) getChannelID(name string) string { + b.RLock() + defer b.RUnlock() + for k, v := range b.channelMap { + if v == name { + return k + } + } + return "" +} + +func (b *Brocketchat) skipMessage(message *models.Message) bool { + return message.User.ID == b.user.ID +} + +func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error { + fb := gomf.New() + if err := fb.WriteField("description", fi.Comment); err != nil { + return err + } + sp := strings.Split(fi.Name, ".") + mtype := mime.TypeByExtension("." + sp[len(sp)-1]) + if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") { + return nil + } + if err := fb.WriteFile("file", fi.Name, mtype, *fi.Data); err != nil { + return err + } + req, err := fb.GetHTTPRequest(context.TODO(), b.GetString("server")+"/api/v1/rooms.upload/"+channel) + if err != nil { + return err + } + req.Header.Add("X-Auth-Token", b.user.Token) + req.Header.Add("X-User-Id", b.user.ID) + client := &http.Client{ + Timeout: time.Second * 5, + } + resp, err := client.Do(req) + if err != nil { + return err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode != 200 { + b.Log.Errorf("failed: %#v", string(body)) + } + return nil +} + +// sendWebhook uses the configured WebhookURL to send the message +func (b *Brocketchat) sendWebhook(msg *config.Message) error { + // skip events + if msg.Event != "" { + return nil + } + + if b.GetBool("PrefixMessagesWithNick") { + msg.Text = msg.Username + msg.Text + } + if msg.Extra != nil { + // this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE + for _, rmsg := range helper.HandleExtra(msg, b.General) { + rmsg := rmsg // scopelint + iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl")) + matterMessage := matterhook.OMessage{ + IconURL: iconURL, + Channel: rmsg.Channel, + UserName: rmsg.Username, + Text: rmsg.Text, + Props: make(map[string]interface{}), + } + if err := b.mh.Send(matterMessage); err != nil { + b.Log.Errorf("sendWebhook failed: %s ", err) + } + } + + // webhook doesn't support file uploads, so we add the url manually + if len(msg.Extra["file"]) > 0 { + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + if fi.URL != "" { + msg.Text += fi.URL + } + } + } + } + iconURL := config.GetIconURL(msg, b.GetString("iconurl")) + matterMessage := matterhook.OMessage{ + IconURL: iconURL, + Channel: msg.Channel, + UserName: msg.Username, + Text: msg.Text, + } + if msg.Avatar != "" { + matterMessage.IconURL = msg.Avatar + } + err := b.mh.Send(matterMessage) + if err != nil { + b.Log.Info(err) + return err + } + return nil +} diff --git a/bridge/rocketchat/rocketchat.go b/bridge/rocketchat/rocketchat.go index 1dbc7be0..82b6627d 100644 --- a/bridge/rocketchat/rocketchat.go +++ b/bridge/rocketchat/rocketchat.go @@ -1,21 +1,37 @@ package brocketchat import ( + "errors" + "sync" + "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/hook/rockethook" "github.com/42wim/matterbridge/matterhook" + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" + "github.com/matterbridge/Rocket.Chat.Go.SDK/realtime" + "github.com/matterbridge/Rocket.Chat.Go.SDK/rest" ) type Brocketchat struct { mh *matterhook.Client rh *rockethook.Client + c *realtime.Client + r *rest.Client *bridge.Config + messageChan chan models.Message + channelMap map[string]string + user *models.User + sync.RWMutex } func New(cfg *bridge.Config) bridge.Bridger { - return &Brocketchat{Config: cfg} + b := &Brocketchat{Config: cfg} + b.messageChan = make(chan models.Message) + b.channelMap = make(map[string]string) + b.Log.Debugf("enabling rocketchat") + return b } func (b *Brocketchat) Command(cmd string) string { @@ -23,70 +39,118 @@ func (b *Brocketchat) Command(cmd string) string { } func (b *Brocketchat) Connect() error { - b.Log.Info("Connecting webhooks") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - DisableServer: true}) - b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")}) - go b.handleRocketHook() + if b.GetString("WebhookBindAddress") != "" { + if err := b.doConnectWebhookBind(); err != nil { + return err + } + go b.handleRocket() + return nil + } + switch { + case b.GetString("WebhookURL") != "": + if err := b.doConnectWebhookURL(); err != nil { + return err + } + go b.handleRocket() + return nil + case b.GetString("Login") != "": + b.Log.Info("Connecting using login/password (sending and receiving)") + err := b.apiLogin() + if err != nil { + return err + } + go b.handleRocket() + } + if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && + b.GetString("Login") == "" { + return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Login/Password/Server configured") + } return nil } func (b *Brocketchat) Disconnect() error { return nil - } func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error { + if b.c == nil { + return nil + } + id, err := b.c.GetChannelId(channel.Name) + if err != nil { + return err + } + b.Lock() + b.channelMap[id] = channel.Name + b.Unlock() + mychannel := &models.Channel{ID: id, Name: channel.Name} + if err := b.c.JoinChannel(id); err != nil { + return err + } + if err := b.c.SubscribeToMessageStream(mychannel, b.messageChan); err != nil { + return err + } return nil } func (b *Brocketchat) Send(msg config.Message) (string, error) { - // ignore delete messages + channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel} + + // Delete message if msg.Event == config.EventMsgDelete { - return "", nil + if msg.ID == "" { + return "", nil + } + return msg.ID, b.c.DeleteMessage(&models.Message{ID: msg.ID}) } - b.Log.Debugf("=> Receiving %#v", msg) + + // Use webhook to send the message + if b.GetString("WebhookURL") != "" { + return "", b.sendWebhook(&msg) + } + + // Prepend nick if configured + if b.GetBool("PrefixMessagesWithNick") { + msg.Text = msg.Username + msg.Text + } + + // Edit message if we have an ID + if msg.ID != "" { + return msg.ID, b.c.EditMessage(&models.Message{ID: msg.ID, Msg: msg.Text, RoomID: b.getChannelID(msg.Channel)}) + } + + // Upload a file if it exists if msg.Extra != nil { for _, rmsg := range helper.HandleExtra(&msg, b.General) { - rmsg := rmsg // scopelint - iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl")) - matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text} - b.mh.Send(matterMessage) + smsg := &models.Message{ + RoomID: b.getChannelID(rmsg.Channel), + Msg: rmsg.Username + rmsg.Text, + PostMessage: models.PostMessage{ + Avatar: rmsg.Avatar, + Alias: rmsg.Username, + }, + } + if _, err := b.c.SendMessage(smsg); err != nil { + b.Log.Errorf("SendMessage failed: %s", err) + } } if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.URL != "" { - msg.Text += fi.URL - } - } + return "", b.handleUploadFile(&msg) } } - iconURL := config.GetIconURL(&msg, b.GetString("iconurl")) - matterMessage := matterhook.OMessage{IconURL: iconURL} - matterMessage.Channel = msg.Channel - matterMessage.UserName = msg.Username - matterMessage.Type = "" - matterMessage.Text = msg.Text - err := b.mh.Send(matterMessage) - if err != nil { - b.Log.Info(err) - return "", err + smsg := &models.Message{ + RoomID: channel.ID, + Msg: msg.Text, + PostMessage: models.PostMessage{ + Avatar: msg.Avatar, + Alias: msg.Username, + }, } - return "", nil -} -func (b *Brocketchat) handleRocketHook() { - for { - message := b.rh.Receive() - b.Log.Debugf("Receiving from rockethook %#v", message) - // do not loop - if message.UserName == b.GetString("Nick") { - continue - } - b.Log.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, UserID: message.UserID} + rmsg, err := b.c.SendMessage(smsg) + if rmsg == nil { + return "", err } + return rmsg.ID, err } diff --git a/go.mod b/go.mod index 1f8cb17f..1320b25a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/42wim/matterbridge require ( github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 // indirect + github.com/Jeffail/gabs v1.1.1 // indirect github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 github.com/bwmarrin/discordgo v0.19.0 github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec @@ -10,6 +11,7 @@ require ( github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc // indirect github.com/google/gops v0.3.5 + github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect github.com/gorilla/schema v1.0.2 github.com/gorilla/websocket v1.4.0 @@ -23,6 +25,7 @@ require ( github.com/lrstanley/girc v0.0.0-20190102153329-c1e59a02f488 github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect + github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544 @@ -31,6 +34,7 @@ require ( github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect + github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 github.com/nicksnyder/go-i18n v1.4.0 // indirect github.com/nlopes/slack v0.5.0 github.com/onsi/ginkgo v1.6.0 // indirect diff --git a/go.sum b/go.sum index c5aca265..e10bbb3a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 h1:IZtuWGfzQnKnCSu github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557/go.mod h1:jL0YSXMs/txjtGJ4PWrmETOk6KUHMDPMshgQZlTeB3Y= github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 h1:v/zr4ns/4sSahF9KBm4Uc933bLsEEv7LuT63CJ019yo= github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E= +github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 h1:xZBoq249G9MSt+XuY7sVQzcfONJ6IQuwpCK+KAaOpnY= github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg= github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58 h1:MkpmYfld/S8kXqTYI68DfL8/hHXjHogL120Dy00TIxc= @@ -27,6 +29,8 @@ github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc h1:wdhDSKrkYy24mcf github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/gops v0.3.5 h1:SIWvPLiYvy5vMwjxB3rVFTE4QBhUFj2KKWr3Xm7CKhw= github.com/google/gops v0.3.5/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0= +github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbVkFXCXHz1QPhVXcHnQKAzBTPfQo= +github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4/go.mod h1:lEO7XoHJ/xNRBCxrn4h/CEB67h0kW1B0t4ooP2yrjUA= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA= @@ -66,6 +70,8 @@ github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408 github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d h1:F+Sr+C0ojSlYQ37BLylQtSFmyQULe3jbAygcyXQ9mVs= +github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A= github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 h1:KzDEcy8eDbTx881giW8a6llsAck3e2bJvMyKvh1IK+k= github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91/go.mod h1:ECDRehsR9TYTKCAsRS8/wLeOk6UUqDydw47ln7wG41Q= github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea h1:kaADGqpK4gGO2BpzEyJrBxq2Jc57Rsar4i2EUxcACUc= @@ -88,6 +94,8 @@ github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 h1:oKIteT github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g= github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E= +github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 h1:mp6tU1r0xLostUGLkTspf/9/AiHuVD7ptyXhySkDEsE= +github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9/go.mod h1:A5SRAcpTemjGgIuBq6Kic2yHcoeUFWUinOAlMP/i9xo= github.com/nicksnyder/go-i18n v1.4.0 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8E+I= github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0= diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index 42d20af7..77f5b51e 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -893,6 +893,21 @@ ShowTopicChange=false #REQUIRED [rocketchat.rockme] +#The rocketchat hostname. (prefix it with http or https) +#REQUIRED (when not using webhooks) +Server="https://yourrocketchatserver.domain.com:443" + +#login/pass of your bot. +#Use a dedicated user for this and not your own! +#REQUIRED (when not using webhooks) +Login="yourlogin" +Password="yourpass" + +#### Settings for webhook matterbridge. +#USE DEDICATED BOT USER WHEN POSSIBLE! This allows you to use advanced features like message editing/deleting and uploads +#You don't need to configure this, if you have configured the settings +#above. + #Url is your incoming webhook url as specified in rocketchat #Read #https://rocket.chat/docs/administrator-guides/integrations/#how-to-create-a-new-incoming-webhook #See administration - integrations - new integration - incoming webhook @@ -917,6 +932,8 @@ NoTLS=false #OPTIONAL (default false) SkipTLSVerify=true +#### End settings for webhook matterbridge. + ## RELOADABLE SETTINGS ## Settings below can be reloaded by editing the file diff --git a/vendor/github.com/Jeffail/gabs/LICENSE b/vendor/github.com/Jeffail/gabs/LICENSE new file mode 100644 index 00000000..99a62c62 --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Ashley Jeffs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/Jeffail/gabs/README.md b/vendor/github.com/Jeffail/gabs/README.md new file mode 100644 index 00000000..a58193fd --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/README.md @@ -0,0 +1,315 @@ +![Gabs](gabs_logo.png "Gabs") + +Gabs is a small utility for dealing with dynamic or unknown JSON structures in +golang. It's pretty much just a helpful wrapper around the golang +`json.Marshal/json.Unmarshal` behaviour and `map[string]interface{}` objects. +It does nothing spectacular except for being fabulous. + +https://godoc.org/github.com/Jeffail/gabs + +## How to install: + +``` bash +go get github.com/Jeffail/gabs +``` + +## How to use + +### Parsing and searching JSON + +``` go +... + +import "github.com/Jeffail/gabs" + +jsonParsed, err := gabs.ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value1":10, + "value2":22 + }, + "alsoInner":{ + "value1":20 + } + } +}`)) + +var value float64 +var ok bool + +value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64) +// value == 10.0, ok == true + +value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64) +// value == 10.0, ok == true + +value, ok = jsonParsed.Path("does.not.exist").Data().(float64) +// value == 0.0, ok == false + +exists := jsonParsed.Exists("outter", "inner", "value1") +// exists == true + +exists := jsonParsed.Exists("does", "not", "exist") +// exists == false + +exists := jsonParsed.ExistsP("does.not.exist") +// exists == false + +... +``` + +### Iterating objects + +``` go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"object":{ "first": 1, "second": 2, "third": 3 }}`)) + +// S is shorthand for Search +children, _ := jsonParsed.S("object").ChildrenMap() +for key, child := range children { + fmt.Printf("key: %v, value: %v\n", key, child.Data().(string)) +} + +... +``` + +### Iterating arrays + +``` go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`)) + +// S is shorthand for Search +children, _ := jsonParsed.S("array").Children() +for _, child := range children { + fmt.Println(child.Data().(string)) +} + +... +``` + +Will print: + +``` +first +second +third +``` + +Children() will return all children of an array in order. This also works on +objects, however, the children will be returned in a random order. + +### Searching through arrays + +If your JSON structure contains arrays you can still search the fields of the +objects within the array, this returns a JSON array containing the results for +each element. + +``` go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ {"value":1}, {"value":2}, {"value":3} ]}`)) +fmt.Println(jsonParsed.Path("array.value").String()) + +... +``` + +Will print: + +``` +[1,2,3] +``` + +### Generating JSON + +``` go +... + +jsonObj := gabs.New() +// or gabs.Consume(jsonObject) to work on an existing map[string]interface{} + +jsonObj.Set(10, "outter", "inner", "value") +jsonObj.SetP(20, "outter.inner.value2") +jsonObj.Set(30, "outter", "inner2", "value3") + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"outter":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}} +``` + +To pretty-print: + +``` go +... + +fmt.Println(jsonObj.StringIndent("", " ")) + +... +``` + +Will print: + +``` +{ + "outter": { + "inner": { + "value": 10, + "value2": 20 + }, + "inner2": { + "value3": 30 + } + } +} +``` + +### Generating Arrays + +``` go +... + +jsonObj := gabs.New() + +jsonObj.Array("foo", "array") +// Or .ArrayP("foo.array") + +jsonObj.ArrayAppend(10, "foo", "array") +jsonObj.ArrayAppend(20, "foo", "array") +jsonObj.ArrayAppend(30, "foo", "array") + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"foo":{"array":[10,20,30]}} +``` + +Working with arrays by index: + +``` go +... + +jsonObj := gabs.New() + +// Create an array with the length of 3 +jsonObj.ArrayOfSize(3, "foo") + +jsonObj.S("foo").SetIndex("test1", 0) +jsonObj.S("foo").SetIndex("test2", 1) + +// Create an embedded array with the length of 3 +jsonObj.S("foo").ArrayOfSizeI(3, 2) + +jsonObj.S("foo").Index(2).SetIndex(1, 0) +jsonObj.S("foo").Index(2).SetIndex(2, 1) +jsonObj.S("foo").Index(2).SetIndex(3, 2) + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"foo":["test1","test2",[1,2,3]]} +``` + +### Converting back to JSON + +This is the easiest part: + +``` go +... + +jsonParsedObj, _ := gabs.ParseJSON([]byte(`{ + "outter":{ + "values":{ + "first":10, + "second":11 + } + }, + "outter2":"hello world" +}`)) + +jsonOutput := jsonParsedObj.String() +// Becomes `{"outter":{"values":{"first":10,"second":11}},"outter2":"hello world"}` + +... +``` + +And to serialize a specific segment is as simple as: + +``` go +... + +jsonParsedObj := gabs.ParseJSON([]byte(`{ + "outter":{ + "values":{ + "first":10, + "second":11 + } + }, + "outter2":"hello world" +}`)) + +jsonOutput := jsonParsedObj.Search("outter").String() +// Becomes `{"values":{"first":10,"second":11}}` + +... +``` + +### Merge two containers + +You can merge a JSON structure into an existing one, where collisions will be +converted into a JSON array. + +``` go +jsonParsed1, _ := ParseJSON([]byte(`{"outter": {"value1": "one"}}`)) +jsonParsed2, _ := ParseJSON([]byte(`{"outter": {"inner": {"value3": "three"}}, "outter2": {"value2": "two"}}`)) + +jsonParsed1.Merge(jsonParsed2) +// Becomes `{"outter":{"inner":{"value3":"three"},"value1":"one"},"outter2":{"value2":"two"}}` +``` + +Arrays are merged: + +``` go +jsonParsed1, _ := ParseJSON([]byte(`{"array": ["one"]}`)) +jsonParsed2, _ := ParseJSON([]byte(`{"array": ["two"]}`)) + +jsonParsed1.Merge(jsonParsed2) +// Becomes `{"array":["one", "two"]}` +``` + +### Parsing Numbers + +Gabs uses the `json` package under the bonnet, which by default will parse all +number values into `float64`. If you need to parse `Int` values then you should +use a `json.Decoder` (https://golang.org/pkg/encoding/json/#Decoder): + +``` go +sample := []byte(`{"test":{"int":10, "float":6.66}}`) +dec := json.NewDecoder(bytes.NewReader(sample)) +dec.UseNumber() + +val, err := gabs.ParseJSONDecoder(dec) +if err != nil { + t.Errorf("Failed to parse: %v", err) + return +} + +intValue, err := val.Path("test.int").Data().(json.Number).Int64() +``` diff --git a/vendor/github.com/Jeffail/gabs/gabs.go b/vendor/github.com/Jeffail/gabs/gabs.go new file mode 100644 index 00000000..a21a79d7 --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/gabs.go @@ -0,0 +1,581 @@ +/* +Copyright (c) 2014 Ashley Jeffs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Package gabs implements a simplified wrapper around creating and parsing JSON. +package gabs + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "io/ioutil" + "strings" +) + +//-------------------------------------------------------------------------------------------------- + +var ( + // ErrOutOfBounds - Index out of bounds. + ErrOutOfBounds = errors.New("out of bounds") + + // ErrNotObjOrArray - The target is not an object or array type. + ErrNotObjOrArray = errors.New("not an object or array") + + // ErrNotObj - The target is not an object type. + ErrNotObj = errors.New("not an object") + + // ErrNotArray - The target is not an array type. + ErrNotArray = errors.New("not an array") + + // ErrPathCollision - Creating a path failed because an element collided with an existing value. + ErrPathCollision = errors.New("encountered value collision whilst building path") + + // ErrInvalidInputObj - The input value was not a map[string]interface{}. + ErrInvalidInputObj = errors.New("invalid input object") + + // ErrInvalidInputText - The input data could not be parsed. + ErrInvalidInputText = errors.New("input text could not be parsed") + + // ErrInvalidPath - The filepath was not valid. + ErrInvalidPath = errors.New("invalid file path") + + // ErrInvalidBuffer - The input buffer contained an invalid JSON string + ErrInvalidBuffer = errors.New("input buffer contained invalid JSON") +) + +//-------------------------------------------------------------------------------------------------- + +// Container - an internal structure that holds a reference to the core interface map of the parsed +// json. Use this container to move context. +type Container struct { + object interface{} +} + +// Data - Return the contained data as an interface{}. +func (g *Container) Data() interface{} { + if g == nil { + return nil + } + return g.object +} + +//-------------------------------------------------------------------------------------------------- + +// Path - Search for a value using dot notation. +func (g *Container) Path(path string) *Container { + return g.Search(strings.Split(path, ".")...) +} + +// Search - Attempt to find and return an object within the JSON structure by specifying the +// hierarchy of field names to locate the target. If the search encounters an array and has not +// reached the end target then it will iterate each object of the array for the target and return +// all of the results in a JSON array. +func (g *Container) Search(hierarchy ...string) *Container { + var object interface{} + + object = g.Data() + for target := 0; target < len(hierarchy); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + object, ok = mmap[hierarchy[target]] + if !ok { + return nil + } + } else if marray, ok := object.([]interface{}); ok { + tmpArray := []interface{}{} + for _, val := range marray { + tmpGabs := &Container{val} + res := tmpGabs.Search(hierarchy[target:]...) + if res != nil { + tmpArray = append(tmpArray, res.Data()) + } + } + if len(tmpArray) == 0 { + return nil + } + return &Container{tmpArray} + } else { + return nil + } + } + return &Container{object} +} + +// S - Shorthand method, does the same thing as Search. +func (g *Container) S(hierarchy ...string) *Container { + return g.Search(hierarchy...) +} + +// Exists - Checks whether a path exists. +func (g *Container) Exists(hierarchy ...string) bool { + return g.Search(hierarchy...) != nil +} + +// ExistsP - Checks whether a dot notation path exists. +func (g *Container) ExistsP(path string) bool { + return g.Exists(strings.Split(path, ".")...) +} + +// Index - Attempt to find and return an object within a JSON array by index. +func (g *Container) Index(index int) *Container { + if array, ok := g.Data().([]interface{}); ok { + if index >= len(array) { + return &Container{nil} + } + return &Container{array[index]} + } + return &Container{nil} +} + +// Children - Return a slice of all the children of the array. This also works for objects, however, +// the children returned for an object will NOT be in order and you lose the names of the returned +// objects this way. +func (g *Container) Children() ([]*Container, error) { + if array, ok := g.Data().([]interface{}); ok { + children := make([]*Container, len(array)) + for i := 0; i < len(array); i++ { + children[i] = &Container{array[i]} + } + return children, nil + } + if mmap, ok := g.Data().(map[string]interface{}); ok { + children := []*Container{} + for _, obj := range mmap { + children = append(children, &Container{obj}) + } + return children, nil + } + return nil, ErrNotObjOrArray +} + +// ChildrenMap - Return a map of all the children of an object. +func (g *Container) ChildrenMap() (map[string]*Container, error) { + if mmap, ok := g.Data().(map[string]interface{}); ok { + children := map[string]*Container{} + for name, obj := range mmap { + children[name] = &Container{obj} + } + return children, nil + } + return nil, ErrNotObj +} + +//-------------------------------------------------------------------------------------------------- + +// Set - Set the value of a field at a JSON path, any parts of the path that do not exist will be +// constructed, and if a collision occurs with a non object type whilst iterating the path an error +// is returned. +func (g *Container) Set(value interface{}, path ...string) (*Container, error) { + if len(path) == 0 { + g.object = value + return g, nil + } + var object interface{} + if g.object == nil { + g.object = map[string]interface{}{} + } + object = g.object + for target := 0; target < len(path); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + if target == len(path)-1 { + mmap[path[target]] = value + } else if mmap[path[target]] == nil { + mmap[path[target]] = map[string]interface{}{} + } + object = mmap[path[target]] + } else { + return &Container{nil}, ErrPathCollision + } + } + return &Container{object}, nil +} + +// SetP - Does the same as Set, but using a dot notation JSON path. +func (g *Container) SetP(value interface{}, path string) (*Container, error) { + return g.Set(value, strings.Split(path, ".")...) +} + +// SetIndex - Set a value of an array element based on the index. +func (g *Container) SetIndex(value interface{}, index int) (*Container, error) { + if array, ok := g.Data().([]interface{}); ok { + if index >= len(array) { + return &Container{nil}, ErrOutOfBounds + } + array[index] = value + return &Container{array[index]}, nil + } + return &Container{nil}, ErrNotArray +} + +// Object - Create a new JSON object at a path. Returns an error if the path contains a collision +// with a non object type. +func (g *Container) Object(path ...string) (*Container, error) { + return g.Set(map[string]interface{}{}, path...) +} + +// ObjectP - Does the same as Object, but using a dot notation JSON path. +func (g *Container) ObjectP(path string) (*Container, error) { + return g.Object(strings.Split(path, ".")...) +} + +// ObjectI - Create a new JSON object at an array index. Returns an error if the object is not an +// array or the index is out of bounds. +func (g *Container) ObjectI(index int) (*Container, error) { + return g.SetIndex(map[string]interface{}{}, index) +} + +// Array - Create a new JSON array at a path. Returns an error if the path contains a collision with +// a non object type. +func (g *Container) Array(path ...string) (*Container, error) { + return g.Set([]interface{}{}, path...) +} + +// ArrayP - Does the same as Array, but using a dot notation JSON path. +func (g *Container) ArrayP(path string) (*Container, error) { + return g.Array(strings.Split(path, ".")...) +} + +// ArrayI - Create a new JSON array at an array index. Returns an error if the object is not an +// array or the index is out of bounds. +func (g *Container) ArrayI(index int) (*Container, error) { + return g.SetIndex([]interface{}{}, index) +} + +// ArrayOfSize - Create a new JSON array of a particular size at a path. Returns an error if the +// path contains a collision with a non object type. +func (g *Container) ArrayOfSize(size int, path ...string) (*Container, error) { + a := make([]interface{}, size) + return g.Set(a, path...) +} + +// ArrayOfSizeP - Does the same as ArrayOfSize, but using a dot notation JSON path. +func (g *Container) ArrayOfSizeP(size int, path string) (*Container, error) { + return g.ArrayOfSize(size, strings.Split(path, ".")...) +} + +// ArrayOfSizeI - Create a new JSON array of a particular size at an array index. Returns an error +// if the object is not an array or the index is out of bounds. +func (g *Container) ArrayOfSizeI(size, index int) (*Container, error) { + a := make([]interface{}, size) + return g.SetIndex(a, index) +} + +// Delete - Delete an element at a JSON path, an error is returned if the element does not exist. +func (g *Container) Delete(path ...string) error { + var object interface{} + + if g.object == nil { + return ErrNotObj + } + object = g.object + for target := 0; target < len(path); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + if target == len(path)-1 { + if _, ok := mmap[path[target]]; ok { + delete(mmap, path[target]) + } else { + return ErrNotObj + } + } + object = mmap[path[target]] + } else { + return ErrNotObj + } + } + return nil +} + +// DeleteP - Does the same as Delete, but using a dot notation JSON path. +func (g *Container) DeleteP(path string) error { + return g.Delete(strings.Split(path, ".")...) +} + +// Merge - Merges two gabs-containers +func (g *Container) Merge(toMerge *Container) error { + var recursiveFnc func(map[string]interface{}, []string) error + recursiveFnc = func(mmap map[string]interface{}, path []string) error { + for key, value := range mmap { + newPath := append(path, key) + if g.Exists(newPath...) { + target := g.Search(newPath...) + switch t := value.(type) { + case map[string]interface{}: + switch targetV := target.Data().(type) { + case map[string]interface{}: + if err := recursiveFnc(t, newPath); err != nil { + return err + } + case []interface{}: + g.Set(append(targetV, t), newPath...) + default: + newSlice := append([]interface{}{}, targetV) + g.Set(append(newSlice, t), newPath...) + } + case []interface{}: + for _, valueOfSlice := range t { + if err := g.ArrayAppend(valueOfSlice, newPath...); err != nil { + return err + } + } + default: + switch targetV := target.Data().(type) { + case []interface{}: + g.Set(append(targetV, t), newPath...) + default: + newSlice := append([]interface{}{}, targetV) + g.Set(append(newSlice, t), newPath...) + } + } + } else { + // path doesn't exist. So set the value + if _, err := g.Set(value, newPath...); err != nil { + return err + } + } + } + return nil + } + if mmap, ok := toMerge.Data().(map[string]interface{}); ok { + return recursiveFnc(mmap, []string{}) + } + return nil +} + +//-------------------------------------------------------------------------------------------------- + +/* +Array modification/search - Keeping these options simple right now, no need for anything more +complicated since you can just cast to []interface{}, modify and then reassign with Set. +*/ + +// ArrayAppend - Append a value onto a JSON array. If the target is not a JSON array then it will be +// converted into one, with its contents as the first element of the array. +func (g *Container) ArrayAppend(value interface{}, path ...string) error { + if array, ok := g.Search(path...).Data().([]interface{}); ok { + array = append(array, value) + _, err := g.Set(array, path...) + return err + } + + newArray := []interface{}{} + if d := g.Search(path...).Data(); d != nil { + newArray = append(newArray, d) + } + newArray = append(newArray, value) + + _, err := g.Set(newArray, path...) + return err +} + +// ArrayAppendP - Append a value onto a JSON array using a dot notation JSON path. +func (g *Container) ArrayAppendP(value interface{}, path string) error { + return g.ArrayAppend(value, strings.Split(path, ".")...) +} + +// ArrayRemove - Remove an element from a JSON array. +func (g *Container) ArrayRemove(index int, path ...string) error { + if index < 0 { + return ErrOutOfBounds + } + array, ok := g.Search(path...).Data().([]interface{}) + if !ok { + return ErrNotArray + } + if index < len(array) { + array = append(array[:index], array[index+1:]...) + } else { + return ErrOutOfBounds + } + _, err := g.Set(array, path...) + return err +} + +// ArrayRemoveP - Remove an element from a JSON array using a dot notation JSON path. +func (g *Container) ArrayRemoveP(index int, path string) error { + return g.ArrayRemove(index, strings.Split(path, ".")...) +} + +// ArrayElement - Access an element from a JSON array. +func (g *Container) ArrayElement(index int, path ...string) (*Container, error) { + if index < 0 { + return &Container{nil}, ErrOutOfBounds + } + array, ok := g.Search(path...).Data().([]interface{}) + if !ok { + return &Container{nil}, ErrNotArray + } + if index < len(array) { + return &Container{array[index]}, nil + } + return &Container{nil}, ErrOutOfBounds +} + +// ArrayElementP - Access an element from a JSON array using a dot notation JSON path. +func (g *Container) ArrayElementP(index int, path string) (*Container, error) { + return g.ArrayElement(index, strings.Split(path, ".")...) +} + +// ArrayCount - Count the number of elements in a JSON array. +func (g *Container) ArrayCount(path ...string) (int, error) { + if array, ok := g.Search(path...).Data().([]interface{}); ok { + return len(array), nil + } + return 0, ErrNotArray +} + +// ArrayCountP - Count the number of elements in a JSON array using a dot notation JSON path. +func (g *Container) ArrayCountP(path string) (int, error) { + return g.ArrayCount(strings.Split(path, ".")...) +} + +//-------------------------------------------------------------------------------------------------- + +// Bytes - Converts the contained object back to a JSON []byte blob. +func (g *Container) Bytes() []byte { + if g.Data() != nil { + if bytes, err := json.Marshal(g.object); err == nil { + return bytes + } + } + return []byte("{}") +} + +// BytesIndent - Converts the contained object to a JSON []byte blob formatted with prefix, indent. +func (g *Container) BytesIndent(prefix string, indent string) []byte { + if g.object != nil { + if bytes, err := json.MarshalIndent(g.object, prefix, indent); err == nil { + return bytes + } + } + return []byte("{}") +} + +// String - Converts the contained object to a JSON formatted string. +func (g *Container) String() string { + return string(g.Bytes()) +} + +// StringIndent - Converts the contained object back to a JSON formatted string with prefix, indent. +func (g *Container) StringIndent(prefix string, indent string) string { + return string(g.BytesIndent(prefix, indent)) +} + +// EncodeOpt is a functional option for the EncodeJSON method. +type EncodeOpt func(e *json.Encoder) + +// EncodeOptHTMLEscape sets the encoder to escape the JSON for html. +func EncodeOptHTMLEscape(doEscape bool) EncodeOpt { + return func(e *json.Encoder) { + e.SetEscapeHTML(doEscape) + } +} + +// EncodeOptIndent sets the encoder to indent the JSON output. +func EncodeOptIndent(prefix string, indent string) EncodeOpt { + return func(e *json.Encoder) { + e.SetIndent(prefix, indent) + } +} + +// EncodeJSON - Encodes the contained object back to a JSON formatted []byte +// using a variant list of modifier functions for the encoder being used. +// Functions for modifying the output are prefixed with EncodeOpt, e.g. +// EncodeOptHTMLEscape. +func (g *Container) EncodeJSON(encodeOpts ...EncodeOpt) []byte { + var b bytes.Buffer + encoder := json.NewEncoder(&b) + encoder.SetEscapeHTML(false) // Do not escape by default. + for _, opt := range encodeOpts { + opt(encoder) + } + if err := encoder.Encode(g.object); err != nil { + return []byte("{}") + } + result := b.Bytes() + if len(result) > 0 { + result = result[:len(result)-1] + } + return result +} + +// New - Create a new gabs JSON object. +func New() *Container { + return &Container{map[string]interface{}{}} +} + +// Consume - Gobble up an already converted JSON object, or a fresh map[string]interface{} object. +func Consume(root interface{}) (*Container, error) { + return &Container{root}, nil +} + +// ParseJSON - Convert a string into a representation of the parsed JSON. +func ParseJSON(sample []byte) (*Container, error) { + var gabs Container + + if err := json.Unmarshal(sample, &gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +// ParseJSONDecoder - Convert a json.Decoder into a representation of the parsed JSON. +func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) { + var gabs Container + + if err := decoder.Decode(&gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +// ParseJSONFile - Read a file and convert into a representation of the parsed JSON. +func ParseJSONFile(path string) (*Container, error) { + if len(path) > 0 { + cBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + container, err := ParseJSON(cBytes) + if err != nil { + return nil, err + } + + return container, nil + } + return nil, ErrInvalidPath +} + +// ParseJSONBuffer - Read the contents of a buffer into a representation of the parsed JSON. +func ParseJSONBuffer(buffer io.Reader) (*Container, error) { + var gabs Container + jsonDecoder := json.NewDecoder(buffer) + if err := jsonDecoder.Decode(&gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +//-------------------------------------------------------------------------------------------------- diff --git a/vendor/github.com/Jeffail/gabs/gabs_logo.png b/vendor/github.com/Jeffail/gabs/gabs_logo.png new file mode 100644 index 00000000..b6c1fad9 Binary files /dev/null and b/vendor/github.com/Jeffail/gabs/gabs_logo.png differ diff --git a/vendor/github.com/gopackage/ddp/.gitignore b/vendor/github.com/gopackage/ddp/.gitignore new file mode 100644 index 00000000..daf913b1 --- /dev/null +++ b/vendor/github.com/gopackage/ddp/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/gopackage/ddp/LICENSE b/vendor/github.com/gopackage/ddp/LICENSE new file mode 100644 index 00000000..03d77e8a --- /dev/null +++ b/vendor/github.com/gopackage/ddp/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2015, Metamech LLC. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/gopackage/ddp/README.md b/vendor/github.com/gopackage/ddp/README.md new file mode 100644 index 00000000..fd62c718 --- /dev/null +++ b/vendor/github.com/gopackage/ddp/README.md @@ -0,0 +1,3 @@ +# ddp + +MeteorJS DDP library for Golang diff --git a/vendor/github.com/gopackage/ddp/ddp.go b/vendor/github.com/gopackage/ddp/ddp.go new file mode 100644 index 00000000..910adafd --- /dev/null +++ b/vendor/github.com/gopackage/ddp/ddp.go @@ -0,0 +1,79 @@ +// Package ddp implements the MeteorJS DDP protocol over websockets. Fallback +// to longpolling is NOT supported (and is not planned on ever being supported +// by this library). We will try to model the library after `net/http` - right +// now the library is barebones and doesn't provide the pluggability of http. +// However, that's the goal for the package eventually. +package ddp + +import ( + "fmt" + "log" + "sync" + "time" +) + +// debugLog is true if we should log debugging information about the connection +var debugLog = true + +// The main file contains common utility types. + +// ------------------------------------------------------------------- + +// idManager provides simple incrementing IDs for ddp messages. +type idManager struct { + // nextID is the next ID for API calls + nextID uint64 + // idMutex is a mutex to protect ID updates + idMutex *sync.Mutex +} + +// newidManager creates a new instance and sets up resources. +func newidManager() *idManager { + return &idManager{idMutex: new(sync.Mutex)} +} + +// newID issues a new ID for use in calls. +func (id *idManager) newID() string { + id.idMutex.Lock() + next := id.nextID + id.nextID++ + id.idMutex.Unlock() + return fmt.Sprintf("%x", next) +} + +// ------------------------------------------------------------------- + +// pingTracker tracks in-flight pings. +type pingTracker struct { + handler func(error) + timeout time.Duration + timer *time.Timer +} + +// ------------------------------------------------------------------- + +// Call represents an active RPC call. +type Call struct { + ID string // The uuid for this method call + ServiceMethod string // The name of the service and method to call. + Args interface{} // The argument to the function (*struct). + Reply interface{} // The reply from the function (*struct). + Error error // After completion, the error status. + Done chan *Call // Strobes when call is complete. + Owner *Client // Client that owns the method call +} + +// done removes the call from any owners and strobes the done channel with itself. +func (call *Call) done() { + delete(call.Owner.calls, call.ID) + select { + case call.Done <- call: + // ok + default: + // We don't want to block here. It is the caller's responsibility to make + // sure the channel has enough buffer space. See comment in Go(). + if debugLog { + log.Println("rpc: discarding Call reply due to insufficient Done chan capacity") + } + } +} diff --git a/vendor/github.com/gopackage/ddp/ddp_client.go b/vendor/github.com/gopackage/ddp/ddp_client.go new file mode 100644 index 00000000..8d6323b7 --- /dev/null +++ b/vendor/github.com/gopackage/ddp/ddp_client.go @@ -0,0 +1,654 @@ +package ddp + +import ( + "encoding/json" + "fmt" + "io" + "log" + "sync" + "time" + + "golang.org/x/net/websocket" + "errors" +) + +const ( + DISCONNECTED = iota + DIALING + CONNECTING + CONNECTED +) + +type ConnectionListener interface { + Connected() +} + +type ConnectionNotifier interface { + AddConnectionListener(listener ConnectionListener) +} + +type StatusListener interface { + Status(status int) +} + +type StatusNotifier interface { + AddStatusListener(listener StatusListener) +} + +// Client represents a DDP client connection. The DDP client establish a DDP +// session and acts as a message pump for other tools. +type Client struct { + // HeartbeatInterval is the time between heartbeats to send + HeartbeatInterval time.Duration + // HeartbeatTimeout is the time for a heartbeat ping to timeout + HeartbeatTimeout time.Duration + // ReconnectInterval is the time between reconnections on bad connections + ReconnectInterval time.Duration + + // writeStats controls statistics gathering for current websocket writes. + writeSocketStats *WriterStats + // writeStats controls statistics gathering for overall client writes. + writeStats *WriterStats + // writeLog controls logging for client writes. + writeLog *WriterLogger + // readStats controls statistics gathering for current websocket reads. + readSocketStats *ReaderStats + // readStats controls statistics gathering for overall client reads. + readStats *ReaderStats + // readLog control logging for clietn reads. + readLog *ReaderLogger + // reconnects in the number of reconnections the client has made + reconnects int64 + // pingsIn is the number of pings received from the server + pingsIn int64 + // pingsOut is te number of pings sent by the client + pingsOut int64 + + // session contains the DDP session token (can be used for reconnects and debugging). + session string + // version contains the negotiated DDP protocol version in use. + version string + // serverID the cluster node ID for the server we connected to + serverID string + // ws is the underlying websocket being used. + ws *websocket.Conn + // encoder is a JSON encoder to send outgoing packets to the websocket. + encoder *json.Encoder + // url the URL the websocket is connected to + url string + // origin is the origin for the websocket connection + origin string + // inbox is an incoming message channel + inbox chan map[string]interface{} + // errors is an incoming errors channel + errors chan error + // pingTimer is a timer for sending regular pings to the server + pingTimer *time.Timer + // pings tracks inflight pings based on each ping ID. + pings map[string][]*pingTracker + // calls tracks method invocations that are still in flight + calls map[string]*Call + // subs tracks active subscriptions. Map contains name->args + subs map[string]*Call + // collections contains all the collections currently subscribed + collections map[string]Collection + // connectionStatus is the current connection status of the client + connectionStatus int + // reconnectTimer is the timer tracking reconnections + reconnectTimer *time.Timer + // reconnectLock protects access to reconnection + reconnectLock *sync.Mutex + + // statusListeners will be informed when the connection status of the client changes + statusListeners []StatusListener + // connectionListeners will be informed when a connection to the server is established + connectionListeners []ConnectionListener + + // idManager tracks IDs for ddp messages + idManager +} + +// NewClient creates a default client (using an internal websocket) to the +// provided URL using the origin for the connection. The client will +// automatically connect, upgrade to a websocket, and establish a DDP +// connection session before returning the client. The client will +// automatically and internally handle heartbeats and reconnects. +// +// TBD create an option to use an external websocket (aka htt.Transport) +// TBD create an option to substitute heartbeat and reconnect behavior (aka http.Tranport) +// TBD create an option to hijack the connection (aka http.Hijacker) +// TBD create profiling features (aka net/http/pprof) +func NewClient(url, origin string) *Client { + c := &Client{ + HeartbeatInterval: time.Minute, // Meteor impl default + 10 (we ping last) + HeartbeatTimeout: 15 * time.Second, // Meteor impl default + ReconnectInterval: 5 * time.Second, + collections: map[string]Collection{}, + url: url, + origin: origin, + inbox: make(chan map[string]interface{}, 100), + errors: make(chan error, 100), + pings: map[string][]*pingTracker{}, + calls: map[string]*Call{}, + subs: map[string]*Call{}, + connectionStatus: DISCONNECTED, + reconnectLock: &sync.Mutex{}, + + // Stats + writeSocketStats: NewWriterStats(nil), + writeStats: NewWriterStats(nil), + readSocketStats: NewReaderStats(nil), + readStats: NewReaderStats(nil), + + // Loggers + writeLog: NewWriterTextLogger(nil), + readLog: NewReaderTextLogger(nil), + + idManager: *newidManager(), + } + c.encoder = json.NewEncoder(c.writeStats) + c.SetSocketLogActive(false) + + // We spin off an inbox processing goroutine + go c.inboxManager() + + return c +} + +// Session returns the negotiated session token for the connection. +func (c *Client) Session() string { + return c.session +} + +// Version returns the negotiated protocol version in use by the client. +func (c *Client) Version() string { + return c.version +} + +// AddStatusListener in order to receive status change updates. +func (c *Client) AddStatusListener(listener StatusListener) { + c.statusListeners = append(c.statusListeners, listener) +} + +// AddConnectionListener in order to receive connection updates. +func (c *Client) AddConnectionListener(listener ConnectionListener) { + c.connectionListeners = append(c.connectionListeners, listener) +} + +// status updates all status listeners with the new client status. +func (c *Client) status(status int) { + if c.connectionStatus == status { + return + } + c.connectionStatus = status + for _, listener := range c.statusListeners { + listener.Status(status) + } +} + +// Connect attempts to connect the client to the server. +func (c *Client) Connect() error { + c.status(DIALING) + ws, err := websocket.Dial(c.url, "", c.origin) + if err != nil { + c.Close() + log.Println("Dial error", err) + c.reconnectLater() + return err + } + // Start DDP connection + c.start(ws, NewConnect()) + return nil +} + +// Reconnect attempts to reconnect the client to the server on the existing +// DDP session. +// +// TODO needs a reconnect backoff so we don't trash a down server +// TODO reconnect should not allow more reconnects while a reconnection is already in progress. +func (c *Client) Reconnect() { + func() { + c.reconnectLock.Lock() + defer c.reconnectLock.Unlock() + if c.reconnectTimer != nil { + c.reconnectTimer.Stop() + c.reconnectTimer = nil + } + }() + + c.Close() + + c.reconnects++ + + // Reconnect + c.status(DIALING) + ws, err := websocket.Dial(c.url, "", c.origin) + if err != nil { + c.Close() + log.Println("Dial error", err) + c.reconnectLater() + return + } + + c.start(ws, NewReconnect(c.session)) + + // -------------------------------------------------------------------- + // We resume inflight or ongoing subscriptions - we don't have to wait + // for connection confirmation (messages can be pipelined). + // -------------------------------------------------------------------- + + // Send calls that haven't been confirmed - may not have been sent + // and effects should be idempotent + for _, call := range c.calls { + c.Send(NewMethod(call.ID, call.ServiceMethod, call.Args.([]interface{}))) + } + + // Resend subscriptions and patch up collections + for _, sub := range c.subs { + c.Send(NewSub(sub.ID, sub.ServiceMethod, sub.Args.([]interface{}))) + } +} + +// Subscribe subscribes to data updates. +func (c *Client) Subscribe(subName string, done chan *Call, args ...interface{}) *Call { + + if args == nil { + args = []interface{}{} + } + call := new(Call) + call.ID = c.newID() + call.ServiceMethod = subName + call.Args = args + call.Owner = c + + if done == nil { + done = make(chan *Call, 10) // buffered. + } else { + // If caller passes done != nil, it must arrange that + // done has enough buffer for the number of simultaneous + // RPCs that will be using that channel. If the channel + // is totally unbuffered, it's best not to run at all. + if cap(done) == 0 { + log.Panic("ddp.rpc: done channel is unbuffered") + } + } + call.Done = done + c.subs[call.ID] = call + + // Save this subscription to the client so we can reconnect + subArgs := make([]interface{}, len(args)) + copy(subArgs, args) + + c.Send(NewSub(call.ID, subName, args)) + + return call +} + +// Sub sends a synchronous subscription request to the server. +func (c *Client) Sub(subName string, args ...interface{}) error { + call := <-c.Subscribe(subName, make(chan *Call, 1), args...).Done + return call.Error +} + +// Go invokes the function asynchronously. It returns the Call structure representing +// the invocation. The done channel will signal when the call is complete by returning +// the same Call object. If done is nil, Go will allocate a new channel. +// If non-nil, done must be buffered or Go will deliberately crash. +// +// Go and Call are modeled after the standard `net/rpc` package versions. +func (c *Client) Go(serviceMethod string, done chan *Call, args ...interface{}) *Call { + + if args == nil { + args = []interface{}{} + } + call := new(Call) + call.ID = c.newID() + call.ServiceMethod = serviceMethod + call.Args = args + call.Owner = c + if done == nil { + done = make(chan *Call, 10) // buffered. + } else { + // If caller passes done != nil, it must arrange that + // done has enough buffer for the number of simultaneous + // RPCs that will be using that channel. If the channel + // is totally unbuffered, it's best not to run at all. + if cap(done) == 0 { + log.Panic("ddp.rpc: done channel is unbuffered") + } + } + call.Done = done + c.calls[call.ID] = call + + c.Send(NewMethod(call.ID, serviceMethod, args)) + + return call +} + +// Call invokes the named function, waits for it to complete, and returns its error status. +func (c *Client) Call(serviceMethod string, args ...interface{}) (interface{}, error) { + call := <-c.Go(serviceMethod, make(chan *Call, 1), args...).Done + return call.Reply, call.Error +} + +// Ping sends a heartbeat signal to the server. The Ping doesn't look for +// a response but may trigger the connection to reconnect if the ping timesout. +// This is primarily useful for reviving an unresponsive Client connection. +func (c *Client) Ping() { + c.PingPong(c.newID(), c.HeartbeatTimeout, func(err error) { + if err != nil { + // Is there anything else we should or can do? + c.reconnectLater() + } + }) +} + +// PingPong sends a heartbeat signal to the server and calls the provided +// function when a pong is received. An optional id can be sent to help +// track the responses - or an empty string can be used. It is the +// responsibility of the caller to respond to any errors that may occur. +func (c *Client) PingPong(id string, timeout time.Duration, handler func(error)) { + err := c.Send(NewPing(id)) + if err != nil { + handler(err) + return + } + c.pingsOut++ + pings, ok := c.pings[id] + if !ok { + pings = make([]*pingTracker, 0, 5) + } + tracker := &pingTracker{handler: handler, timeout: timeout, timer: time.AfterFunc(timeout, func() { + handler(fmt.Errorf("ping timeout")) + })} + c.pings[id] = append(pings, tracker) +} + +// Send transmits messages to the server. The msg parameter must be json +// encoder compatible. +func (c *Client) Send(msg interface{}) error { + return c.encoder.Encode(msg) +} + +// Close implements the io.Closer interface. +func (c *Client) Close() { + // Shutdown out all outstanding pings + if c.pingTimer != nil { + c.pingTimer.Stop() + c.pingTimer = nil + } + + // Close websocket + if c.ws != nil { + c.ws.Close() + c.ws = nil + } + for _, collection := range c.collections { + collection.reset() + } + c.status(DISCONNECTED) +} + +// ResetStats resets the statistics for the client. +func (c *Client) ResetStats() { + c.readSocketStats.Reset() + c.readStats.Reset() + c.writeSocketStats.Reset() + c.writeStats.Reset() + c.reconnects = 0 + c.pingsIn = 0 + c.pingsOut = 0 +} + +// Stats returns the read and write statistics of the client. +func (c *Client) Stats() *ClientStats { + return &ClientStats{ + Reads: c.readSocketStats.Snapshot(), + TotalReads: c.readStats.Snapshot(), + Writes: c.writeSocketStats.Snapshot(), + TotalWrites: c.writeStats.Snapshot(), + Reconnects: c.reconnects, + PingsSent: c.pingsOut, + PingsRecv: c.pingsIn, + } +} + +// SocketLogActive returns the current logging status for the socket. +func (c *Client) SocketLogActive() bool { + return c.writeLog.Active +} + +// SetSocketLogActive to true to enable logging of raw socket data. +func (c *Client) SetSocketLogActive(active bool) { + c.writeLog.Active = active + c.readLog.Active = active +} + +// CollectionByName retrieves a collection by it's name. +func (c *Client) CollectionByName(name string) Collection { + collection, ok := c.collections[name] + if !ok { + collection = NewCollection(name) + c.collections[name] = collection + } + return collection +} + +// CollectionStats returns a snapshot of statistics for the currently known collections. +func (c *Client) CollectionStats() []CollectionStats { + stats := make([]CollectionStats, 0, len(c.collections)) + for name, collection := range c.collections { + stats = append(stats, CollectionStats{Name: name, Count: len(collection.FindAll())}) + } + return stats +} + +// start starts a new client connection on the provided websocket +func (c *Client) start(ws *websocket.Conn, connect *Connect) { + + c.status(CONNECTING) + + c.ws = ws + c.writeLog.SetWriter(ws) + c.writeSocketStats = NewWriterStats(c.writeLog) + c.writeStats.SetWriter(c.writeSocketStats) + c.readLog.SetReader(ws) + c.readSocketStats = NewReaderStats(c.readLog) + c.readStats.SetReader(c.readSocketStats) + + // We spin off an inbox stuffing goroutine + go c.inboxWorker(c.readStats) + + c.Send(connect) +} + +// inboxManager pulls messages from the inbox and routes them to appropriate +// handlers. +func (c *Client) inboxManager() { + for { + select { + case msg := <-c.inbox: + // Message! + //log.Println("Got message", msg) + mtype, ok := msg["msg"] + if ok { + switch mtype.(string) { + // Connection management + case "connected": + c.status(CONNECTED) + for _, collection := range c.collections { + collection.init() + } + c.version = "1" // Currently the only version we support + c.session = msg["session"].(string) + // Start automatic heartbeats + c.pingTimer = time.AfterFunc(c.HeartbeatInterval, func() { + c.Ping() + c.pingTimer.Reset(c.HeartbeatInterval) + }) + // Notify connection listeners + for _, listener := range c.connectionListeners { + go listener.Connected() + } + case "failed": + log.Fatalf("IM Failed to connect, we support version 1 but server supports %s", msg["version"]) + + // Heartbeats + case "ping": + // We received a ping - need to respond with a pong + id, ok := msg["id"] + if ok { + c.Send(NewPong(id.(string))) + } else { + c.Send(NewPong("")) + } + c.pingsIn++ + case "pong": + // We received a pong - we can clear the ping tracker and call its handler + id, ok := msg["id"] + var key string + if ok { + key = id.(string) + } + pings, ok := c.pings[key] + if ok && len(pings) > 0 { + ping := pings[0] + pings = pings[1:] + if len(key) == 0 || len(pings) > 0 { + c.pings[key] = pings + } + ping.timer.Stop() + ping.handler(nil) + } + + // Live Data + case "nosub": + log.Println("Subscription returned a nosub error", msg) + // Clear related subscriptions + sub, ok := msg["id"] + if ok { + id := sub.(string) + runningSub := c.subs[id] + + if runningSub != nil { + runningSub.Error = errors.New("Subscription returned a nosub error") + runningSub.done() + delete(c.subs, id) + } + } + case "ready": + // Run 'done' callbacks on all ready subscriptions + subs, ok := msg["subs"] + if ok { + for _, sub := range subs.([]interface{}) { + call, ok := c.subs[sub.(string)] + if ok { + call.done() + } + } + } + case "added": + c.collectionBy(msg).added(msg) + case "changed": + c.collectionBy(msg).changed(msg) + case "removed": + c.collectionBy(msg).removed(msg) + case "addedBefore": + c.collectionBy(msg).addedBefore(msg) + case "movedBefore": + c.collectionBy(msg).movedBefore(msg) + + // RPC + case "result": + id, ok := msg["id"] + if ok { + call := c.calls[id.(string)] + delete(c.calls, id.(string)) + e, ok := msg["error"] + if ok { + txt, _ := json.Marshal(e) + call.Error = fmt.Errorf(string(txt)) + call.Reply = e + } else { + call.Reply = msg["result"] + } + call.done() + } + case "updated": + // We currently don't do anything with updated status + + default: + // Ignore? + log.Println("Server sent unexpected message", msg) + } + } else { + // Current Meteor server sends an undocumented DDP message + // (looks like clustering "hint"). We will register and + // ignore rather than log an error. + serverID, ok := msg["server_id"] + if ok { + switch ID := serverID.(type) { + case string: + c.serverID = ID + default: + log.Println("Server cluster node", serverID) + } + } else { + log.Println("Server sent message with no `msg` field", msg) + } + } + case err := <-c.errors: + log.Println("Websocket error", err) + } + } +} + +func (c *Client) collectionBy(msg map[string]interface{}) Collection { + n, ok := msg["collection"] + if !ok { + return NewMockCollection() + } + switch name := n.(type) { + case string: + return c.CollectionByName(name) + default: + return NewMockCollection() + } +} + +// inboxWorker pulls messages from a websocket, decodes JSON packets, and +// stuffs them into a message channel. +func (c *Client) inboxWorker(ws io.Reader) { + dec := json.NewDecoder(ws) + for { + var event interface{} + + if err := dec.Decode(&event); err == io.EOF { + break + } else if err != nil { + c.errors <- err + } + if c.pingTimer != nil { + c.pingTimer.Reset(c.HeartbeatInterval) + } + if event == nil { + log.Println("Inbox worker found nil event. May be due to broken websocket. Reconnecting.") + break + } else { + c.inbox <- event.(map[string]interface{}) + } + } + + c.reconnectLater() +} + +// reconnectLater schedules a reconnect for later. We need to make sure that we don't +// block, and that we don't reconnect more frequently than once every c.ReconnectInterval +func (c *Client) reconnectLater() { + c.Close() + c.reconnectLock.Lock() + defer c.reconnectLock.Unlock() + if c.reconnectTimer == nil { + c.reconnectTimer = time.AfterFunc(c.ReconnectInterval, c.Reconnect) + } +} diff --git a/vendor/github.com/gopackage/ddp/ddp_collection.go b/vendor/github.com/gopackage/ddp/ddp_collection.go new file mode 100644 index 00000000..f417e68a --- /dev/null +++ b/vendor/github.com/gopackage/ddp/ddp_collection.go @@ -0,0 +1,245 @@ +package ddp + +// ---------------------------------------------------------------------- +// Collection +// ---------------------------------------------------------------------- + +type Update map[string]interface{} +type UpdateListener interface { + CollectionUpdate(collection, operation, id string, doc Update) +} + +// Collection managed cached collection data sent from the server in a +// livedata subscription. +// +// It would be great to build an entire mongo compatible local store (minimongo) +type Collection interface { + + // FindOne queries objects and returns the first match. + FindOne(id string) Update + // FindAll returns a map of all items in the cache - this is a hack + // until we have time to build out a real minimongo interface. + FindAll() map[string]Update + // AddUpdateListener adds a channel that receives update messages. + AddUpdateListener(listener UpdateListener) + + // livedata updates + added(msg Update) + changed(msg Update) + removed(msg Update) + addedBefore(msg Update) + movedBefore(msg Update) + init() // init informs the collection that the connection to the server has begun/resumed + reset() // reset informs the collection that the connection to the server has been lost +} + +// NewMockCollection creates an empty collection that does nothing. +func NewMockCollection() Collection { + return &MockCache{} +} + +// NewCollection creates a new collection - always KeyCache. +func NewCollection(name string) Collection { + return &KeyCache{name, make(map[string]Update), nil} +} + +// KeyCache caches items keyed on unique ID. +type KeyCache struct { + // The name of the collection + Name string + // items contains collection items by ID + items map[string]Update + // listeners contains all the listeners that should be notified of collection updates. + listeners []UpdateListener + // TODO(badslug): do we need to protect from multiple threads +} + +func (c *KeyCache) added(msg Update) { + id, fields := parseUpdate(msg) + if fields != nil { + c.items[id] = fields + c.notify("create", id, fields) + } +} + +func (c *KeyCache) changed(msg Update) { + id, fields := parseUpdate(msg) + if fields != nil { + item, ok := c.items[id] + if ok { + for key, value := range fields { + item[key] = value + } + c.items[id] = item + c.notify("update", id, item) + } + } +} + +func (c *KeyCache) removed(msg Update) { + id, _ := parseUpdate(msg) + if len(id) > 0 { + delete(c.items, id) + c.notify("remove", id, nil) + } +} + +func (c *KeyCache) addedBefore(msg Update) { + // for keyed cache, ordered commands are a noop +} + +func (c *KeyCache) movedBefore(msg Update) { + // for keyed cache, ordered commands are a noop +} + +// init prepares the collection for data updates (called when a new connection is +// made or a connection/session is resumed). +func (c *KeyCache) init() { + // TODO start to patch up the current data with fresh server state +} + +func (c *KeyCache) reset() { + // TODO we should mark the collection but maintain it's contents and then + // patch up the current contents with the new contents when we receive them. + //c.items = nil + c.notify("reset", "", nil) +} + +// notify sends a Update to all UpdateListener's which should never block. +func (c *KeyCache) notify(operation, id string, doc Update) { + for _, listener := range c.listeners { + listener.CollectionUpdate(c.Name, operation, id, doc) + } +} + +// FindOne returns the item with matching id. +func (c *KeyCache) FindOne(id string) Update { + return c.items[id] +} + +// FindAll returns a dump of all items in the collection +func (c *KeyCache) FindAll() map[string]Update { + return c.items +} + +// AddUpdateListener adds a listener for changes on a collection. +func (c *KeyCache) AddUpdateListener(listener UpdateListener) { + c.listeners = append(c.listeners, listener) +} + +// OrderedCache caches items based on list order. +// This is a placeholder, currently not implemented as the Meteor server +// does not transmit ordered collections over DDP yet. +type OrderedCache struct { + // ranks contains ordered collection items for ordered collections + items []interface{} +} + +func (c *OrderedCache) added(msg Update) { + // for ordered cache, key commands are a noop +} + +func (c *OrderedCache) changed(msg Update) { + +} + +func (c *OrderedCache) removed(msg Update) { + +} + +func (c *OrderedCache) addedBefore(msg Update) { + +} + +func (c *OrderedCache) movedBefore(msg Update) { + +} + +func (c *OrderedCache) init() { + +} + +func (c *OrderedCache) reset() { + +} + +// FindOne returns the item with matching id. +func (c *OrderedCache) FindOne(id string) Update { + return nil +} + +// FindAll returns a dump of all items in the collection +func (c *OrderedCache) FindAll() map[string]Update { + return map[string]Update{} +} + +// AddUpdateListener does nothing. +func (c *OrderedCache) AddUpdateListener(ch UpdateListener) { +} + +// MockCache implements the Collection interface but does nothing with the data. +type MockCache struct { +} + +func (c *MockCache) added(msg Update) { + +} + +func (c *MockCache) changed(msg Update) { + +} + +func (c *MockCache) removed(msg Update) { + +} + +func (c *MockCache) addedBefore(msg Update) { + +} + +func (c *MockCache) movedBefore(msg Update) { + +} + +func (c *MockCache) init() { + +} + +func (c *MockCache) reset() { + +} + +// FindOne returns the item with matching id. +func (c *MockCache) FindOne(id string) Update { + return nil +} + +// FindAll returns a dump of all items in the collection +func (c *MockCache) FindAll() map[string]Update { + return map[string]Update{} +} + +// AddUpdateListener does nothing. +func (c *MockCache) AddUpdateListener(ch UpdateListener) { +} + +// parseUpdate returns the ID and fields from a DDP Update document. +func parseUpdate(up Update) (ID string, Fields Update) { + key, ok := up["id"] + if ok { + switch id := key.(type) { + case string: + updates, ok := up["fields"] + if ok { + switch fields := updates.(type) { + case map[string]interface{}: + return id, Update(fields) + default: + // Don't know what to do... + } + } + return id, nil + } + } + return "", nil +} diff --git a/vendor/github.com/gopackage/ddp/ddp_ejson.go b/vendor/github.com/gopackage/ddp/ddp_ejson.go new file mode 100644 index 00000000..a3e1fec0 --- /dev/null +++ b/vendor/github.com/gopackage/ddp/ddp_ejson.go @@ -0,0 +1,217 @@ +package ddp + +import ( + "crypto/sha256" + "encoding/hex" + "io" + "strings" + "time" +) + +// ---------------------------------------------------------------------- +// EJSON document interface +// ---------------------------------------------------------------------- +// https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md#appendix-ejson + +// Doc provides hides the complexity of ejson documents. +type Doc struct { + root interface{} +} + +// NewDoc creates a new document from a generic json parsed document. +func NewDoc(in interface{}) *Doc { + doc := &Doc{in} + return doc +} + +// Map locates a map[string]interface{} - json object - at a path +// or returns nil if not found. +func (d *Doc) Map(path string) map[string]interface{} { + item := d.Item(path) + if item != nil { + switch m := item.(type) { + case map[string]interface{}: + return m + default: + return nil + } + } + return nil +} + +// Array locates an []interface{} - json array - at a path +// or returns nil if not found. +func (d *Doc) Array(path string) []interface{} { + item := d.Item(path) + if item != nil { + switch m := item.(type) { + case []interface{}: + return m + default: + return nil + } + } + return nil +} + +// StringArray locates an []string - json array of strings - at a path +// or returns nil if not found. The string array will contain all string values +// in the array and skip any non-string entries. +func (d *Doc) StringArray(path string) []string { + item := d.Item(path) + if item != nil { + switch m := item.(type) { + case []interface{}: + items := []string{} + for _, item := range m { + switch val := item.(type) { + case string: + items = append(items, val) + } + } + return items + case []string: + return m + default: + return nil + } + } + return nil +} + +// String returns a string value located at the path or an empty string if not found. +func (d *Doc) String(path string) string { + item := d.Item(path) + if item != nil { + switch m := item.(type) { + case string: + return m + default: + return "" + } + } + return "" +} + +// Bool returns a boolean value located at the path or false if not found. +func (d *Doc) Bool(path string) bool { + item := d.Item(path) + if item != nil { + switch m := item.(type) { + case bool: + return m + default: + return false + } + } + return false +} + +// Float returns a float64 value located at the path or zero if not found. +func (d *Doc) Float(path string) float64 { + item := d.Item(path) + if item != nil { + switch m := item.(type) { + case float64: + return m + default: + return 0 + } + } + return 0 +} + +// Time returns a time value located at the path or nil if not found. +func (d *Doc) Time(path string) time.Time { + ticks := d.Float(path + ".$date") + var t time.Time + if ticks > 0 { + sec := int64(ticks / 1000) + t = time.Unix(int64(sec), 0) + } + return t +} + +// Item locates a "raw" item at the provided path, returning +// the item found or nil if not found. +func (d *Doc) Item(path string) interface{} { + item := d.root + steps := strings.Split(path, ".") + for _, step := range steps { + // This is an intermediate step - we must be in a map + switch m := item.(type) { + case map[string]interface{}: + item = m[step] + case Update: + item = m[step] + default: + return nil + } + } + return item +} + +// Set a value for a path. Intermediate items are created as necessary. +func (d *Doc) Set(path string, value interface{}) { + item := d.root + steps := strings.Split(path, ".") + last := steps[len(steps)-1] + steps = steps[:len(steps)-1] + for _, step := range steps { + // This is an intermediate step - we must be in a map + switch m := item.(type) { + case map[string]interface{}: + item = m[step] + if item == nil { + item = map[string]interface{}{} + m[step] = item + } + default: + return + } + } + // Item is now the last map so we just set the value + switch m := item.(type) { + case map[string]interface{}: + m[last] = value + } +} + +// Accounts password login support +type Login struct { + User *User `json:"user"` + Password *Password `json:"password"` +} + +func NewEmailLogin(email, pass string) *Login { + return &Login{User: &User{Email: email}, Password: NewPassword(pass)} +} + +func NewUsernameLogin(user, pass string) *Login { + return &Login{User: &User{Username: user}, Password: NewPassword(pass)} +} + +type LoginResume struct { + Token string `json:"resume"` +} + +func NewLoginResume(token string) *LoginResume { + return &LoginResume{Token: token} +} + +type User struct { + Email string `json:"email,omitempty"` + Username string `json:"username,omitempty"` +} + +type Password struct { + Digest string `json:"digest"` + Algorithm string `json:"algorithm"` +} + +func NewPassword(pass string) *Password { + sha := sha256.New() + io.WriteString(sha, pass) + digest := sha.Sum(nil) + return &Password{Digest: hex.EncodeToString(digest), Algorithm: "sha-256"} +} diff --git a/vendor/github.com/gopackage/ddp/ddp_messages.go b/vendor/github.com/gopackage/ddp/ddp_messages.go new file mode 100644 index 00000000..68c9eab4 --- /dev/null +++ b/vendor/github.com/gopackage/ddp/ddp_messages.go @@ -0,0 +1,82 @@ +package ddp + +// ------------------------------------------------------------ +// DDP Messages +// +// Go structs representing DDP raw messages ready for JSON +// encoding. +// ------------------------------------------------------------ + +// Message contains the common fields that all DDP messages use. +type Message struct { + Type string `json:"msg"` + ID string `json:"id,omitempty"` +} + +// Connect represents a DDP connect message. +type Connect struct { + Message + Version string `json:"version"` + Support []string `json:"support"` + Session string `json:"session,omitempty"` +} + +// NewConnect creates a new connect message +func NewConnect() *Connect { + return &Connect{Message: Message{Type: "connect"}, Version: "1", Support: []string{"1"}} +} + +// NewReconnect creates a new connect message with a session ID to resume. +func NewReconnect(session string) *Connect { + c := NewConnect() + c.Session = session + return c +} + +// Ping represents a DDP ping message. +type Ping Message + +// NewPing creates a new ping message with optional ID. +func NewPing(id string) *Ping { + return &Ping{Type: "ping", ID: id} +} + +// Pong represents a DDP pong message. +type Pong Message + +// NewPong creates a new pong message with optional ID. +func NewPong(id string) *Pong { + return &Pong{Type: "pong", ID: id} +} + +// Method is used to send a remote procedure call to the server. +type Method struct { + Message + ServiceMethod string `json:"method"` + Args []interface{} `json:"params"` +} + +// NewMethod creates a new method invocation object. +func NewMethod(id, serviceMethod string, args []interface{}) *Method { + return &Method{ + Message: Message{Type: "method", ID: id}, + ServiceMethod: serviceMethod, + Args: args, + } +} + +// Sub is used to send a subscription request to the server. +type Sub struct { + Message + SubName string `json:"name"` + Args []interface{} `json:"params"` +} + +// NewSub creates a new sub object. +func NewSub(id, subName string, args []interface{}) *Sub { + return &Sub{ + Message: Message{Type: "sub", ID: id}, + SubName: subName, + Args: args, + } +} diff --git a/vendor/github.com/gopackage/ddp/ddp_stats.go b/vendor/github.com/gopackage/ddp/ddp_stats.go new file mode 100644 index 00000000..1546b547 --- /dev/null +++ b/vendor/github.com/gopackage/ddp/ddp_stats.go @@ -0,0 +1,321 @@ +package ddp + +import ( + "encoding/hex" + "fmt" + "io" + "log" + "os" + "sync" + "time" +) + +// Gather statistics about a DDP connection. + +// --------------------------------------------------------- +// io utilities +// +// This is generic - should be moved into a stand alone lib +// --------------------------------------------------------- + +// ReaderProxy provides common tooling for structs that manage an io.Reader. +type ReaderProxy struct { + reader io.Reader +} + +// NewReaderProxy creates a new proxy for the provided reader. +func NewReaderProxy(reader io.Reader) *ReaderProxy { + return &ReaderProxy{reader} +} + +// SetReader sets the reader on the proxy. +func (r *ReaderProxy) SetReader(reader io.Reader) { + r.reader = reader +} + +// WriterProxy provides common tooling for structs that manage an io.Writer. +type WriterProxy struct { + writer io.Writer +} + +// NewWriterProxy creates a new proxy for the provided writer. +func NewWriterProxy(writer io.Writer) *WriterProxy { + return &WriterProxy{writer} +} + +// SetWriter sets the writer on the proxy. +func (w *WriterProxy) SetWriter(writer io.Writer) { + w.writer = writer +} + +// Logging data types +const ( + DataByte = iota // data is raw []byte + DataText // data is string data +) + +// Logger logs data from i/o sources. +type Logger struct { + // Active is true if the logger should be logging reads + Active bool + // Truncate is >0 to indicate the number of characters to truncate output + Truncate int + + logger *log.Logger + dtype int +} + +// NewLogger creates a new i/o logger. +func NewLogger(logger *log.Logger, active bool, dataType int, truncate int) Logger { + return Logger{logger: logger, Active: active, dtype: dataType, Truncate: truncate} +} + +// Log logs the current i/o operation and returns the read and error for +// easy call chaining. +func (l *Logger) Log(p []byte, n int, err error) (int, error) { + if l.Active && err == nil { + limit := n + truncated := false + if l.Truncate > 0 && l.Truncate < limit { + limit = l.Truncate + truncated = true + } + switch l.dtype { + case DataText: + if truncated { + l.logger.Printf("[%d] %s...", n, string(p[:limit])) + } else { + l.logger.Printf("[%d] %s", n, string(p[:limit])) + } + case DataByte: + fallthrough + default: + l.logger.Println(hex.Dump(p[:limit])) + } + } + return n, err +} + +// ReaderLogger logs data from any io.Reader. +// ReaderLogger wraps a Reader and passes data to the actual data consumer. +type ReaderLogger struct { + Logger + ReaderProxy +} + +// NewReaderDataLogger creates an active binary data logger with a default +// log.Logger and a '->' prefix. +func NewReaderDataLogger(reader io.Reader) *ReaderLogger { + logger := log.New(os.Stdout, "<- ", log.LstdFlags) + return NewReaderLogger(reader, logger, true, DataByte, 0) +} + +// NewReaderTextLogger creates an active binary data logger with a default +// log.Logger and a '->' prefix. +func NewReaderTextLogger(reader io.Reader) *ReaderLogger { + logger := log.New(os.Stdout, "<- ", log.LstdFlags) + return NewReaderLogger(reader, logger, true, DataText, 80) +} + +// NewReaderLogger creates a Reader logger for the provided parameters. +func NewReaderLogger(reader io.Reader, logger *log.Logger, active bool, dataType int, truncate int) *ReaderLogger { + return &ReaderLogger{ReaderProxy: *NewReaderProxy(reader), Logger: NewLogger(logger, active, dataType, truncate)} +} + +// Read logs the read bytes and passes them to the wrapped reader. +func (r *ReaderLogger) Read(p []byte) (int, error) { + n, err := r.reader.Read(p) + return r.Log(p, n, err) +} + +// WriterLogger logs data from any io.Writer. +// WriterLogger wraps a Writer and passes data to the actual data producer. +type WriterLogger struct { + Logger + WriterProxy +} + +// NewWriterDataLogger creates an active binary data logger with a default +// log.Logger and a '->' prefix. +func NewWriterDataLogger(writer io.Writer) *WriterLogger { + logger := log.New(os.Stdout, "-> ", log.LstdFlags) + return NewWriterLogger(writer, logger, true, DataByte, 0) +} + +// NewWriterTextLogger creates an active binary data logger with a default +// log.Logger and a '->' prefix. +func NewWriterTextLogger(writer io.Writer) *WriterLogger { + logger := log.New(os.Stdout, "-> ", log.LstdFlags) + return NewWriterLogger(writer, logger, true, DataText, 80) +} + +// NewWriterLogger creates a Reader logger for the provided parameters. +func NewWriterLogger(writer io.Writer, logger *log.Logger, active bool, dataType int, truncate int) *WriterLogger { + return &WriterLogger{WriterProxy: *NewWriterProxy(writer), Logger: NewLogger(logger, active, dataType, truncate)} +} + +// Write logs the written bytes and passes them to the wrapped writer. +func (w *WriterLogger) Write(p []byte) (int, error) { + if w.writer != nil { + n, err := w.writer.Write(p) + return w.Log(p, n, err) + } + return 0, nil +} + +// Stats tracks statistics for i/o operations. Stats are produced from a +// of a running stats agent. +type Stats struct { + // Bytes is the total number of bytes transferred. + Bytes int64 + // Ops is the total number of i/o operations performed. + Ops int64 + // Errors is the total number of i/o errors encountered. + Errors int64 + // Runtime is the duration that stats have been gathered. + Runtime time.Duration +} + +// ClientStats displays combined statistics for the Client. +type ClientStats struct { + // Reads provides statistics on the raw i/o network reads for the current connection. + Reads *Stats + // Reads provides statistics on the raw i/o network reads for the all client connections. + TotalReads *Stats + // Writes provides statistics on the raw i/o network writes for the current connection. + Writes *Stats + // Writes provides statistics on the raw i/o network writes for all the client connections. + TotalWrites *Stats + // Reconnects is the number of reconnections the client has made. + Reconnects int64 + // PingsSent is the number of pings sent by the client + PingsSent int64 + // PingsRecv is the number of pings received by the client + PingsRecv int64 +} + +// String produces a compact string representation of the client stats. +func (stats *ClientStats) String() string { + i := stats.Reads + ti := stats.TotalReads + o := stats.Writes + to := stats.TotalWrites + totalRun := (ti.Runtime * 1000000) / 1000000 + run := (i.Runtime * 1000000) / 1000000 + return fmt.Sprintf("bytes: %d/%d##%d/%d ops: %d/%d##%d/%d err: %d/%d##%d/%d reconnects: %d pings: %d/%d uptime: %v##%v", + i.Bytes, o.Bytes, + ti.Bytes, to.Bytes, + i.Ops, o.Ops, + ti.Ops, to.Ops, + i.Errors, o.Errors, + ti.Errors, to.Errors, + stats.Reconnects, + stats.PingsRecv, stats.PingsSent, + run, totalRun) +} + +// CollectionStats combines statistics about a collection. +type CollectionStats struct { + Name string // Name of the collection + Count int // Count is the total number of documents in the collection +} + +// String produces a compact string representation of the collection stat. +func (s *CollectionStats) String() string { + return fmt.Sprintf("%s[%d]", s.Name, s.Count) +} + +// StatsTracker provides the basic tooling for tracking i/o stats. +type StatsTracker struct { + bytes int64 + ops int64 + errors int64 + start time.Time + lock *sync.Mutex +} + +// NewStatsTracker create a new stats tracker with start time set to now. +func NewStatsTracker() *StatsTracker { + return &StatsTracker{start: time.Now(), lock: new(sync.Mutex)} +} + +// Op records an i/o operation. The parameters are passed through to +// allow easy chaining. +func (t *StatsTracker) Op(n int, err error) (int, error) { + t.lock.Lock() + defer t.lock.Unlock() + t.ops++ + if err == nil { + t.bytes += int64(n) + } else { + if err == io.EOF { + // I don't think we should log EOF stats as an error + } else { + t.errors++ + } + } + + return n, err +} + +// Snapshot takes a snapshot of the current reader statistics. +func (t *StatsTracker) Snapshot() *Stats { + t.lock.Lock() + defer t.lock.Unlock() + return t.snap() +} + +// Reset sets all of the stats to initial values. +func (t *StatsTracker) Reset() *Stats { + t.lock.Lock() + defer t.lock.Unlock() + + stats := t.snap() + t.bytes = 0 + t.ops = 0 + t.errors = 0 + t.start = time.Now() + + return stats +} + +func (t *StatsTracker) snap() *Stats { + return &Stats{Bytes: t.bytes, Ops: t.ops, Errors: t.errors, Runtime: time.Since(t.start)} +} + +// ReaderStats tracks statistics on any io.Reader. +// ReaderStats wraps a Reader and passes data to the actual data consumer. +type ReaderStats struct { + StatsTracker + ReaderProxy +} + +// NewReaderStats creates a ReaderStats object for the provided reader. +func NewReaderStats(reader io.Reader) *ReaderStats { + return &ReaderStats{ReaderProxy: *NewReaderProxy(reader), StatsTracker: *NewStatsTracker()} +} + +// Read passes through a read collecting statistics and logging activity. +func (r *ReaderStats) Read(p []byte) (int, error) { + return r.Op(r.reader.Read(p)) +} + +// WriterStats tracks statistics on any io.Writer. +// WriterStats wraps a Writer and passes data to the actual data producer. +type WriterStats struct { + StatsTracker + WriterProxy +} + +// NewWriterStats creates a WriterStats object for the provided writer. +func NewWriterStats(writer io.Writer) *WriterStats { + return &WriterStats{WriterProxy: *NewWriterProxy(writer), StatsTracker: *NewStatsTracker()} +} + +// Write passes through a write collecting statistics. +func (w *WriterStats) Write(p []byte) (int, error) { + if w.writer != nil { + return w.Op(w.writer.Write(p)) + } + return 0, nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/channel.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/channel.go new file mode 100644 index 00000000..c6579ece --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/channel.go @@ -0,0 +1,39 @@ +package models + +import "time" + +type Channel struct { + ID string `json:"_id"` + Name string `json:"name"` + Fname string `json:"fname,omitempty"` + Type string `json:"t"` + Msgs int `json:"msgs"` + + ReadOnly bool `json:"ro,omitempty"` + SysMes bool `json:"sysMes,omitempty"` + Default bool `json:"default"` + Broadcast bool `json:"broadcast,omitempty"` + + Timestamp *time.Time `json:"ts,omitempty"` + UpdatedAt *time.Time `json:"_updatedAt,omitempty"` + + User *User `json:"u,omitempty"` + LastMessage *Message `json:"lastMessage,omitempty"` + + // Lm interface{} `json:"lm"` + // CustomFields struct { + // } `json:"customFields,omitempty"` +} + +type ChannelSubscription struct { + ID string `json:"_id"` + Alert bool `json:"alert"` + Name string `json:"name"` + DisplayName string `json:"fname"` + Open bool `json:"open"` + RoomId string `json:"rid"` + Type string `json:"c"` + User User `json:"u"` + Roles []string `json:"roles"` + Unread float64 `json:"unread"` +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/info.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/info.go new file mode 100644 index 00000000..fb99e7c2 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/info.go @@ -0,0 +1,133 @@ +package models + +import "time" + +type Info struct { + Version string `json:"version"` + + Build struct { + NodeVersion string `json:"nodeVersion"` + Arch string `json:"arch"` + Platform string `json:"platform"` + Cpus int `json:"cpus"` + } `json:"build"` + + Commit struct { + Hash string `json:"hash"` + Date string `json:"date"` + Author string `json:"author"` + Subject string `json:"subject"` + Tag string `json:"tag"` + Branch string `json:"branch"` + } `json:"commit"` +} + +type Pagination struct { + Count int `json:"count"` + Offset int `json:"offset"` + Total int `json:"total"` +} + +type Directory struct { + Result []struct { + ID string `json:"_id"` + CreatedAt time.Time `json:"createdAt"` + Emails []struct { + Address string `json:"address"` + Verified bool `json:"verified"` + } `json:"emails"` + Name string `json:"name"` + Username string `json:"username"` + } `json:"result"` + + Pagination +} + +type Spotlight struct { + Users []User `json:"users"` + Rooms []Channel `json:"rooms"` +} + +type Statistics struct { + ID string `json:"_id"` + UniqueID string `json:"uniqueId"` + Version string `json:"version"` + + ActiveUsers int `json:"activeUsers"` + NonActiveUsers int `json:"nonActiveUsers"` + OnlineUsers int `json:"onlineUsers"` + AwayUsers int `json:"awayUsers"` + OfflineUsers int `json:"offlineUsers"` + TotalUsers int `json:"totalUsers"` + + TotalRooms int `json:"totalRooms"` + TotalChannels int `json:"totalChannels"` + TotalPrivateGroups int `json:"totalPrivateGroups"` + TotalDirect int `json:"totalDirect"` + TotlalLivechat int `json:"totlalLivechat"` + TotalMessages int `json:"totalMessages"` + TotalChannelMessages int `json:"totalChannelMessages"` + TotalPrivateGroupMessages int `json:"totalPrivateGroupMessages"` + TotalDirectMessages int `json:"totalDirectMessages"` + TotalLivechatMessages int `json:"totalLivechatMessages"` + + InstalledAt time.Time `json:"installedAt"` + LastLogin time.Time `json:"lastLogin"` + LastMessageSentAt time.Time `json:"lastMessageSentAt"` + LastSeenSubscription time.Time `json:"lastSeenSubscription"` + + Os struct { + Type string `json:"type"` + Platform string `json:"platform"` + Arch string `json:"arch"` + Release string `json:"release"` + Uptime int `json:"uptime"` + Loadavg []float64 `json:"loadavg"` + Totalmem int64 `json:"totalmem"` + Freemem int `json:"freemem"` + Cpus []struct { + Model string `json:"model"` + Speed int `json:"speed"` + Times struct { + User int `json:"user"` + Nice int `json:"nice"` + Sys int `json:"sys"` + Idle int `json:"idle"` + Irq int `json:"irq"` + } `json:"times"` + } `json:"cpus"` + } `json:"os"` + + Process struct { + NodeVersion string `json:"nodeVersion"` + Pid int `json:"pid"` + Uptime float64 `json:"uptime"` + } `json:"process"` + + Deploy struct { + Method string `json:"method"` + Platform string `json:"platform"` + } `json:"deploy"` + + Migration struct { + ID string `json:"_id"` + Version int `json:"version"` + Locked bool `json:"locked"` + LockedAt time.Time `json:"lockedAt"` + BuildAt time.Time `json:"buildAt"` + } `json:"migration"` + + InstanceCount int `json:"instanceCount"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"_updatedAt"` +} + +type StatisticsInfo struct { + Statistics Statistics `json:"statistics"` +} + +type StatisticsList struct { + Statistics []Statistics `json:"statistics"` + + Pagination +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/message.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/message.go new file mode 100644 index 00000000..8be3e3b6 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/message.go @@ -0,0 +1,75 @@ +package models + +import "time" + +type Message struct { + ID string `json:"_id"` + RoomID string `json:"rid"` + Msg string `json:"msg"` + EditedBy string `json:"editedBy,omitempty"` + + Groupable bool `json:"groupable,omitempty"` + + EditedAt *time.Time `json:"editedAt,omitempty"` + Timestamp *time.Time `json:"ts,omitempty"` + UpdatedAt *time.Time `json:"_updatedAt,omitempty"` + + Mentions []User `json:"mentions,omitempty"` + User *User `json:"u,omitempty"` + PostMessage + + // Bot interface{} `json:"bot"` + // CustomFields interface{} `json:"customFields"` + // Channels []interface{} `json:"channels"` + // SandstormSessionID interface{} `json:"sandstormSessionId"` +} + +// PostMessage Payload for postmessage rest API +// +// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ +type PostMessage struct { + RoomID string `json:"roomId,omitempty"` + Channel string `json:"channel,omitempty"` + Text string `json:"text,omitempty"` + ParseUrls bool `json:"parseUrls,omitempty"` + Alias string `json:"alias,omitempty"` + Emoji string `json:"emoji,omitempty"` + Avatar string `json:"avatar,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` +} + +// Attachment Payload for postmessage rest API +// +// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ +type Attachment struct { + Color string `json:"color,omitempty"` + Text string `json:"text,omitempty"` + Timestamp string `json:"ts,omitempty"` + ThumbURL string `json:"thumb_url,omitempty"` + MessageLink string `json:"message_link,omitempty"` + Collapsed bool `json:"collapsed"` + + AuthorName string `json:"author_name,omitempty"` + AuthorLink string `json:"author_link,omitempty"` + AuthorIcon string `json:"author_icon,omitempty"` + + Title string `json:"title,omitempty"` + TitleLink string `json:"title_link,omitempty"` + TitleLinkDownload string `json:"title_link_download,omitempty"` + + ImageURL string `json:"image_url,omitempty"` + + AudioURL string `json:"audio_url,omitempty"` + VideoURL string `json:"video_url,omitempty"` + + Fields []AttachmentField `json:"fields,omitempty"` +} + +// AttachmentField Payload for postmessage rest API +// +// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ +type AttachmentField struct { + Short bool `json:"short"` + Title string `json:"title"` + Value string `json:"value"` +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/permission.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/permission.go new file mode 100644 index 00000000..052bad8a --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/permission.go @@ -0,0 +1,7 @@ +package models + +type Permission struct { + ID string `json:"_id"` + UpdatedAt string `json:"_updatedAt.$date"` + Roles []string `json:"roles"` +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/setting.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/setting.go new file mode 100644 index 00000000..aeacb385 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/setting.go @@ -0,0 +1,21 @@ +package models + +type Setting struct { + ID string `json:"_id"` + Blocked bool `json:"blocked"` + Group string `json:"group"` + Hidden bool `json:"hidden"` + Public bool `json:"public"` + Type string `json:"type"` + PackageValue string `json:"packageValue"` + Sorter int `json:"sorter"` + Value string `json:"value"` + ValueBool bool `json:"valueBool"` + ValueInt float64 `json:"valueInt"` + ValueSource string `json:"valueSource"` + ValueAsset Asset `json:"asset"` +} + +type Asset struct { + DefaultUrl string `json:"defaultUrl"` +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/user.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/user.go new file mode 100644 index 00000000..ee56bdc3 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/user.go @@ -0,0 +1,29 @@ +package models + +type User struct { + ID string `json:"_id"` + Name string `json:"name"` + UserName string `json:"username"` + Status string `json:"status"` + Token string `json:"token"` + TokenExpires int64 `json:"tokenExpires"` +} + +type CreateUserRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` + Username string `json:"username"` + CustomFields map[string]string `json:"customFields,omitempty"` +} + +type UpdateUserRequest struct { + UserID string `json:"userId"` + Data struct { + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` + Username string `json:"username"` + CustomFields map[string]string `json:"customFields,omitempty"` + } `json:"data"` +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/userCredentials.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/userCredentials.go new file mode 100644 index 00000000..296e26fb --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/models/userCredentials.go @@ -0,0 +1,10 @@ +package models + +type UserCredentials struct { + ID string `json:"id"` + Token string `json:"token"` + + Email string `json:"email"` + Name string `json:"name"` + Password string `json:"pass"` +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/channels.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/channels.go new file mode 100644 index 00000000..5779cb38 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/channels.go @@ -0,0 +1,263 @@ +package realtime + +import ( + "github.com/Jeffail/gabs" + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +func (c *Client) GetChannelId(name string) (string, error) { + rawResponse, err := c.ddp.Call("getRoomIdByNameOrId", name) + if err != nil { + return "", err + } + + //log.Println(rawResponse) + + return rawResponse.(string), nil +} + +// GetChannelsIn returns list of channels +// Optionally includes date to get all since last check or 0 to get all +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/get-rooms/ +func (c *Client) GetChannelsIn() ([]models.Channel, error) { + rawResponse, err := c.ddp.Call("rooms/get", map[string]int{ + "$date": 0, + }) + if err != nil { + return nil, err + } + + document, _ := gabs.Consume(rawResponse.(map[string]interface{})["update"]) + + chans, err := document.Children() + + var channels []models.Channel + + for _, i := range chans { + channels = append(channels, models.Channel{ + ID: stringOrZero(i.Path("_id").Data()), + //Default: stringOrZero(i.Path("default").Data()), + Name: stringOrZero(i.Path("name").Data()), + Type: stringOrZero(i.Path("t").Data()), + }) + } + + return channels, nil +} + +// GetChannelSubscriptions gets users channel subscriptions +// Optionally includes date to get all since last check or 0 to get all +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/get-subscriptions +func (c *Client) GetChannelSubscriptions() ([]models.ChannelSubscription, error) { + rawResponse, err := c.ddp.Call("subscriptions/get", map[string]int{ + "$date": 0, + }) + if err != nil { + return nil, err + } + + document, _ := gabs.Consume(rawResponse.(map[string]interface{})["update"]) + + channelSubs, err := document.Children() + + var channelSubscriptions []models.ChannelSubscription + + for _, sub := range channelSubs { + channelSubscription := models.ChannelSubscription{ + ID: stringOrZero(sub.Path("_id").Data()), + Alert: sub.Path("alert").Data().(bool), + Name: stringOrZero(sub.Path("name").Data()), + DisplayName: stringOrZero(sub.Path("fname").Data()), + Open: sub.Path("open").Data().(bool), + Type: stringOrZero(sub.Path("t").Data()), + User: models.User{ + ID: stringOrZero(sub.Path("u._id").Data()), + UserName: stringOrZero(sub.Path("u.username").Data()), + }, + Unread: sub.Path("unread").Data().(float64), + } + + if sub.Path("roles").Data() != nil { + var roles []string + for _, role := range sub.Path("roles").Data().([]interface{}) { + roles = append(roles, role.(string)) + } + + channelSubscription.Roles = roles + } + + channelSubscriptions = append(channelSubscriptions, channelSubscription) + } + + return channelSubscriptions, nil +} + +// GetChannelRoles returns room roles +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/get-room-roles +func (c *Client) GetChannelRoles(roomId string) error { + _, err := c.ddp.Call("getRoomRoles", roomId) + if err != nil { + return err + } + + return nil +} + +// CreateChannel creates a channel +// Takes name and users array +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/create-channels +func (c *Client) CreateChannel(name string, users []string) error { + _, err := c.ddp.Call("createChannel", name, users) + if err != nil { + return err + } + + return nil +} + +// CreateGroup creates a private group +// Takes group name and array of users +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/create-private-groups +func (c *Client) CreateGroup(name string, users []string) error { + _, err := c.ddp.Call("createPrivateGroup", name, users) + if err != nil { + return err + } + + return nil +} + +// JoinChannel joins a channel +// Takes roomId +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/joining-channels +func (c *Client) JoinChannel(roomId string) error { + _, err := c.ddp.Call("joinRoom", roomId) + if err != nil { + return err + } + + return nil +} + +// LeaveChannel leaves a channel +// Takes roomId +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/leaving-rooms +func (c *Client) LeaveChannel(roomId string) error { + _, err := c.ddp.Call("leaveRoom", roomId) + if err != nil { + return err + } + + return nil +} + +// ArchiveChannel archives the channel +// Takes roomId +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/archive-rooms +func (c *Client) ArchiveChannel(roomId string) error { + _, err := c.ddp.Call("archiveRoom", roomId) + if err != nil { + return err + } + + return nil +} + +// UnArchiveChannel unarchives the channel +// Takes roomId +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/unarchive-rooms +func (c *Client) UnArchiveChannel(roomId string) error { + _, err := c.ddp.Call("unarchiveRoom", roomId) + if err != nil { + return err + } + + return nil +} + +// DeleteChannel deletes the channel +// Takes roomId +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/delete-rooms +func (c *Client) DeleteChannel(roomId string) error { + _, err := c.ddp.Call("eraseRoom", roomId) + if err != nil { + return err + } + + return nil +} + +// SetChannelTopic sets channel topic +// takes roomId and topic +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/save-room-settings +func (c *Client) SetChannelTopic(roomId string, topic string) error { + _, err := c.ddp.Call("saveRoomSettings", roomId, "roomTopic", topic) + if err != nil { + return err + } + + return nil +} + +// SetChannelType sets the channel type +// takes roomId and roomType +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/save-room-settings +func (c *Client) SetChannelType(roomId string, roomType string) error { + _, err := c.ddp.Call("saveRoomSettings", roomId, "roomType", roomType) + if err != nil { + return err + } + + return nil +} + +// SetChannelJoinCode sets channel join code +// takes roomId and joinCode +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/save-room-settings +func (c *Client) SetChannelJoinCode(roomId string, joinCode string) error { + _, err := c.ddp.Call("saveRoomSettings", roomId, "joinCode", joinCode) + if err != nil { + return err + } + + return nil +} + +// SetChannelReadOnly sets channel as read only +// takes roomId and boolean +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/save-room-settings +func (c *Client) SetChannelReadOnly(roomId string, readOnly bool) error { + _, err := c.ddp.Call("saveRoomSettings", roomId, "readOnly", readOnly) + if err != nil { + return err + } + + return nil +} + +// SetChannelDescription sets channels description +// takes roomId and description +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/save-room-settings +func (c *Client) SetChannelDescription(roomId string, description string) error { + _, err := c.ddp.Call("saveRoomSettings", roomId, "roomDescription", description) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/client.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/client.go new file mode 100644 index 00000000..1dde80bf --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/client.go @@ -0,0 +1,96 @@ +// Provides access to Rocket.Chat's realtime API via ddp +package realtime + +import ( + "fmt" + "math/rand" + "net/url" + "strconv" + "time" + + "github.com/gopackage/ddp" +) + +type Client struct { + ddp *ddp.Client +} + +// Creates a new instance and connects to the websocket. +func NewClient(serverURL *url.URL, debug bool) (*Client, error) { + rand.Seed(time.Now().UTC().UnixNano()) + + wsURL := "ws" + port := 80 + + if serverURL.Scheme == "https" { + wsURL = "wss" + port = 443 + } + + if len(serverURL.Port()) > 0 { + port, _ = strconv.Atoi(serverURL.Port()) + } + + wsURL = fmt.Sprintf("%s://%v:%v%s/websocket", wsURL, serverURL.Hostname(), port, serverURL.Path) + + // log.Println("About to connect to:", wsURL, port, serverURL.Scheme) + + c := new(Client) + c.ddp = ddp.NewClient(wsURL, serverURL.String()) + + if debug { + c.ddp.SetSocketLogActive(true) + } + + if err := c.ddp.Connect(); err != nil { + return nil, err + } + + return c, nil +} + +type statusListener struct { + listener func(int) +} + +func (s statusListener) Status(status int) { + s.listener(status) +} + +func (c *Client) AddStatusListener(listener func(int)) { + c.ddp.AddStatusListener(statusListener{listener: listener}) +} + +func (c *Client) Reconnect() { + c.ddp.Reconnect() +} + +// ConnectionAway sets connection status to away +func (c *Client) ConnectionAway() error { + _, err := c.ddp.Call("UserPresence:away") + if err != nil { + return err + } + + return nil +} + +// ConnectionOnline sets connection status to online +func (c *Client) ConnectionOnline() error { + _, err := c.ddp.Call("UserPresence:online") + if err != nil { + return err + } + + return nil +} + +// Close closes the ddp session +func (c *Client) Close() { + c.ddp.Close() +} + +// Some of the rocketchat objects need unique IDs specified by the client +func (c *Client) newRandomId() string { + return fmt.Sprintf("%f", rand.Float64()) +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/emoji.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/emoji.go new file mode 100644 index 00000000..90f2c6ee --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/emoji.go @@ -0,0 +1,10 @@ +package realtime + +func (c *Client) getCustomEmoji() error { + _, err := c.ddp.Call("listEmojiCustom") + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/events.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/events.go new file mode 100644 index 00000000..f3c945cf --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/events.go @@ -0,0 +1,21 @@ +package realtime + +import "fmt" + +func (c *Client) StartTyping(roomId string, username string) error { + _, err := c.ddp.Call("stream-notify-room", fmt.Sprintf("%s/typing", roomId), username, true) + if err != nil { + return err + } + + return nil +} + +func (c *Client) StopTyping(roomId string, username string) error { + _, err := c.ddp.Call("stream-notify-room", fmt.Sprintf("%s/typing", roomId), username, false) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/messages.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/messages.go new file mode 100644 index 00000000..9c0c9bb4 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/messages.go @@ -0,0 +1,240 @@ +package realtime + +import ( + "fmt" + "strconv" + "time" + + "github.com/Jeffail/gabs" + "github.com/gopackage/ddp" + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +const ( + // RocketChat doesn't send the `added` event for new messages by default, only `changed`. + send_added_event = true + default_buffer_size = 100 +) + +// LoadHistory loads history +// Takes roomId +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/load-history +func (c *Client) LoadHistory(roomId string) error { + _, err := c.ddp.Call("loadHistory", roomId) + if err != nil { + return err + } + + return nil +} + +// SendMessage sends message to channel +// takes channel and message +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/send-message +func (c *Client) SendMessage(m *models.Message) (*models.Message, error) { + m.ID = c.newRandomId() + + rawResponse, err := c.ddp.Call("sendMessage", m) + if err != nil { + return nil, err + } + + return getMessageFromData(rawResponse.(map[string]interface{})), nil +} + +// EditMessage edits a message +// takes message object +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/update-message +func (c *Client) EditMessage(message *models.Message) error { + _, err := c.ddp.Call("updateMessage", message) + if err != nil { + return err + } + + return nil +} + +// DeleteMessage deletes a message +// takes a message object +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/delete-message +func (c *Client) DeleteMessage(message *models.Message) error { + _, err := c.ddp.Call("deleteMessage", map[string]string{ + "_id": message.ID, + }) + if err != nil { + return err + } + + return nil +} + +// ReactToMessage adds a reaction to a message +// takes a message and emoji +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/set-reaction +func (c *Client) ReactToMessage(message *models.Message, reaction string) error { + _, err := c.ddp.Call("setReaction", reaction, message.ID) + if err != nil { + return err + } + + return nil +} + +// StarMessage stars message +// takes a message object +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/star-message +func (c *Client) StarMessage(message *models.Message) error { + _, err := c.ddp.Call("starMessage", map[string]interface{}{ + "_id": message.ID, + "rid": message.RoomID, + "starred": true, + }) + + if err != nil { + return err + } + + return nil +} + +// UnStarMessage unstars message +// takes message object +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/star-message +func (c *Client) UnStarMessage(message *models.Message) error { + _, err := c.ddp.Call("starMessage", map[string]interface{}{ + "_id": message.ID, + "rid": message.RoomID, + "starred": false, + }) + + if err != nil { + return err + } + + return nil +} + +// PinMessage pins a message +// takes a message object +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/pin-message +func (c *Client) PinMessage(message *models.Message) error { + _, err := c.ddp.Call("pinMessage", message) + + if err != nil { + return err + } + + return nil +} + +// UnPinMessage unpins message +// takes a message object +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/unpin-messages +func (c *Client) UnPinMessage(message *models.Message) error { + _, err := c.ddp.Call("unpinMessage", message) + + if err != nil { + return err + } + + return nil +} + +// SubscribeToMessageStream Subscribes to the message updates of a channel +// Returns a buffered channel +// +// https://rocket.chat/docs/developer-guides/realtime-api/subscriptions/stream-room-messages/ +func (c *Client) SubscribeToMessageStream(channel *models.Channel, msgChannel chan models.Message) error { + + if err := c.ddp.Sub("stream-room-messages", channel.ID, send_added_event); err != nil { + return err + } + + //msgChannel := make(chan models.Message, default_buffer_size) + c.ddp.CollectionByName("stream-room-messages").AddUpdateListener(messageExtractor{msgChannel, "update"}) + + return nil +} + +func getMessagesFromUpdateEvent(update ddp.Update) []models.Message { + document, _ := gabs.Consume(update["args"]) + args, err := document.Children() + + if err != nil { + // log.Printf("Event arguments are in an unexpected format: %v", err) + return make([]models.Message, 0) + } + + messages := make([]models.Message, len(args)) + + for i, arg := range args { + messages[i] = *getMessageFromDocument(arg) + } + + return messages +} + +func getMessageFromData(data interface{}) *models.Message { + // TODO: We should know what this will look like, we shouldn't need to use gabs + document, _ := gabs.Consume(data) + return getMessageFromDocument(document) +} + +func getMessageFromDocument(arg *gabs.Container) *models.Message { + var ts *time.Time + date := stringOrZero(arg.Path("ts.$date").Data()) + if len(date) > 0 { + if ti, err := strconv.ParseFloat(date, 64); err == nil { + t := time.Unix(int64(ti)/1e3, int64(ti)%1e3) + ts = &t + } + } + return &models.Message{ + ID: stringOrZero(arg.Path("_id").Data()), + RoomID: stringOrZero(arg.Path("rid").Data()), + Msg: stringOrZero(arg.Path("msg").Data()), + Timestamp: ts, + User: &models.User{ + ID: stringOrZero(arg.Path("u._id").Data()), + UserName: stringOrZero(arg.Path("u.username").Data()), + }, + } +} + +func stringOrZero(i interface{}) string { + if i == nil { + return "" + } + + switch i.(type) { + case string: + return i.(string) + case float64: + return fmt.Sprintf("%f", i.(float64)) + default: + return "" + } +} + +type messageExtractor struct { + messageChannel chan models.Message + operation string +} + +func (u messageExtractor) CollectionUpdate(collection, operation, id string, doc ddp.Update) { + if operation == u.operation { + msgs := getMessagesFromUpdateEvent(doc) + for _, m := range msgs { + u.messageChannel <- m + } + } +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/permissions.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/permissions.go new file mode 100644 index 00000000..fc5df3da --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/permissions.go @@ -0,0 +1,54 @@ +package realtime + +import ( + "github.com/Jeffail/gabs" + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +// GetPermissions gets permissions +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/get-permissions +func (c *Client) GetPermissions() ([]models.Permission, error) { + rawResponse, err := c.ddp.Call("permissions/get") + if err != nil { + return nil, err + } + + document, _ := gabs.Consume(rawResponse) + + perms, _ := document.Children() + + var permissions []models.Permission + + for _, permission := range perms { + var roles []string + for _, role := range permission.Path("roles").Data().([]interface{}) { + roles = append(roles, role.(string)) + } + + permissions = append(permissions, models.Permission{ + ID: stringOrZero(permission.Path("_id").Data()), + Roles: roles, + }) + } + + return permissions, nil +} + +// GetUserRoles gets current users roles +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/get-user-roles +func (c *Client) GetUserRoles() error { + rawResponse, err := c.ddp.Call("getUserRoles") + if err != nil { + return err + } + + document, _ := gabs.Consume(rawResponse) + + _, err = document.Children() + // TODO: Figure out if this function is even useful if so return it + //log.Println(roles) + + return nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/settings.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/settings.go new file mode 100644 index 00000000..c37eedbd --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/settings.go @@ -0,0 +1,53 @@ +package realtime + +import ( + "github.com/Jeffail/gabs" + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +// GetPublicSettings gets public settings +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/get-public-settings +func (c *Client) GetPublicSettings() ([]models.Setting, error) { + rawResponse, err := c.ddp.Call("public-settings/get") + if err != nil { + return nil, err + } + + document, _ := gabs.Consume(rawResponse) + + sett, _ := document.Children() + + var settings []models.Setting + + for _, rawSetting := range sett { + setting := models.Setting{ + ID: stringOrZero(rawSetting.Path("_id").Data()), + Type: stringOrZero(rawSetting.Path("type").Data()), + } + + switch setting.Type { + case "boolean": + setting.ValueBool = rawSetting.Path("value").Data().(bool) + case "string": + setting.Value = stringOrZero(rawSetting.Path("value").Data()) + case "code": + setting.Value = stringOrZero(rawSetting.Path("value").Data()) + case "color": + setting.Value = stringOrZero(rawSetting.Path("value").Data()) + case "int": + setting.ValueInt = rawSetting.Path("value").Data().(float64) + case "asset": + setting.ValueAsset = models.Asset{ + DefaultUrl: stringOrZero(rawSetting.Path("value").Data().(map[string]interface{})["defaultUrl"]), + } + + default: + // log.Println(setting.Type, rawSetting.Path("value").Data()) + } + + settings = append(settings, setting) + } + + return settings, nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/subscriptions.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/subscriptions.go new file mode 100644 index 00000000..5013e53d --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/subscriptions.go @@ -0,0 +1,41 @@ +package realtime + +import ( + "fmt" + + "github.com/gopackage/ddp" +) + +// Subscribes to stream-notify-logged +// Returns a buffered channel +// +// https://rocket.chat/docs/developer-guides/realtime-api/subscriptions/stream-room-messages/ +func (c *Client) Sub(name string, args ...interface{}) (chan string, error) { + + if args == nil { + //log.Println("no args passed") + if err := c.ddp.Sub(name); err != nil { + return nil, err + } + } else { + if err := c.ddp.Sub(name, args[0], false); err != nil { + return nil, err + } + } + + msgChannel := make(chan string, default_buffer_size) + c.ddp.CollectionByName("stream-room-messages").AddUpdateListener(genericExtractor{msgChannel, "update"}) + + return msgChannel, nil +} + +type genericExtractor struct { + messageChannel chan string + operation string +} + +func (u genericExtractor) CollectionUpdate(collection, operation, id string, doc ddp.Update) { + if operation == u.operation { + u.messageChannel <- fmt.Sprintf("%s -> update", collection) + } +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/users.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/users.go new file mode 100644 index 00000000..09a4f1f4 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/realtime/users.go @@ -0,0 +1,103 @@ +package realtime + +import ( + "crypto/sha256" + "encoding/hex" + "strconv" + + "github.com/Jeffail/gabs" + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +type ddpLoginRequest struct { + User ddpUser `json:"user"` + Password ddpPassword `json:"password"` +} + +type ddpTokenLoginRequest struct { + Token string `json:"resume"` +} + +type ddpUser struct { + Email string `json:"email"` +} + +type ddpPassword struct { + Digest string `json:"digest"` + Algorithm string `json:"algorithm"` +} + +// RegisterUser a new user on the server. This function does not need a logged in user. The registered user gets logged in +// to set its username. +func (c *Client) RegisterUser(credentials *models.UserCredentials) (*models.User, error) { + + if _, err := c.ddp.Call("registerUser", credentials); err != nil { + return nil, err + } + + user, err := c.Login(credentials) + if err != nil { + return nil, err + } + + if _, err := c.ddp.Call("setUsername", credentials.Name); err != nil { + return nil, err + } + + return user, nil +} + +// Login a user. +// token shouldn't be nil, otherwise the password and the email are not allowed to be nil. +// +// https://rocket.chat/docs/developer-guides/realtime-api/method-calls/login/ +func (c *Client) Login(credentials *models.UserCredentials) (*models.User, error) { + var request interface{} + if credentials.Token != "" { + request = ddpTokenLoginRequest{ + Token: credentials.Token, + } + } else { + digest := sha256.Sum256([]byte(credentials.Password)) + request = ddpLoginRequest{ + User: ddpUser{Email: credentials.Email}, + Password: ddpPassword{ + Digest: hex.EncodeToString(digest[:]), + Algorithm: "sha-256", + }, + } + } + + rawResponse, err := c.ddp.Call("login", request) + if err != nil { + return nil, err + } + + user := getUserFromData(rawResponse.(map[string]interface{})) + if credentials.Token == "" { + credentials.ID, credentials.Token = user.ID, user.Token + } + + return user, nil +} + +func getUserFromData(data interface{}) *models.User { + document, _ := gabs.Consume(data) + + expires, _ := strconv.ParseFloat(stringOrZero(document.Path("tokenExpires.$date").Data()), 64) + return &models.User{ + ID: stringOrZero(document.Path("id").Data()), + Token: stringOrZero(document.Path("token").Data()), + TokenExpires: int64(expires), + } +} + +// SetPresence set user presence +func (c *Client) SetPresence(status string) error { + _, err := c.ddp.Call("UserPresence:setDefaultStatus", status) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/channels.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/channels.go new file mode 100644 index 00000000..71377500 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/channels.go @@ -0,0 +1,64 @@ +package rest + +import ( + "bytes" + "fmt" + "net/url" + + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +type ChannelsResponse struct { + Status + models.Pagination + Channels []models.Channel `json:"channels"` +} + +type ChannelResponse struct { + Status + Channel models.Channel `json:"channel"` +} + +// GetPublicChannels returns all channels that can be seen by the logged in user. +// +// https://rocket.chat/docs/developer-guides/rest-api/channels/list +func (c *Client) GetPublicChannels() (*ChannelsResponse, error) { + response := new(ChannelsResponse) + if err := c.Get("channels.list", nil, response); err != nil { + return nil, err + } + + return response, nil +} + +// GetJoinedChannels returns all channels that the user has joined. +// +// https://rocket.chat/docs/developer-guides/rest-api/channels/list-joined +func (c *Client) GetJoinedChannels(params url.Values) (*ChannelsResponse, error) { + response := new(ChannelsResponse) + if err := c.Get("channels.list.joined", params, response); err != nil { + return nil, err + } + + return response, nil +} + +// LeaveChannel leaves a channel. The id of the channel has to be not nil. +// +// https://rocket.chat/docs/developer-guides/rest-api/channels/leave +func (c *Client) LeaveChannel(channel *models.Channel) error { + var body = fmt.Sprintf(`{ "roomId": "%s"}`, channel.ID) + return c.Post("channels.leave", bytes.NewBufferString(body), new(ChannelResponse)) +} + +// GetChannelInfo get information about a channel. That might be useful to update the usernames. +// +// https://rocket.chat/docs/developer-guides/rest-api/channels/info +func (c *Client) GetChannelInfo(channel *models.Channel) (*models.Channel, error) { + response := new(ChannelResponse) + if err := c.Get("channels.info", url.Values{"roomId": []string{channel.ID}}, response); err != nil { + return nil, err + } + + return &response.Channel, nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/client.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/client.go new file mode 100644 index 00000000..0e37123e --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/client.go @@ -0,0 +1,176 @@ +//Package rest provides a RocketChat rest client. +package rest + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" +) + +var ( + ResponseErr = fmt.Errorf("got false response") +) + +type Response interface { + OK() error +} + +type Client struct { + Protocol string + Host string + Path string + Port string + Version string + + // Use this switch to see all network communication. + Debug bool + + auth *authInfo +} + +type Status struct { + Success bool `json:"success"` + Error string `json:"error"` + + Status string `json:"status"` + Message string `json:"message"` +} + +type authInfo struct { + token string + id string +} + +func (s Status) OK() error { + if s.Success { + return nil + } + + if len(s.Error) > 0 { + return fmt.Errorf(s.Error) + } + + if s.Status == "success" { + return nil + } + + if len(s.Message) > 0 { + return fmt.Errorf("status: %s, message: %s", s.Status, s.Message) + } + return ResponseErr +} + +// StatusResponse The base for the most of the json responses +type StatusResponse struct { + Status + Channel string `json:"channel"` +} + +func NewClient(serverUrl *url.URL, debug bool) *Client { + protocol := "http" + port := "80" + + if serverUrl.Scheme == "https" { + protocol = "https" + port = "443" + } + + if len(serverUrl.Port()) > 0 { + port = serverUrl.Port() + } + + return &Client{Host: serverUrl.Hostname(), Path: serverUrl.Path, Port: port, Protocol: protocol, Version: "v1", Debug: debug} +} + +func (c *Client) getUrl() string { + if len(c.Version) == 0 { + c.Version = "v1" + } + return fmt.Sprintf("%v://%v:%v%s/api/%s", c.Protocol, c.Host, c.Port, c.Path, c.Version) +} + +// Get call Get +func (c *Client) Get(api string, params url.Values, response Response) error { + return c.doRequest(http.MethodGet, api, params, nil, response) +} + +// Post call as JSON +func (c *Client) Post(api string, body io.Reader, response Response) error { + return c.doRequest(http.MethodPost, api, nil, body, response) +} + +// PostForm call as Form Data +func (c *Client) PostForm(api string, params url.Values, response Response) error { + return c.doRequest(http.MethodPost, api, params, nil, response) +} + +func (c *Client) doRequest(method, api string, params url.Values, body io.Reader, response Response) error { + contentType := "application/x-www-form-urlencoded" + if method == http.MethodPost { + if body != nil { + contentType = "application/json" + } else if len(params) > 0 { + body = bytes.NewBufferString(params.Encode()) + } + } + + request, err := http.NewRequest(method, c.getUrl()+"/"+api, body) + if err != nil { + return err + } + + if method == http.MethodGet { + if len(params) > 0 { + request.URL.RawQuery = params.Encode() + } + } else { + request.Header.Set("Content-Type", contentType) + } + + if c.auth != nil { + request.Header.Set("X-Auth-Token", c.auth.token) + request.Header.Set("X-User-Id", c.auth.id) + } + + if c.Debug { + log.Println(request) + } + + resp, err := http.DefaultClient.Do(request) + + if err != nil { + return err + } + + defer resp.Body.Close() + bodyBytes, err := ioutil.ReadAll(resp.Body) + + if c.Debug { + log.Println(string(bodyBytes)) + } + + var parse bool + if err == nil { + if e := json.Unmarshal(bodyBytes, response); e == nil { + parse = true + } + } + if resp.StatusCode != http.StatusOK { + if parse { + return response.OK() + } + return errors.New("Request error: " + resp.Status) + } + + if err != nil { + return err + } + + return response.OK() +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/information.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/information.go new file mode 100644 index 00000000..dd831c85 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/information.go @@ -0,0 +1,98 @@ +package rest + +import ( + "net/url" + + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +type InfoResponse struct { + Status + Info models.Info `json:"info"` +} + +// GetServerInfo a simple method, requires no authentication, +// that returns information about the server including version information. +// +// https://rocket.chat/docs/developer-guides/rest-api/miscellaneous/info +func (c *Client) GetServerInfo() (*models.Info, error) { + response := new(InfoResponse) + if err := c.Get("info", nil, response); err != nil { + return nil, err + } + + return &response.Info, nil +} + +type DirectoryResponse struct { + Status + models.Directory +} + +// GetDirectory a method, that searches by users or channels on all users and channels available on server. +// It supports the Offset, Count, and Sort Query Parameters along with Query and Fields Query Parameters. +// +// https://rocket.chat/docs/developer-guides/rest-api/miscellaneous/directory +func (c *Client) GetDirectory(params url.Values) (*models.Directory, error) { + response := new(DirectoryResponse) + if err := c.Get("directory", params, response); err != nil { + return nil, err + } + + return &response.Directory, nil +} + +type SpotlightResponse struct { + Status + models.Spotlight +} + +// GetSpotlight searches for users or rooms that are visible to the user. +// WARNING: It will only return rooms that user didn’t join yet. +// +// https://rocket.chat/docs/developer-guides/rest-api/miscellaneous/spotlight +func (c *Client) GetSpotlight(params url.Values) (*models.Spotlight, error) { + response := new(SpotlightResponse) + if err := c.Get("spotlight", params, response); err != nil { + return nil, err + } + + return &response.Spotlight, nil +} + +type StatisticsResponse struct { + Status + models.StatisticsInfo +} + +// GetStatistics +// Statistics about the Rocket.Chat server. +// +// https://rocket.chat/docs/developer-guides/rest-api/miscellaneous/statistics +func (c *Client) GetStatistics() (*models.StatisticsInfo, error) { + response := new(StatisticsResponse) + if err := c.Get("statistics", nil, response); err != nil { + return nil, err + } + + return &response.StatisticsInfo, nil +} + +type StatisticsListResponse struct { + Status + models.StatisticsList +} + +// GetStatisticsList +// Selectable statistics about the Rocket.Chat server. +// It supports the Offset, Count and Sort Query Parameters along with just the Fields and Query Parameters. +// +// https://rocket.chat/docs/developer-guides/rest-api/miscellaneous/statistics.list +func (c *Client) GetStatisticsList(params url.Values) (*models.StatisticsList, error) { + response := new(StatisticsListResponse) + if err := c.Get("statistics.list", params, response); err != nil { + return nil, err + } + + return &response.StatisticsList, nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/messages.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/messages.go new file mode 100644 index 00000000..b3ad5846 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/messages.go @@ -0,0 +1,67 @@ +package rest + +import ( + "bytes" + "encoding/json" + "fmt" + "html" + "net/url" + "strconv" + + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +type MessagesResponse struct { + Status + Messages []models.Message `json:"messages"` +} + +type MessageResponse struct { + Status + Message models.Message `json:"message"` +} + +// Sends a message to a channel. The name of the channel has to be not nil. +// The message will be html escaped. +// +// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage +func (c *Client) Send(channel *models.Channel, msg string) error { + body := fmt.Sprintf(`{ "channel": "%s", "text": "%s"}`, channel.Name, html.EscapeString(msg)) + return c.Post("chat.postMessage", bytes.NewBufferString(body), new(MessageResponse)) +} + +// PostMessage send a message to a channel. The channel or roomId has to be not nil. +// The message will be json encode. +// +// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage +func (c *Client) PostMessage(msg *models.PostMessage) (*MessageResponse, error) { + body, err := json.Marshal(msg) + if err != nil { + return nil, err + } + + response := new(MessageResponse) + err = c.Post("chat.postMessage", bytes.NewBuffer(body), response) + return response, err +} + +// Get messages from a channel. The channel id has to be not nil. Optionally a +// count can be specified to limit the size of the returned messages. +// +// https://rocket.chat/docs/developer-guides/rest-api/channels/history +func (c *Client) GetMessages(channel *models.Channel, page *models.Pagination) ([]models.Message, error) { + params := url.Values{ + "roomId": []string{channel.ID}, + } + + if page != nil { + params.Add("count", strconv.Itoa(page.Count)) + } + + response := new(MessagesResponse) + if err := c.Get("channels.history", params, response); err != nil { + return nil, err + } + + return response.Messages, nil +} diff --git a/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/users.go b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/users.go new file mode 100644 index 00000000..dcf783a0 --- /dev/null +++ b/vendor/github.com/matterbridge/Rocket.Chat.Go.SDK/rest/users.go @@ -0,0 +1,145 @@ +package rest + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + "time" + + "github.com/matterbridge/Rocket.Chat.Go.SDK/models" +) + +type logoutResponse struct { + Status + Data struct { + Message string `json:"message"` + } `json:"data"` +} + +type logonResponse struct { + Status + Data struct { + Token string `json:"authToken"` + UserID string `json:"userID"` + } `json:"data"` +} + +type CreateUserResponse struct { + Status + User struct { + ID string `json:"_id"` + CreatedAt time.Time `json:"createdAt"` + Services struct { + Password struct { + Bcrypt string `json:"bcrypt"` + } `json:"password"` + } `json:"services"` + Username string `json:"username"` + Emails []struct { + Address string `json:"address"` + Verified bool `json:"verified"` + } `json:"emails"` + Type string `json:"type"` + Status string `json:"status"` + Active bool `json:"active"` + Roles []string `json:"roles"` + UpdatedAt time.Time `json:"_updatedAt"` + Name string `json:"name"` + CustomFields map[string]string `json:"customFields"` + } `json:"user"` +} + +// Login a user. The Email and the Password are mandatory. The auth token of the user is stored in the Client instance. +// +// https://rocket.chat/docs/developer-guides/rest-api/authentication/login +func (c *Client) Login(credentials *models.UserCredentials) error { + if c.auth != nil { + return nil + } + + if credentials.ID != "" && credentials.Token != "" { + c.auth = &authInfo{id: credentials.ID, token: credentials.Token} + return nil + } + + response := new(logonResponse) + data := url.Values{"user": {credentials.Email}, "password": {credentials.Password}} + if err := c.PostForm("login", data, response); err != nil { + return err + } + + c.auth = &authInfo{id: response.Data.UserID, token: response.Data.Token} + credentials.ID, credentials.Token = response.Data.UserID, response.Data.Token + return nil +} + +// CreateToken creates an access token for a user +// +// https://rocket.chat/docs/developer-guides/rest-api/users/createtoken/ +func (c *Client) CreateToken(userID, username string) (*models.UserCredentials, error) { + response := new(logonResponse) + data := url.Values{"userId": {userID}, "username": {username}} + if err := c.PostForm("users.createToken", data, response); err != nil { + return nil, err + } + credentials := &models.UserCredentials{} + credentials.ID, credentials.Token = response.Data.UserID, response.Data.Token + return credentials, nil +} + +// Logout a user. The function returns the response message of the server. +// +// https://rocket.chat/docs/developer-guides/rest-api/authentication/logout +func (c *Client) Logout() (string, error) { + + if c.auth == nil { + return "Was not logged in", nil + } + + response := new(logoutResponse) + if err := c.Get("logout", nil, response); err != nil { + return "", err + } + + return response.Data.Message, nil +} + +// CreateUser being logged in with a user that has permission to do so. +// +// https://rocket.chat/docs/developer-guides/rest-api/users/create +func (c *Client) CreateUser(req *models.CreateUserRequest) (*CreateUserResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + + response := new(CreateUserResponse) + err = c.Post("users.create", bytes.NewBuffer(body), response) + return response, err +} + +// UpdateUser updates a user's data being logged in with a user that has permission to do so. +// +// https://rocket.chat/docs/developer-guides/rest-api/users/update/ +func (c *Client) UpdateUser(req *models.UpdateUserRequest) (*CreateUserResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + + response := new(CreateUserResponse) + err = c.Post("users.update", bytes.NewBuffer(body), response) + return response, err +} + +// SetUserAvatar updates a user's avatar being logged in with a user that has permission to do so. +// Currently only passing an URL is possible. +// +// https://rocket.chat/docs/developer-guides/rest-api/users/setavatar/ +func (c *Client) SetUserAvatar(userID, username, avatarURL string) (*Status, error) { + body := fmt.Sprintf(`{ "userId": "%s","username": "%s","avatarUrl":"%s"}`, userID, username, avatarURL) + response := new(Status) + err := c.Post("users.setAvatar", bytes.NewBufferString(body), response) + return response, err +} diff --git a/vendor/github.com/nelsonken/gomf/README.md b/vendor/github.com/nelsonken/gomf/README.md new file mode 100644 index 00000000..237e9370 --- /dev/null +++ b/vendor/github.com/nelsonken/gomf/README.md @@ -0,0 +1,37 @@ +# golang 可多文件上传的request builder 库 + +## 测试方法 + +1. start php upload server: php -S 127.0.0.1:8080 ./ +2. run go test -v + +## 使用方法 + +```go + fb := gomf.New() + fb.WriteField("name", "accountName") + fb.WriteField("password", "pwd") + fb.WriteFile("picture", "icon.png", "image/jpeg", []byte(strings.Repeat("0", 100))) + + log.Println(fb.GetBuffer().String()) + + req, err := fb.GetHTTPRequest(context.Background(), "http://127.0.0.1:8080/up.php") + if err != nil { + log.Fatal(err) + } + res, err := http.DefaultClient.Do(req) + + log.Println(res.StatusCode) + log.Println(res.Status) + + if err != nil { + log.Fatal(err) + } + + b, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + + log.Println(string(b)) +``` diff --git a/vendor/github.com/nelsonken/gomf/form_builder.go b/vendor/github.com/nelsonken/gomf/form_builder.go new file mode 100644 index 00000000..9b0d2294 --- /dev/null +++ b/vendor/github.com/nelsonken/gomf/form_builder.go @@ -0,0 +1,89 @@ +package gomf + +import ( + "bytes" + "context" + "fmt" + "mime/multipart" + "net/http" + "net/textproto" +) + +type FormBuilder struct { + w *multipart.Writer + b *bytes.Buffer +} + +func New() *FormBuilder { + buf := new(bytes.Buffer) + writer := multipart.NewWriter(buf) + return &FormBuilder{ + w: writer, + b: buf, + } +} + +func (ufw *FormBuilder) WriteField(name, value string) error { + w, err := ufw.w.CreateFormField(name) + if err != nil { + return err + } + + _, err = w.Write([]byte(value)) + if err != nil { + return err + } + + return nil +} + +// WriteFile if contentType is empty-string, will auto convert to application/octet-stream +func (ufw *FormBuilder) WriteFile(fieldName, fileName, contentType string, content []byte) error { + if contentType == "" { + contentType = "application/octet-stream" + } + + wx, err := ufw.w.CreatePart(textproto.MIMEHeader{ + "Content-Type": []string{ + contentType, + }, + "Content-Disposition": []string{ + fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, fileName), + }, + }) + if err != nil { + return err + } + + _, err = wx.Write(content) + if err != nil { + return err + } + + return nil +} + +func (fb *FormBuilder) Close() error { + return fb.w.Close() +} + +func (fb *FormBuilder) GetBuffer() *bytes.Buffer { + return fb.b +} + +func (fb *FormBuilder) GetHTTPRequest(ctx context.Context, reqURL string) (*http.Request, error) { + err := fb.Close() + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", reqURL, fb.b) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", fb.w.FormDataContentType()) + req = req.WithContext(ctx) + + return req, nil +} diff --git a/vendor/github.com/nelsonken/gomf/up.php b/vendor/github.com/nelsonken/gomf/up.php new file mode 100644 index 00000000..e56e5bbb --- /dev/null +++ b/vendor/github.com/nelsonken/gomf/up.php @@ -0,0 +1,33 @@ + 0) { + echo "Return Code: " . $_FILES["picture"]["error"] . "\n"; +} else { + echo "Upload: " . $_FILES["picture"]["name"] . "\n"; + echo "Type: " . $_FILES["picture"]["type"] . "\n"; + echo "Size: " . ($_FILES["picture"]["size"] / 1024) . " Kb\n"; + echo "Temp file: " . $_FILES["picture"]["tmp_name"] . "\n>"; + + if (file_exists($_FILES["picture"]["name"])) + { + echo $_FILES["picture"]["name"] . " already exists. \n"; + } + else + { + move_uploaded_file($_FILES["picture"]["tmp_name"], $_FILES["picture"]["name"]); + echo "Stored in: " . $_FILES["picture"]["name"] . "\n"; + } +} + + + + diff --git a/vendor/golang.org/x/net/AUTHORS b/vendor/golang.org/x/net/AUTHORS new file mode 100644 index 00000000..15167cd7 --- /dev/null +++ b/vendor/golang.org/x/net/AUTHORS @@ -0,0 +1,3 @@ +# This source code refers to The Go Authors for copyright purposes. +# The master list of authors is in the main Go distribution, +# visible at http://tip.golang.org/AUTHORS. diff --git a/vendor/golang.org/x/net/CONTRIBUTORS b/vendor/golang.org/x/net/CONTRIBUTORS new file mode 100644 index 00000000..1c4577e9 --- /dev/null +++ b/vendor/golang.org/x/net/CONTRIBUTORS @@ -0,0 +1,3 @@ +# This source code was written by the Go contributors. +# The master list of contributors is in the main Go distribution, +# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/vendor/golang.org/x/net/LICENSE b/vendor/golang.org/x/net/LICENSE new file mode 100644 index 00000000..6a66aea5 --- /dev/null +++ b/vendor/golang.org/x/net/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/net/PATENTS b/vendor/golang.org/x/net/PATENTS new file mode 100644 index 00000000..73309904 --- /dev/null +++ b/vendor/golang.org/x/net/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/net/websocket/client.go b/vendor/golang.org/x/net/websocket/client.go new file mode 100644 index 00000000..69a4ac7e --- /dev/null +++ b/vendor/golang.org/x/net/websocket/client.go @@ -0,0 +1,106 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "io" + "net" + "net/http" + "net/url" +) + +// DialError is an error that occurs while dialling a websocket server. +type DialError struct { + *Config + Err error +} + +func (e *DialError) Error() string { + return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error() +} + +// NewConfig creates a new WebSocket config for client connection. +func NewConfig(server, origin string) (config *Config, err error) { + config = new(Config) + config.Version = ProtocolVersionHybi13 + config.Location, err = url.ParseRequestURI(server) + if err != nil { + return + } + config.Origin, err = url.ParseRequestURI(origin) + if err != nil { + return + } + config.Header = http.Header(make(map[string][]string)) + return +} + +// NewClient creates a new WebSocket client connection over rwc. +func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) { + br := bufio.NewReader(rwc) + bw := bufio.NewWriter(rwc) + err = hybiClientHandshake(config, br, bw) + if err != nil { + return + } + buf := bufio.NewReadWriter(br, bw) + ws = newHybiClientConn(config, buf, rwc) + return +} + +// Dial opens a new client connection to a WebSocket. +func Dial(url_, protocol, origin string) (ws *Conn, err error) { + config, err := NewConfig(url_, origin) + if err != nil { + return nil, err + } + if protocol != "" { + config.Protocol = []string{protocol} + } + return DialConfig(config) +} + +var portMap = map[string]string{ + "ws": "80", + "wss": "443", +} + +func parseAuthority(location *url.URL) string { + if _, ok := portMap[location.Scheme]; ok { + if _, _, err := net.SplitHostPort(location.Host); err != nil { + return net.JoinHostPort(location.Host, portMap[location.Scheme]) + } + } + return location.Host +} + +// DialConfig opens a new client connection to a WebSocket with a config. +func DialConfig(config *Config) (ws *Conn, err error) { + var client net.Conn + if config.Location == nil { + return nil, &DialError{config, ErrBadWebSocketLocation} + } + if config.Origin == nil { + return nil, &DialError{config, ErrBadWebSocketOrigin} + } + dialer := config.Dialer + if dialer == nil { + dialer = &net.Dialer{} + } + client, err = dialWithDialer(dialer, config) + if err != nil { + goto Error + } + ws, err = NewClient(config, client) + if err != nil { + client.Close() + goto Error + } + return + +Error: + return nil, &DialError{config, err} +} diff --git a/vendor/golang.org/x/net/websocket/dial.go b/vendor/golang.org/x/net/websocket/dial.go new file mode 100644 index 00000000..2dab943a --- /dev/null +++ b/vendor/golang.org/x/net/websocket/dial.go @@ -0,0 +1,24 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "crypto/tls" + "net" +) + +func dialWithDialer(dialer *net.Dialer, config *Config) (conn net.Conn, err error) { + switch config.Location.Scheme { + case "ws": + conn, err = dialer.Dial("tcp", parseAuthority(config.Location)) + + case "wss": + conn, err = tls.DialWithDialer(dialer, "tcp", parseAuthority(config.Location), config.TlsConfig) + + default: + err = ErrBadScheme + } + return +} diff --git a/vendor/golang.org/x/net/websocket/hybi.go b/vendor/golang.org/x/net/websocket/hybi.go new file mode 100644 index 00000000..8cffdd16 --- /dev/null +++ b/vendor/golang.org/x/net/websocket/hybi.go @@ -0,0 +1,583 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +// This file implements a protocol of hybi draft. +// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +const ( + websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + closeStatusNormal = 1000 + closeStatusGoingAway = 1001 + closeStatusProtocolError = 1002 + closeStatusUnsupportedData = 1003 + closeStatusFrameTooLarge = 1004 + closeStatusNoStatusRcvd = 1005 + closeStatusAbnormalClosure = 1006 + closeStatusBadMessageData = 1007 + closeStatusPolicyViolation = 1008 + closeStatusTooBigData = 1009 + closeStatusExtensionMismatch = 1010 + + maxControlFramePayloadLength = 125 +) + +var ( + ErrBadMaskingKey = &ProtocolError{"bad masking key"} + ErrBadPongMessage = &ProtocolError{"bad pong message"} + ErrBadClosingStatus = &ProtocolError{"bad closing status"} + ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"} + ErrNotImplemented = &ProtocolError{"not implemented"} + + handshakeHeader = map[string]bool{ + "Host": true, + "Upgrade": true, + "Connection": true, + "Sec-Websocket-Key": true, + "Sec-Websocket-Origin": true, + "Sec-Websocket-Version": true, + "Sec-Websocket-Protocol": true, + "Sec-Websocket-Accept": true, + } +) + +// A hybiFrameHeader is a frame header as defined in hybi draft. +type hybiFrameHeader struct { + Fin bool + Rsv [3]bool + OpCode byte + Length int64 + MaskingKey []byte + + data *bytes.Buffer +} + +// A hybiFrameReader is a reader for hybi frame. +type hybiFrameReader struct { + reader io.Reader + + header hybiFrameHeader + pos int64 + length int +} + +func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) { + n, err = frame.reader.Read(msg) + if frame.header.MaskingKey != nil { + for i := 0; i < n; i++ { + msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4] + frame.pos++ + } + } + return n, err +} + +func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode } + +func (frame *hybiFrameReader) HeaderReader() io.Reader { + if frame.header.data == nil { + return nil + } + if frame.header.data.Len() == 0 { + return nil + } + return frame.header.data +} + +func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil } + +func (frame *hybiFrameReader) Len() (n int) { return frame.length } + +// A hybiFrameReaderFactory creates new frame reader based on its frame type. +type hybiFrameReaderFactory struct { + *bufio.Reader +} + +// NewFrameReader reads a frame header from the connection, and creates new reader for the frame. +// See Section 5.2 Base Framing protocol for detail. +// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2 +func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) { + hybiFrame := new(hybiFrameReader) + frame = hybiFrame + var header []byte + var b byte + // First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits) + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0 + for i := 0; i < 3; i++ { + j := uint(6 - i) + hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0 + } + hybiFrame.header.OpCode = header[0] & 0x0f + + // Second byte. Mask/Payload len(7bits) + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + mask := (b & 0x80) != 0 + b &= 0x7f + lengthFields := 0 + switch { + case b <= 125: // Payload length 7bits. + hybiFrame.header.Length = int64(b) + case b == 126: // Payload length 7+16bits + lengthFields = 2 + case b == 127: // Payload length 7+64bits + lengthFields = 8 + } + for i := 0; i < lengthFields; i++ { + b, err = buf.ReadByte() + if err != nil { + return + } + if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits + b &= 0x7f + } + header = append(header, b) + hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b) + } + if mask { + // Masking key. 4 bytes. + for i := 0; i < 4; i++ { + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b) + } + } + hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length) + hybiFrame.header.data = bytes.NewBuffer(header) + hybiFrame.length = len(header) + int(hybiFrame.header.Length) + return +} + +// A HybiFrameWriter is a writer for hybi frame. +type hybiFrameWriter struct { + writer *bufio.Writer + + header *hybiFrameHeader +} + +func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) { + var header []byte + var b byte + if frame.header.Fin { + b |= 0x80 + } + for i := 0; i < 3; i++ { + if frame.header.Rsv[i] { + j := uint(6 - i) + b |= 1 << j + } + } + b |= frame.header.OpCode + header = append(header, b) + if frame.header.MaskingKey != nil { + b = 0x80 + } else { + b = 0 + } + lengthFields := 0 + length := len(msg) + switch { + case length <= 125: + b |= byte(length) + case length < 65536: + b |= 126 + lengthFields = 2 + default: + b |= 127 + lengthFields = 8 + } + header = append(header, b) + for i := 0; i < lengthFields; i++ { + j := uint((lengthFields - i - 1) * 8) + b = byte((length >> j) & 0xff) + header = append(header, b) + } + if frame.header.MaskingKey != nil { + if len(frame.header.MaskingKey) != 4 { + return 0, ErrBadMaskingKey + } + header = append(header, frame.header.MaskingKey...) + frame.writer.Write(header) + data := make([]byte, length) + for i := range data { + data[i] = msg[i] ^ frame.header.MaskingKey[i%4] + } + frame.writer.Write(data) + err = frame.writer.Flush() + return length, err + } + frame.writer.Write(header) + frame.writer.Write(msg) + err = frame.writer.Flush() + return length, err +} + +func (frame *hybiFrameWriter) Close() error { return nil } + +type hybiFrameWriterFactory struct { + *bufio.Writer + needMaskingKey bool +} + +func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) { + frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType} + if buf.needMaskingKey { + frameHeader.MaskingKey, err = generateMaskingKey() + if err != nil { + return nil, err + } + } + return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil +} + +type hybiFrameHandler struct { + conn *Conn + payloadType byte +} + +func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) { + if handler.conn.IsServerConn() { + // The client MUST mask all frames sent to the server. + if frame.(*hybiFrameReader).header.MaskingKey == nil { + handler.WriteClose(closeStatusProtocolError) + return nil, io.EOF + } + } else { + // The server MUST NOT mask all frames. + if frame.(*hybiFrameReader).header.MaskingKey != nil { + handler.WriteClose(closeStatusProtocolError) + return nil, io.EOF + } + } + if header := frame.HeaderReader(); header != nil { + io.Copy(ioutil.Discard, header) + } + switch frame.PayloadType() { + case ContinuationFrame: + frame.(*hybiFrameReader).header.OpCode = handler.payloadType + case TextFrame, BinaryFrame: + handler.payloadType = frame.PayloadType() + case CloseFrame: + return nil, io.EOF + case PingFrame, PongFrame: + b := make([]byte, maxControlFramePayloadLength) + n, err := io.ReadFull(frame, b) + if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { + return nil, err + } + io.Copy(ioutil.Discard, frame) + if frame.PayloadType() == PingFrame { + if _, err := handler.WritePong(b[:n]); err != nil { + return nil, err + } + } + return nil, nil + } + return frame, nil +} + +func (handler *hybiFrameHandler) WriteClose(status int) (err error) { + handler.conn.wio.Lock() + defer handler.conn.wio.Unlock() + w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame) + if err != nil { + return err + } + msg := make([]byte, 2) + binary.BigEndian.PutUint16(msg, uint16(status)) + _, err = w.Write(msg) + w.Close() + return err +} + +func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) { + handler.conn.wio.Lock() + defer handler.conn.wio.Unlock() + w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame) + if err != nil { + return 0, err + } + n, err = w.Write(msg) + w.Close() + return n, err +} + +// newHybiConn creates a new WebSocket connection speaking hybi draft protocol. +func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + if buf == nil { + br := bufio.NewReader(rwc) + bw := bufio.NewWriter(rwc) + buf = bufio.NewReadWriter(br, bw) + } + ws := &Conn{config: config, request: request, buf: buf, rwc: rwc, + frameReaderFactory: hybiFrameReaderFactory{buf.Reader}, + frameWriterFactory: hybiFrameWriterFactory{ + buf.Writer, request == nil}, + PayloadType: TextFrame, + defaultCloseStatus: closeStatusNormal} + ws.frameHandler = &hybiFrameHandler{conn: ws} + return ws +} + +// generateMaskingKey generates a masking key for a frame. +func generateMaskingKey() (maskingKey []byte, err error) { + maskingKey = make([]byte, 4) + if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil { + return + } + return +} + +// generateNonce generates a nonce consisting of a randomly selected 16-byte +// value that has been base64-encoded. +func generateNonce() (nonce []byte) { + key := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + panic(err) + } + nonce = make([]byte, 24) + base64.StdEncoding.Encode(nonce, key) + return +} + +// removeZone removes IPv6 zone identifer from host. +// E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080" +func removeZone(host string) string { + if !strings.HasPrefix(host, "[") { + return host + } + i := strings.LastIndex(host, "]") + if i < 0 { + return host + } + j := strings.LastIndex(host[:i], "%") + if j < 0 { + return host + } + return host[:j] + host[i:] +} + +// getNonceAccept computes the base64-encoded SHA-1 of the concatenation of +// the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string. +func getNonceAccept(nonce []byte) (expected []byte, err error) { + h := sha1.New() + if _, err = h.Write(nonce); err != nil { + return + } + if _, err = h.Write([]byte(websocketGUID)); err != nil { + return + } + expected = make([]byte, 28) + base64.StdEncoding.Encode(expected, h.Sum(nil)) + return +} + +// Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17 +func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { + bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") + + // According to RFC 6874, an HTTP client, proxy, or other + // intermediary must remove any IPv6 zone identifier attached + // to an outgoing URI. + bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n") + bw.WriteString("Upgrade: websocket\r\n") + bw.WriteString("Connection: Upgrade\r\n") + nonce := generateNonce() + if config.handshakeData != nil { + nonce = []byte(config.handshakeData["key"]) + } + bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n") + bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") + + if config.Version != ProtocolVersionHybi13 { + return ErrBadProtocolVersion + } + + bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n") + if len(config.Protocol) > 0 { + bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n") + } + // TODO(ukai): send Sec-WebSocket-Extensions. + err = config.Header.WriteSubset(bw, handshakeHeader) + if err != nil { + return err + } + + bw.WriteString("\r\n") + if err = bw.Flush(); err != nil { + return err + } + + resp, err := http.ReadResponse(br, &http.Request{Method: "GET"}) + if err != nil { + return err + } + if resp.StatusCode != 101 { + return ErrBadStatus + } + if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" || + strings.ToLower(resp.Header.Get("Connection")) != "upgrade" { + return ErrBadUpgrade + } + expectedAccept, err := getNonceAccept(nonce) + if err != nil { + return err + } + if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) { + return ErrChallengeResponse + } + if resp.Header.Get("Sec-WebSocket-Extensions") != "" { + return ErrUnsupportedExtensions + } + offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol") + if offeredProtocol != "" { + protocolMatched := false + for i := 0; i < len(config.Protocol); i++ { + if config.Protocol[i] == offeredProtocol { + protocolMatched = true + break + } + } + if !protocolMatched { + return ErrBadWebSocketProtocol + } + config.Protocol = []string{offeredProtocol} + } + + return nil +} + +// newHybiClientConn creates a client WebSocket connection after handshake. +func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn { + return newHybiConn(config, buf, rwc, nil) +} + +// A HybiServerHandshaker performs a server handshake using hybi draft protocol. +type hybiServerHandshaker struct { + *Config + accept []byte +} + +func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) { + c.Version = ProtocolVersionHybi13 + if req.Method != "GET" { + return http.StatusMethodNotAllowed, ErrBadRequestMethod + } + // HTTP version can be safely ignored. + + if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" || + !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") { + return http.StatusBadRequest, ErrNotWebSocket + } + + key := req.Header.Get("Sec-Websocket-Key") + if key == "" { + return http.StatusBadRequest, ErrChallengeResponse + } + version := req.Header.Get("Sec-Websocket-Version") + switch version { + case "13": + c.Version = ProtocolVersionHybi13 + default: + return http.StatusBadRequest, ErrBadWebSocketVersion + } + var scheme string + if req.TLS != nil { + scheme = "wss" + } else { + scheme = "ws" + } + c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI()) + if err != nil { + return http.StatusBadRequest, err + } + protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol")) + if protocol != "" { + protocols := strings.Split(protocol, ",") + for i := 0; i < len(protocols); i++ { + c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i])) + } + } + c.accept, err = getNonceAccept([]byte(key)) + if err != nil { + return http.StatusInternalServerError, err + } + return http.StatusSwitchingProtocols, nil +} + +// Origin parses the Origin header in req. +// If the Origin header is not set, it returns nil and nil. +func Origin(config *Config, req *http.Request) (*url.URL, error) { + var origin string + switch config.Version { + case ProtocolVersionHybi13: + origin = req.Header.Get("Origin") + } + if origin == "" { + return nil, nil + } + return url.ParseRequestURI(origin) +} + +func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) { + if len(c.Protocol) > 0 { + if len(c.Protocol) != 1 { + // You need choose a Protocol in Handshake func in Server. + return ErrBadWebSocketProtocol + } + } + buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n") + buf.WriteString("Upgrade: websocket\r\n") + buf.WriteString("Connection: Upgrade\r\n") + buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n") + if len(c.Protocol) > 0 { + buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n") + } + // TODO(ukai): send Sec-WebSocket-Extensions. + if c.Header != nil { + err := c.Header.WriteSubset(buf, handshakeHeader) + if err != nil { + return err + } + } + buf.WriteString("\r\n") + return buf.Flush() +} + +func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + return newHybiServerConn(c.Config, buf, rwc, request) +} + +// newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol. +func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + return newHybiConn(config, buf, rwc, request) +} diff --git a/vendor/golang.org/x/net/websocket/server.go b/vendor/golang.org/x/net/websocket/server.go new file mode 100644 index 00000000..0895dea1 --- /dev/null +++ b/vendor/golang.org/x/net/websocket/server.go @@ -0,0 +1,113 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "fmt" + "io" + "net/http" +) + +func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) { + var hs serverHandshaker = &hybiServerHandshaker{Config: config} + code, err := hs.ReadHandshake(buf.Reader, req) + if err == ErrBadWebSocketVersion { + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion) + buf.WriteString("\r\n") + buf.WriteString(err.Error()) + buf.Flush() + return + } + if err != nil { + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.WriteString(err.Error()) + buf.Flush() + return + } + if handshake != nil { + err = handshake(config, req) + if err != nil { + code = http.StatusForbidden + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.Flush() + return + } + } + err = hs.AcceptHandshake(buf.Writer) + if err != nil { + code = http.StatusBadRequest + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.Flush() + return + } + conn = hs.NewServerConn(buf, rwc, req) + return +} + +// Server represents a server of a WebSocket. +type Server struct { + // Config is a WebSocket configuration for new WebSocket connection. + Config + + // Handshake is an optional function in WebSocket handshake. + // For example, you can check, or don't check Origin header. + // Another example, you can select config.Protocol. + Handshake func(*Config, *http.Request) error + + // Handler handles a WebSocket connection. + Handler +} + +// ServeHTTP implements the http.Handler interface for a WebSocket +func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s.serveWebSocket(w, req) +} + +func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { + rwc, buf, err := w.(http.Hijacker).Hijack() + if err != nil { + panic("Hijack failed: " + err.Error()) + } + // The server should abort the WebSocket connection if it finds + // the client did not send a handshake that matches with protocol + // specification. + defer rwc.Close() + conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) + if err != nil { + return + } + if conn == nil { + panic("unexpected nil conn") + } + s.Handler(conn) +} + +// Handler is a simple interface to a WebSocket browser client. +// It checks if Origin header is valid URL by default. +// You might want to verify websocket.Conn.Config().Origin in the func. +// If you use Server instead of Handler, you could call websocket.Origin and +// check the origin in your Handshake func. So, if you want to accept +// non-browser clients, which do not send an Origin header, set a +// Server.Handshake that does not check the origin. +type Handler func(*Conn) + +func checkOrigin(config *Config, req *http.Request) (err error) { + config.Origin, err = Origin(config, req) + if err == nil && config.Origin == nil { + return fmt.Errorf("null origin") + } + return err +} + +// ServeHTTP implements the http.Handler interface for a WebSocket +func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s := Server{Handler: h, Handshake: checkOrigin} + s.serveWebSocket(w, req) +} diff --git a/vendor/golang.org/x/net/websocket/websocket.go b/vendor/golang.org/x/net/websocket/websocket.go new file mode 100644 index 00000000..e242c89a --- /dev/null +++ b/vendor/golang.org/x/net/websocket/websocket.go @@ -0,0 +1,448 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package websocket implements a client and server for the WebSocket protocol +// as specified in RFC 6455. +// +// This package currently lacks some features found in an alternative +// and more actively maintained WebSocket package: +// +// https://godoc.org/github.com/gorilla/websocket +// +package websocket // import "golang.org/x/net/websocket" + +import ( + "bufio" + "crypto/tls" + "encoding/json" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "sync" + "time" +) + +const ( + ProtocolVersionHybi13 = 13 + ProtocolVersionHybi = ProtocolVersionHybi13 + SupportedProtocolVersion = "13" + + ContinuationFrame = 0 + TextFrame = 1 + BinaryFrame = 2 + CloseFrame = 8 + PingFrame = 9 + PongFrame = 10 + UnknownFrame = 255 + + DefaultMaxPayloadBytes = 32 << 20 // 32MB +) + +// ProtocolError represents WebSocket protocol errors. +type ProtocolError struct { + ErrorString string +} + +func (err *ProtocolError) Error() string { return err.ErrorString } + +var ( + ErrBadProtocolVersion = &ProtocolError{"bad protocol version"} + ErrBadScheme = &ProtocolError{"bad scheme"} + ErrBadStatus = &ProtocolError{"bad status"} + ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"} + ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"} + ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"} + ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"} + ErrBadWebSocketVersion = &ProtocolError{"missing or bad WebSocket Version"} + ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"} + ErrBadFrame = &ProtocolError{"bad frame"} + ErrBadFrameBoundary = &ProtocolError{"not on frame boundary"} + ErrNotWebSocket = &ProtocolError{"not websocket protocol"} + ErrBadRequestMethod = &ProtocolError{"bad method"} + ErrNotSupported = &ProtocolError{"not supported"} +) + +// ErrFrameTooLarge is returned by Codec's Receive method if payload size +// exceeds limit set by Conn.MaxPayloadBytes +var ErrFrameTooLarge = errors.New("websocket: frame payload size exceeds limit") + +// Addr is an implementation of net.Addr for WebSocket. +type Addr struct { + *url.URL +} + +// Network returns the network type for a WebSocket, "websocket". +func (addr *Addr) Network() string { return "websocket" } + +// Config is a WebSocket configuration +type Config struct { + // A WebSocket server address. + Location *url.URL + + // A Websocket client origin. + Origin *url.URL + + // WebSocket subprotocols. + Protocol []string + + // WebSocket protocol version. + Version int + + // TLS config for secure WebSocket (wss). + TlsConfig *tls.Config + + // Additional header fields to be sent in WebSocket opening handshake. + Header http.Header + + // Dialer used when opening websocket connections. + Dialer *net.Dialer + + handshakeData map[string]string +} + +// serverHandshaker is an interface to handle WebSocket server side handshake. +type serverHandshaker interface { + // ReadHandshake reads handshake request message from client. + // Returns http response code and error if any. + ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) + + // AcceptHandshake accepts the client handshake request and sends + // handshake response back to client. + AcceptHandshake(buf *bufio.Writer) (err error) + + // NewServerConn creates a new WebSocket connection. + NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) (conn *Conn) +} + +// frameReader is an interface to read a WebSocket frame. +type frameReader interface { + // Reader is to read payload of the frame. + io.Reader + + // PayloadType returns payload type. + PayloadType() byte + + // HeaderReader returns a reader to read header of the frame. + HeaderReader() io.Reader + + // TrailerReader returns a reader to read trailer of the frame. + // If it returns nil, there is no trailer in the frame. + TrailerReader() io.Reader + + // Len returns total length of the frame, including header and trailer. + Len() int +} + +// frameReaderFactory is an interface to creates new frame reader. +type frameReaderFactory interface { + NewFrameReader() (r frameReader, err error) +} + +// frameWriter is an interface to write a WebSocket frame. +type frameWriter interface { + // Writer is to write payload of the frame. + io.WriteCloser +} + +// frameWriterFactory is an interface to create new frame writer. +type frameWriterFactory interface { + NewFrameWriter(payloadType byte) (w frameWriter, err error) +} + +type frameHandler interface { + HandleFrame(frame frameReader) (r frameReader, err error) + WriteClose(status int) (err error) +} + +// Conn represents a WebSocket connection. +// +// Multiple goroutines may invoke methods on a Conn simultaneously. +type Conn struct { + config *Config + request *http.Request + + buf *bufio.ReadWriter + rwc io.ReadWriteCloser + + rio sync.Mutex + frameReaderFactory + frameReader + + wio sync.Mutex + frameWriterFactory + + frameHandler + PayloadType byte + defaultCloseStatus int + + // MaxPayloadBytes limits the size of frame payload received over Conn + // by Codec's Receive method. If zero, DefaultMaxPayloadBytes is used. + MaxPayloadBytes int +} + +// Read implements the io.Reader interface: +// it reads data of a frame from the WebSocket connection. +// if msg is not large enough for the frame data, it fills the msg and next Read +// will read the rest of the frame data. +// it reads Text frame or Binary frame. +func (ws *Conn) Read(msg []byte) (n int, err error) { + ws.rio.Lock() + defer ws.rio.Unlock() +again: + if ws.frameReader == nil { + frame, err := ws.frameReaderFactory.NewFrameReader() + if err != nil { + return 0, err + } + ws.frameReader, err = ws.frameHandler.HandleFrame(frame) + if err != nil { + return 0, err + } + if ws.frameReader == nil { + goto again + } + } + n, err = ws.frameReader.Read(msg) + if err == io.EOF { + if trailer := ws.frameReader.TrailerReader(); trailer != nil { + io.Copy(ioutil.Discard, trailer) + } + ws.frameReader = nil + goto again + } + return n, err +} + +// Write implements the io.Writer interface: +// it writes data as a frame to the WebSocket connection. +func (ws *Conn) Write(msg []byte) (n int, err error) { + ws.wio.Lock() + defer ws.wio.Unlock() + w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType) + if err != nil { + return 0, err + } + n, err = w.Write(msg) + w.Close() + return n, err +} + +// Close implements the io.Closer interface. +func (ws *Conn) Close() error { + err := ws.frameHandler.WriteClose(ws.defaultCloseStatus) + err1 := ws.rwc.Close() + if err != nil { + return err + } + return err1 +} + +func (ws *Conn) IsClientConn() bool { return ws.request == nil } +func (ws *Conn) IsServerConn() bool { return ws.request != nil } + +// LocalAddr returns the WebSocket Origin for the connection for client, or +// the WebSocket location for server. +func (ws *Conn) LocalAddr() net.Addr { + if ws.IsClientConn() { + return &Addr{ws.config.Origin} + } + return &Addr{ws.config.Location} +} + +// RemoteAddr returns the WebSocket location for the connection for client, or +// the Websocket Origin for server. +func (ws *Conn) RemoteAddr() net.Addr { + if ws.IsClientConn() { + return &Addr{ws.config.Location} + } + return &Addr{ws.config.Origin} +} + +var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn") + +// SetDeadline sets the connection's network read & write deadlines. +func (ws *Conn) SetDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetDeadline(t) + } + return errSetDeadline +} + +// SetReadDeadline sets the connection's network read deadline. +func (ws *Conn) SetReadDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetReadDeadline(t) + } + return errSetDeadline +} + +// SetWriteDeadline sets the connection's network write deadline. +func (ws *Conn) SetWriteDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetWriteDeadline(t) + } + return errSetDeadline +} + +// Config returns the WebSocket config. +func (ws *Conn) Config() *Config { return ws.config } + +// Request returns the http request upgraded to the WebSocket. +// It is nil for client side. +func (ws *Conn) Request() *http.Request { return ws.request } + +// Codec represents a symmetric pair of functions that implement a codec. +type Codec struct { + Marshal func(v interface{}) (data []byte, payloadType byte, err error) + Unmarshal func(data []byte, payloadType byte, v interface{}) (err error) +} + +// Send sends v marshaled by cd.Marshal as single frame to ws. +func (cd Codec) Send(ws *Conn, v interface{}) (err error) { + data, payloadType, err := cd.Marshal(v) + if err != nil { + return err + } + ws.wio.Lock() + defer ws.wio.Unlock() + w, err := ws.frameWriterFactory.NewFrameWriter(payloadType) + if err != nil { + return err + } + _, err = w.Write(data) + w.Close() + return err +} + +// Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores +// in v. The whole frame payload is read to an in-memory buffer; max size of +// payload is defined by ws.MaxPayloadBytes. If frame payload size exceeds +// limit, ErrFrameTooLarge is returned; in this case frame is not read off wire +// completely. The next call to Receive would read and discard leftover data of +// previous oversized frame before processing next frame. +func (cd Codec) Receive(ws *Conn, v interface{}) (err error) { + ws.rio.Lock() + defer ws.rio.Unlock() + if ws.frameReader != nil { + _, err = io.Copy(ioutil.Discard, ws.frameReader) + if err != nil { + return err + } + ws.frameReader = nil + } +again: + frame, err := ws.frameReaderFactory.NewFrameReader() + if err != nil { + return err + } + frame, err = ws.frameHandler.HandleFrame(frame) + if err != nil { + return err + } + if frame == nil { + goto again + } + maxPayloadBytes := ws.MaxPayloadBytes + if maxPayloadBytes == 0 { + maxPayloadBytes = DefaultMaxPayloadBytes + } + if hf, ok := frame.(*hybiFrameReader); ok && hf.header.Length > int64(maxPayloadBytes) { + // payload size exceeds limit, no need to call Unmarshal + // + // set frameReader to current oversized frame so that + // the next call to this function can drain leftover + // data before processing the next frame + ws.frameReader = frame + return ErrFrameTooLarge + } + payloadType := frame.PayloadType() + data, err := ioutil.ReadAll(frame) + if err != nil { + return err + } + return cd.Unmarshal(data, payloadType, v) +} + +func marshal(v interface{}) (msg []byte, payloadType byte, err error) { + switch data := v.(type) { + case string: + return []byte(data), TextFrame, nil + case []byte: + return data, BinaryFrame, nil + } + return nil, UnknownFrame, ErrNotSupported +} + +func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) { + switch data := v.(type) { + case *string: + *data = string(msg) + return nil + case *[]byte: + *data = msg + return nil + } + return ErrNotSupported +} + +/* +Message is a codec to send/receive text/binary data in a frame on WebSocket connection. +To send/receive text frame, use string type. +To send/receive binary frame, use []byte type. + +Trivial usage: + + import "websocket" + + // receive text frame + var message string + websocket.Message.Receive(ws, &message) + + // send text frame + message = "hello" + websocket.Message.Send(ws, message) + + // receive binary frame + var data []byte + websocket.Message.Receive(ws, &data) + + // send binary frame + data = []byte{0, 1, 2} + websocket.Message.Send(ws, data) + +*/ +var Message = Codec{marshal, unmarshal} + +func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) { + msg, err = json.Marshal(v) + return msg, TextFrame, err +} + +func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) { + return json.Unmarshal(msg, v) +} + +/* +JSON is a codec to send/receive JSON data in a frame from a WebSocket connection. + +Trivial usage: + + import "websocket" + + type T struct { + Msg string + Count int + } + + // receive JSON type T + var data T + websocket.JSON.Receive(ws, &data) + + // send JSON type T + websocket.JSON.Send(ws, data) +*/ +var JSON = Codec{jsonMarshal, jsonUnmarshal} diff --git a/vendor/modules.txt b/vendor/modules.txt index 3baabde5..a2e4a3da 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,5 +1,7 @@ # github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 github.com/42wim/go-gitter +# github.com/Jeffail/gabs v1.1.1 +github.com/Jeffail/gabs # github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 github.com/Philipp15b/go-steam github.com/Philipp15b/go-steam/protocol/steamlang @@ -30,6 +32,8 @@ github.com/golang/protobuf/protoc-gen-go/descriptor github.com/google/gops/agent github.com/google/gops/internal github.com/google/gops/signal +# github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 +github.com/gopackage/ddp # github.com/gorilla/schema v1.0.2 github.com/gorilla/schema # github.com/gorilla/websocket v1.4.0 @@ -66,6 +70,10 @@ github.com/labstack/gommon/random github.com/lrstanley/girc # github.com/magiconair/properties v1.8.0 github.com/magiconair/properties +# github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d +github.com/matterbridge/Rocket.Chat.Go.SDK/models +github.com/matterbridge/Rocket.Chat.Go.SDK/realtime +github.com/matterbridge/Rocket.Chat.Go.SDK/rest # github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 github.com/matterbridge/go-xmpp # github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea @@ -91,6 +99,8 @@ github.com/mitchellh/mapstructure github.com/mreiferson/go-httpclient # github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff github.com/mrexodia/wray +# github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 +github.com/nelsonken/gomf # github.com/nicksnyder/go-i18n v1.4.0 github.com/nicksnyder/go-i18n/i18n github.com/nicksnyder/go-i18n/i18n/bundle @@ -184,6 +194,8 @@ golang.org/x/crypto/curve25519 golang.org/x/crypto/ed25519 golang.org/x/crypto/internal/chacha20 golang.org/x/crypto/ed25519/internal/edwards25519 +# golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 +golang.org/x/net/websocket # golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc golang.org/x/sys/unix golang.org/x/sys/windows -- cgit v1.2.3