From 6ebd5cbbd8a941e0bc5f99f0d8e99cfd1d8ac0d7 Mon Sep 17 00:00:00 2001 From: Wim Date: Sun, 10 Feb 2019 17:00:11 +0100 Subject: [PATCH] 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 | 154 +++-- 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 ++++++++++++++++++ .../gopackage/ddp/ddp_collection.go | 245 +++++++ vendor/github.com/gopackage/ddp/ddp_ejson.go | 217 ++++++ .../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 ++ .../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 + .../Rocket.Chat.Go.SDK/models/user.go | 29 + .../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 +++++++ .../realtime/permissions.go | 54 ++ .../Rocket.Chat.Go.SDK/realtime/settings.go | 53 ++ .../realtime/subscriptions.go | 41 ++ .../Rocket.Chat.Go.SDK/realtime/users.go | 103 +++ .../Rocket.Chat.Go.SDK/rest/channels.go | 64 ++ .../Rocket.Chat.Go.SDK/rest/client.go | 176 +++++ .../Rocket.Chat.Go.SDK/rest/information.go | 98 +++ .../Rocket.Chat.Go.SDK/rest/messages.go | 67 ++ .../Rocket.Chat.Go.SDK/rest/users.go | 145 ++++ vendor/github.com/nelsonken/gomf/README.md | 37 + .../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 +++ .../golang.org/x/net/websocket/websocket.go | 448 ++++++++++++ vendor/modules.txt | 12 + 53 files changed, 6203 insertions(+), 45 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) - } - if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.URL != "" { - msg.Text += fi.URL - } + 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 { + 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) + smsg := &models.Message{ + RoomID: channel.ID, + Msg: msg.Text, + PostMessage: models.PostMessage{ + Avatar: msg.Avatar, + Alias: msg.Username, + }, + } + + rmsg, err := b.c.SendMessage(smsg) + if rmsg == nil { return "", err } - 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} - } + 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 0000000000000000000000000000000000000000..b6c1fad9931d5f5d8269150c097ac749ed32a73b GIT binary patch literal 167771 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⪚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

n+6tcEJ-gDvw3N$)^N|3 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 literal 0 HcmV?d00001 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