summaryrefslogtreecommitdiffstats
path: root/model
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2016-07-12 09:36:27 -0400
committerGitHub <noreply@github.com>2016-07-12 09:36:27 -0400
commitad343a0f4ad175053f7d0da12a0587bcbb396d1c (patch)
tree8e1be00202a1d3a037ec75879538eb0ba1f25c01 /model
parent06eacf30b97aacf6544552448635b7f078d2c90b (diff)
downloadchat-ad343a0f4ad175053f7d0da12a0587bcbb396d1c.tar.gz
chat-ad343a0f4ad175053f7d0da12a0587bcbb396d1c.tar.bz2
chat-ad343a0f4ad175053f7d0da12a0587bcbb396d1c.zip
Added infrastructure for basic WebSocket API (#3432)
Diffstat (limited to 'model')
-rw-r--r--model/client.go1
-rw-r--r--model/message.go61
-rw-r--r--model/message_test.go24
-rw-r--r--model/utils.go12
-rw-r--r--model/websocket_client.go102
-rw-r--r--model/websocket_message.go114
-rw-r--r--model/websocket_message_test.go56
-rw-r--r--model/websocket_request.go43
-rw-r--r--model/websocket_request_test.go25
9 files changed, 347 insertions, 91 deletions
diff --git a/model/client.go b/model/client.go
index 2f1e846c2..0ba8913af 100644
--- a/model/client.go
+++ b/model/client.go
@@ -32,6 +32,7 @@ const (
HEADER_REQUESTED_WITH_XML = "XMLHttpRequest"
STATUS = "status"
STATUS_OK = "OK"
+ STATUS_FAIL = "FAIL"
API_URL_SUFFIX_V1 = "/api/v1"
API_URL_SUFFIX_V3 = "/api/v3"
diff --git a/model/message.go b/model/message.go
deleted file mode 100644
index 12f3be663..000000000
--- a/model/message.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package model
-
-import (
- "encoding/json"
- "io"
-)
-
-const (
- ACTION_TYPING = "typing"
- ACTION_POSTED = "posted"
- ACTION_POST_EDITED = "post_edited"
- ACTION_POST_DELETED = "post_deleted"
- ACTION_CHANNEL_DELETED = "channel_deleted"
- ACTION_CHANNEL_VIEWED = "channel_viewed"
- ACTION_DIRECT_ADDED = "direct_added"
- ACTION_NEW_USER = "new_user"
- ACTION_LEAVE_TEAM = "leave_team"
- ACTION_USER_ADDED = "user_added"
- ACTION_USER_REMOVED = "user_removed"
- ACTION_PREFERENCE_CHANGED = "preference_changed"
- ACTION_EPHEMERAL_MESSAGE = "ephemeral_message"
-)
-
-type Message struct {
- TeamId string `json:"team_id"`
- ChannelId string `json:"channel_id"`
- UserId string `json:"user_id"`
- Action string `json:"action"`
- Props map[string]string `json:"props"`
-}
-
-func (m *Message) Add(key string, value string) {
- m.Props[key] = value
-}
-
-func NewMessage(teamId string, channelId string, userId string, action string) *Message {
- return &Message{TeamId: teamId, ChannelId: channelId, UserId: userId, Action: action, Props: make(map[string]string)}
-}
-
-func (o *Message) ToJson() string {
- b, err := json.Marshal(o)
- if err != nil {
- return ""
- } else {
- return string(b)
- }
-}
-
-func MessageFromJson(data io.Reader) *Message {
- decoder := json.NewDecoder(data)
- var o Message
- err := decoder.Decode(&o)
- if err == nil {
- return &o
- } else {
- return nil
- }
-}
diff --git a/model/message_test.go b/model/message_test.go
deleted file mode 100644
index 182678d8e..000000000
--- a/model/message_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package model
-
-import (
- "strings"
- "testing"
-)
-
-func TestMessgaeJson(t *testing.T) {
- m := NewMessage(NewId(), NewId(), NewId(), ACTION_TYPING)
- m.Add("RootId", NewId())
- json := m.ToJson()
- result := MessageFromJson(strings.NewReader(json))
-
- if m.TeamId != result.TeamId {
- t.Fatal("Ids do not match")
- }
-
- if m.Props["RootId"] != result.Props["RootId"] {
- t.Fatal("Ids do not match")
- }
-}
diff --git a/model/utils.go b/model/utils.go
index 27ab3e27e..a4a4208c2 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -34,12 +34,12 @@ type EncryptStringMap map[string]string
type AppError struct {
Id string `json:"id"`
- Message string `json:"message"` // Message to be display to the end user without debugging information
- DetailedError string `json:"detailed_error"` // Internal error string to help the developer
- RequestId string `json:"request_id"` // The RequestId that's also set in the header
- StatusCode int `json:"status_code"` // The http status code
- Where string `json:"-"` // The function where it happened in the form of Struct.Func
- IsOAuth bool `json:"is_oauth"` // Whether the error is OAuth specific
+ Message string `json:"message"` // Message to be display to the end user without debugging information
+ DetailedError string `json:"detailed_error"` // Internal error string to help the developer
+ RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header
+ StatusCode int `json:"status_code,omitempty"` // The http status code
+ Where string `json:"-"` // The function where it happened in the form of Struct.Func
+ IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific
params map[string]interface{} `json:"-"`
}
diff --git a/model/websocket_client.go b/model/websocket_client.go
new file mode 100644
index 000000000..7b9dc0b50
--- /dev/null
+++ b/model/websocket_client.go
@@ -0,0 +1,102 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "github.com/gorilla/websocket"
+ "net/http"
+)
+
+type WebSocketClient struct {
+ Url string // The location of the server like "ws://localhost:8065"
+ ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3"
+ Conn *websocket.Conn // The WebSocket connection
+ AuthToken string // The token used to open the WebSocket
+ Sequence int64 // The ever-incrementing sequence attached to each WebSocket action
+ EventChannel chan *WebSocketEvent
+ ResponseChannel chan *WebSocketResponse
+}
+
+// NewWebSocketClient constructs a new WebSocket client with convienence
+// methods for talking to the server.
+func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
+ header := http.Header{}
+ header.Set(HEADER_AUTH, "BEARER "+authToken)
+ conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", header)
+ if err != nil {
+ return nil, NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
+ }
+
+ return &WebSocketClient{
+ url,
+ url + API_URL_SUFFIX,
+ conn,
+ authToken,
+ 1,
+ make(chan *WebSocketEvent, 100),
+ make(chan *WebSocketResponse, 100),
+ }, nil
+}
+
+func (wsc *WebSocketClient) Connect() *AppError {
+ header := http.Header{}
+ header.Set(HEADER_AUTH, "BEARER "+wsc.AuthToken)
+
+ var err error
+ wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ApiUrl+"/users/websocket", header)
+ if err != nil {
+ return NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func (wsc *WebSocketClient) Close() {
+ wsc.Conn.Close()
+}
+
+func (wsc *WebSocketClient) Listen() {
+ go func() {
+ for {
+ var rawMsg json.RawMessage
+ var err error
+ if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil {
+ return
+ }
+
+ var event WebSocketEvent
+ if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
+ wsc.EventChannel <- &event
+ continue
+ }
+
+ var response WebSocketResponse
+ if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
+ wsc.ResponseChannel <- &response
+ continue
+ }
+ }
+ }()
+}
+
+func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface{}) {
+ req := &WebSocketRequest{}
+ req.Seq = wsc.Sequence
+ req.Action = action
+ req.Data = data
+
+ wsc.Sequence++
+
+ wsc.Conn.WriteJSON(req)
+}
+
+func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
+ data := map[string]interface{}{
+ "channel_id": channelId,
+ "parent_id": parentId,
+ }
+
+ wsc.SendMessage("user_typing", data)
+}
diff --git a/model/websocket_message.go b/model/websocket_message.go
new file mode 100644
index 000000000..ae9a140c3
--- /dev/null
+++ b/model/websocket_message.go
@@ -0,0 +1,114 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ WEBSOCKET_EVENT_TYPING = "typing"
+ WEBSOCKET_EVENT_POSTED = "posted"
+ WEBSOCKET_EVENT_POST_EDITED = "post_edited"
+ WEBSOCKET_EVENT_POST_DELETED = "post_deleted"
+ WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted"
+ WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed"
+ WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
+ WEBSOCKET_EVENT_NEW_USER = "new_user"
+ WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
+ WEBSOCKET_EVENT_USER_ADDED = "user_added"
+ WEBSOCKET_EVENT_USER_REMOVED = "user_removed"
+ WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed"
+ WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message"
+ WEBSOCKET_EVENT_STATUS_CHANGE = "status_change"
+)
+
+type WebSocketMessage interface {
+ ToJson() string
+ IsValid() bool
+}
+
+type WebSocketEvent struct {
+ TeamId string `json:"team_id"`
+ ChannelId string `json:"channel_id"`
+ UserId string `json:"user_id"`
+ Event string `json:"event"`
+ Data map[string]interface{} `json:"data"`
+}
+
+func (m *WebSocketEvent) Add(key string, value interface{}) {
+ m.Data[key] = value
+}
+
+func NewWebSocketEvent(teamId string, channelId string, userId string, event string) *WebSocketEvent {
+ return &WebSocketEvent{TeamId: teamId, ChannelId: channelId, UserId: userId, Event: event, Data: make(map[string]interface{})}
+}
+
+func (o *WebSocketEvent) IsValid() bool {
+ return o.Event != ""
+}
+
+func (o *WebSocketEvent) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func WebSocketEventFromJson(data io.Reader) *WebSocketEvent {
+ decoder := json.NewDecoder(data)
+ var o WebSocketEvent
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+type WebSocketResponse struct {
+ Status string `json:"status"`
+ SeqReply int64 `json:"seq_reply,omitempty"`
+ Data map[string]interface{} `json:"data,omitempty"`
+ Error *AppError `json:"error,omitempty"`
+}
+
+func (m *WebSocketResponse) Add(key string, value interface{}) {
+ m.Data[key] = value
+}
+
+func NewWebSocketResponse(status string, seqReply int64, data map[string]interface{}) *WebSocketResponse {
+ return &WebSocketResponse{Status: status, SeqReply: seqReply, Data: data}
+}
+
+func NewWebSocketError(seqReply int64, err *AppError) *WebSocketResponse {
+ return &WebSocketResponse{Status: STATUS_FAIL, SeqReply: seqReply, Error: err}
+}
+
+func (o *WebSocketResponse) IsValid() bool {
+ return o.Status != ""
+}
+
+func (o *WebSocketResponse) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse {
+ decoder := json.NewDecoder(data)
+ var o WebSocketResponse
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/websocket_message_test.go b/model/websocket_message_test.go
new file mode 100644
index 000000000..cbc564b6c
--- /dev/null
+++ b/model/websocket_message_test.go
@@ -0,0 +1,56 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestWebSocketEvent(t *testing.T) {
+ m := NewWebSocketEvent(NewId(), NewId(), NewId(), "some_event")
+ m.Add("RootId", NewId())
+ json := m.ToJson()
+ result := WebSocketEventFromJson(strings.NewReader(json))
+
+ badresult := WebSocketEventFromJson(strings.NewReader("junk"))
+ if badresult != nil {
+ t.Fatal("should not have parsed")
+ }
+
+ if !m.IsValid() {
+ t.Fatal("should be valid")
+ }
+
+ if m.TeamId != result.TeamId {
+ t.Fatal("Ids do not match")
+ }
+
+ if m.Data["RootId"] != result.Data["RootId"] {
+ t.Fatal("Ids do not match")
+ }
+}
+
+func TestWebSocketResponse(t *testing.T) {
+ m := NewWebSocketResponse("OK", 1, map[string]interface{}{})
+ e := NewWebSocketError(1, &AppError{})
+ m.Add("RootId", NewId())
+ json := m.ToJson()
+ result := WebSocketResponseFromJson(strings.NewReader(json))
+ json2 := e.ToJson()
+ WebSocketResponseFromJson(strings.NewReader(json2))
+
+ badresult := WebSocketResponseFromJson(strings.NewReader("junk"))
+ if badresult != nil {
+ t.Fatal("should not have parsed")
+ }
+
+ if !m.IsValid() {
+ t.Fatal("should be valid")
+ }
+
+ if m.Data["RootId"] != result.Data["RootId"] {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/websocket_request.go b/model/websocket_request.go
new file mode 100644
index 000000000..d0f35f68b
--- /dev/null
+++ b/model/websocket_request.go
@@ -0,0 +1,43 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+
+ goi18n "github.com/nicksnyder/go-i18n/i18n"
+)
+
+type WebSocketRequest struct {
+ // Client-provided fields
+ Seq int64 `json:"seq"`
+ Action string `json:"action"`
+ Data map[string]interface{} `json:"data"`
+
+ // Server-provided fields
+ Session Session `json:"-"`
+ T goi18n.TranslateFunc `json:"-"`
+ Locale string `json:"-"`
+}
+
+func (o *WebSocketRequest) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func WebSocketRequestFromJson(data io.Reader) *WebSocketRequest {
+ decoder := json.NewDecoder(data)
+ var o WebSocketRequest
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/websocket_request_test.go b/model/websocket_request_test.go
new file mode 100644
index 000000000..52de82069
--- /dev/null
+++ b/model/websocket_request_test.go
@@ -0,0 +1,25 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestWebSocketRequest(t *testing.T) {
+ m := WebSocketRequest{Seq: 1, Action: "test"}
+ json := m.ToJson()
+ result := WebSocketRequestFromJson(strings.NewReader(json))
+
+ if result == nil {
+ t.Fatal("should not be nil")
+ }
+
+ badresult := WebSocketRequestFromJson(strings.NewReader("junk"))
+
+ if badresult != nil {
+ t.Fatal("should have been nil")
+ }
+}