github.com/gigforks/mattermost-server@v4.9.1-0.20180619094218-800d97fa55d0+incompatible/model/websocket_client.go (about)

     1  // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package model
     5  
     6  import (
     7  	"encoding/json"
     8  	"net/http"
     9  	"time"
    10  
    11  	"github.com/gorilla/websocket"
    12  )
    13  
    14  const (
    15  	SOCKET_MAX_MESSAGE_SIZE_KB  = 8 * 1024 // 8KB
    16  	PING_TIMEOUT_BUFFER_SECONDS = 5
    17  )
    18  
    19  type WebSocketClient struct {
    20  	Url                string          // The location of the server like "ws://localhost:8065"
    21  	ApiUrl             string          // The api location of the server like "ws://localhost:8065/api/v3"
    22  	ConnectUrl         string          // The websocket URL to connect to like "ws://localhost:8065/api/v3/path/to/websocket"
    23  	Conn               *websocket.Conn // The WebSocket connection
    24  	AuthToken          string          // The token used to open the WebSocket
    25  	Sequence           int64           // The ever-incrementing sequence attached to each WebSocket action
    26  	PingTimeoutChannel chan bool       // The channel used to signal ping timeouts
    27  	EventChannel       chan *WebSocketEvent
    28  	ResponseChannel    chan *WebSocketResponse
    29  	ListenError        *AppError
    30  	pingTimeoutTimer   *time.Timer
    31  }
    32  
    33  // NewWebSocketClient constructs a new WebSocket client with convenience
    34  // methods for talking to the server.
    35  func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
    36  	return NewWebSocketClientWithDialer(websocket.DefaultDialer, url, authToken)
    37  }
    38  
    39  // NewWebSocketClientWithDialer constructs a new WebSocket client with convenience
    40  // methods for talking to the server using a custom dialer.
    41  func NewWebSocketClientWithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) {
    42  	conn, _, err := dialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil)
    43  	if err != nil {
    44  		return nil, NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
    45  	}
    46  
    47  	client := &WebSocketClient{
    48  		url,
    49  		url + API_URL_SUFFIX_V3,
    50  		url + API_URL_SUFFIX_V3 + "/users/websocket",
    51  		conn,
    52  		authToken,
    53  		1,
    54  		make(chan bool, 1),
    55  		make(chan *WebSocketEvent, 100),
    56  		make(chan *WebSocketResponse, 100),
    57  		nil,
    58  		nil,
    59  	}
    60  
    61  	client.configurePingHandling()
    62  
    63  	client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken})
    64  
    65  	return client, nil
    66  }
    67  
    68  // NewWebSocketClient4 constructs a new WebSocket client with convenience
    69  // methods for talking to the server. Uses the v4 endpoint.
    70  func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) {
    71  	return NewWebSocketClient4WithDialer(websocket.DefaultDialer, url, authToken)
    72  }
    73  
    74  // NewWebSocketClient4WithDialer constructs a new WebSocket client with convenience
    75  // methods for talking to the server using a custom dialer. Uses the v4 endpoint.
    76  func NewWebSocketClient4WithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) {
    77  	conn, _, err := dialer.Dial(url+API_URL_SUFFIX+"/websocket", nil)
    78  	if err != nil {
    79  		return nil, NewAppError("NewWebSocketClient4", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
    80  	}
    81  
    82  	client := &WebSocketClient{
    83  		url,
    84  		url + API_URL_SUFFIX,
    85  		url + API_URL_SUFFIX + "/websocket",
    86  		conn,
    87  		authToken,
    88  		1,
    89  		make(chan bool, 1),
    90  		make(chan *WebSocketEvent, 100),
    91  		make(chan *WebSocketResponse, 100),
    92  		nil,
    93  		nil,
    94  	}
    95  
    96  	client.configurePingHandling()
    97  
    98  	client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken})
    99  
   100  	return client, nil
   101  }
   102  
   103  func (wsc *WebSocketClient) Connect() *AppError {
   104  	return wsc.ConnectWithDialer(websocket.DefaultDialer)
   105  }
   106  
   107  func (wsc *WebSocketClient) ConnectWithDialer(dialer *websocket.Dialer) *AppError {
   108  	var err error
   109  	wsc.Conn, _, err = dialer.Dial(wsc.ConnectUrl, nil)
   110  	if err != nil {
   111  		return NewAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
   112  	}
   113  
   114  	wsc.configurePingHandling()
   115  
   116  	wsc.EventChannel = make(chan *WebSocketEvent, 100)
   117  	wsc.ResponseChannel = make(chan *WebSocketResponse, 100)
   118  
   119  	wsc.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": wsc.AuthToken})
   120  
   121  	return nil
   122  }
   123  
   124  func (wsc *WebSocketClient) Close() {
   125  	wsc.Conn.Close()
   126  }
   127  
   128  func (wsc *WebSocketClient) Listen() {
   129  	go func() {
   130  		defer func() {
   131  			wsc.Conn.Close()
   132  			close(wsc.EventChannel)
   133  			close(wsc.ResponseChannel)
   134  		}()
   135  
   136  		for {
   137  			var rawMsg json.RawMessage
   138  			var err error
   139  			if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil {
   140  				if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
   141  					wsc.ListenError = NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
   142  				}
   143  
   144  				return
   145  			}
   146  
   147  			var event WebSocketEvent
   148  			if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
   149  				wsc.EventChannel <- &event
   150  				continue
   151  			}
   152  
   153  			var response WebSocketResponse
   154  			if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
   155  				wsc.ResponseChannel <- &response
   156  				continue
   157  			}
   158  
   159  		}
   160  	}()
   161  }
   162  
   163  func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface{}) {
   164  	req := &WebSocketRequest{}
   165  	req.Seq = wsc.Sequence
   166  	req.Action = action
   167  	req.Data = data
   168  
   169  	wsc.Sequence++
   170  
   171  	wsc.Conn.WriteJSON(req)
   172  }
   173  
   174  // UserTyping will push a user_typing event out to all connected users
   175  // who are in the specified channel
   176  func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
   177  	data := map[string]interface{}{
   178  		"channel_id": channelId,
   179  		"parent_id":  parentId,
   180  	}
   181  
   182  	wsc.SendMessage("user_typing", data)
   183  }
   184  
   185  // GetStatuses will return a map of string statuses using user id as the key
   186  func (wsc *WebSocketClient) GetStatuses() {
   187  	wsc.SendMessage("get_statuses", nil)
   188  }
   189  
   190  // GetStatusesByIds will fetch certain user statuses based on ids and return
   191  // a map of string statuses using user id as the key
   192  func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) {
   193  	data := map[string]interface{}{
   194  		"user_ids": userIds,
   195  	}
   196  	wsc.SendMessage("get_statuses_by_ids", data)
   197  }
   198  
   199  func (wsc *WebSocketClient) configurePingHandling() {
   200  	wsc.Conn.SetPingHandler(wsc.pingHandler)
   201  	wsc.pingTimeoutTimer = time.NewTimer(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS))
   202  	go wsc.pingWatchdog()
   203  }
   204  
   205  func (wsc *WebSocketClient) pingHandler(appData string) error {
   206  	if !wsc.pingTimeoutTimer.Stop() {
   207  		<-wsc.pingTimeoutTimer.C
   208  	}
   209  
   210  	wsc.pingTimeoutTimer.Reset(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS))
   211  	wsc.Conn.WriteMessage(websocket.PongMessage, []byte{})
   212  	return nil
   213  }
   214  
   215  func (wsc *WebSocketClient) pingWatchdog() {
   216  	<-wsc.pingTimeoutTimer.C
   217  	wsc.PingTimeoutChannel <- true
   218  }