Added infrastructure for basic WebSocket API (#3432)

Этот коммит содержится в:
Joram Wilander
2016-07-12 09:36:27 -04:00
коммит произвёл GitHub
родитель 06eacf30b9
Коммит ad343a0f4a
32 изменённых файлов: 820 добавлений и 363 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

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

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

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

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

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