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
pull/707/head
Wim 5 years ago
parent 2cfd880cdb
commit 6ebd5cbbd8

@ -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
}

@ -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
}

@ -1,21 +1,37 @@
package brocketchat
import (
"errors"
"sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
)
type Brocketchat struct {
mh *matterhook.Client
rh *rockethook.Client
c *realtime.Client
r *rest.Client
*bridge.Config
messageChan chan models.Message
channelMap map[string]string
user *models.User
sync.RWMutex
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Brocketchat{Config: cfg}
b := &Brocketchat{Config: cfg}
b.messageChan = make(chan models.Message)
b.channelMap = make(map[string]string)
b.Log.Debugf("enabling rocketchat")
return b
}
func (b *Brocketchat) Command(cmd string) string {
@ -23,70 +39,118 @@ func (b *Brocketchat) Command(cmd string) string {
}
func (b *Brocketchat) Connect() error {
b.Log.Info("Connecting webhooks")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true})
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
go b.handleRocketHook()
if b.GetString("WebhookBindAddress") != "" {
if err := b.doConnectWebhookBind(); err != nil {
return err
}
go b.handleRocket()
return nil
}
switch {
case b.GetString("WebhookURL") != "":
if err := b.doConnectWebhookURL(); err != nil {
return err
}
go b.handleRocket()
return nil
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending and receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleRocket()
}
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
b.GetString("Login") == "" {
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Login/Password/Server configured")
}
return nil
}
func (b *Brocketchat) Disconnect() error {
return nil
}
func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
if b.c == nil {
return nil
}
id, err := b.c.GetChannelId(channel.Name)
if err != nil {
return err
}
b.Lock()
b.channelMap[id] = channel.Name
b.Unlock()
mychannel := &models.Channel{ID: id, Name: channel.Name}
if err := b.c.JoinChannel(id); err != nil {
return err
}
if err := b.c.SubscribeToMessageStream(mychannel, b.messageChan); err != nil {
return err
}
return nil
}
func (b *Brocketchat) Send(msg config.Message) (string, error) {
// ignore delete messages
channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel}
// Delete message
if msg.Event == config.EventMsgDelete {
return "", nil
if msg.ID == "" {
return "", nil
}
return msg.ID, b.c.DeleteMessage(&models.Message{ID: msg.ID})
}
b.Log.Debugf("=> Receiving %#v", msg)
// Use webhook to send the message
if b.GetString("WebhookURL") != "" {
return "", b.sendWebhook(&msg)
}
// Prepend nick if configured
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
// Edit message if we have an ID
if msg.ID != "" {
return msg.ID, b.c.EditMessage(&models.Message{ID: msg.ID, Msg: msg.Text, RoomID: b.getChannelID(msg.Channel)})
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg := rmsg // scopelint
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text}
b.mh.Send(matterMessage)
smsg := &models.Message{
RoomID: b.getChannelID(rmsg.Channel),
Msg: rmsg.Username + rmsg.Text,
PostMessage: models.PostMessage{
Avatar: rmsg.Avatar,
Alias: rmsg.Username,
},
}
if _, err := b.c.SendMessage(smsg); err != nil {
b.Log.Errorf("SendMessage failed: %s", err)
}
}
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
}
}
return "", b.handleUploadFile(&msg)
}
}
iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{IconURL: iconURL}
matterMessage.Channel = msg.Channel
matterMessage.UserName = msg.Username
matterMessage.Type = ""
matterMessage.Text = msg.Text
err := b.mh.Send(matterMessage)
if err != nil {
b.Log.Info(err)
return "", err
smsg := &models.Message{
RoomID: channel.ID,
Msg: msg.Text,
PostMessage: models.PostMessage{
Avatar: msg.Avatar,
Alias: msg.Username,
},
}
return "", nil
}
func (b *Brocketchat) handleRocketHook() {
for {
message := b.rh.Receive()
b.Log.Debugf("Receiving from rockethook %#v", message)
// do not loop
if message.UserName == b.GetString("Nick") {
continue
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.UserName, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account, UserID: message.UserID}
rmsg, err := b.c.SendMessage(smsg)
if rmsg == nil {
return "", err
}
return rmsg.ID, err
}

@ -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

@ -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=

@ -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

@ -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.

@ -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()
```

@ -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
}
//--------------------------------------------------------------------------------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

@ -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

@ -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.

@ -0,0 +1,3 @@
# ddp
MeteorJS DDP library for Golang

@ -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")
}
}
}

@ -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)
}
}

@ -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
}

@ -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"}
}

@ -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,
}
}

@ -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
}

@ -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"`
}

@ -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
}

@ -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"`
}

@ -0,0 +1,7 @@
package models
type Permission struct {
ID string `json:"_id"`
UpdatedAt string `json:"_updatedAt.$date"`
Roles []string `json:"roles"`
}

@ -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"`
}

@ -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"`
}

@ -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"`
}

@ -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
}

@ -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())
}

@ -0,0 +1,10 @@
package realtime
func (c *Client) getCustomEmoji() error {
_, err := c.ddp.Call("listEmojiCustom")
if err != nil {
return err
}
return nil
}

@ -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
}

@ -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
}
}
}

@ -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
}

@ -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
}

@ -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)
}
}

@ -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
}

@ -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
}

@ -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()
}

@ -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 didnt 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
}

@ -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
}

@ -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
}

@ -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))
```

@ -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
}

@ -0,0 +1,33 @@
<?php
/**
*
* PHP UPLOAD DEMO for gomf test
* USAGE:
* php -S 127.0.0.1:8080 -t ./
*
*/
print_r($_FILES);
if ($_FILES["picture"]["error"] > 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";
}
}

3
vendor/golang.org/x/net/AUTHORS generated vendored

@ -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.

@ -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.

27
vendor/golang.org/x/net/LICENSE generated vendored

@ -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.

22
vendor/golang.org/x/net/PATENTS generated vendored

@ -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.

@ -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}
}

@ -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
}

@ -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)
}

@ -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)
}

@ -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}

12
vendor/modules.txt vendored

@ -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

Loading…
Cancel
Save