Added infrastructure for basic WebSocket API (#3432)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
06eacf30b9
Коммит
ad343a0f4a
@@ -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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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")
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user