github.com/jlevesy/mattermost-server@v5.3.2-0.20181003190404-7468f35cb0c8+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+"/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,
    50  		url + API_URL_SUFFIX + "/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  	return NewWebSocketClientWithDialer(dialer, url, authToken)
    78  }
    79  
    80  func (wsc *WebSocketClient) Connect() *AppError {
    81  	return wsc.ConnectWithDialer(websocket.DefaultDialer)
    82  }
    83  
    84  func (wsc *WebSocketClient) ConnectWithDialer(dialer *websocket.Dialer) *AppError {
    85  	var err error
    86  	wsc.Conn, _, err = dialer.Dial(wsc.ConnectUrl, nil)
    87  	if err != nil {
    88  		return NewAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
    89  	}
    90  
    91  	wsc.configurePingHandling()
    92  
    93  	wsc.EventChannel = make(chan *WebSocketEvent, 100)
    94  	wsc.ResponseChannel = make(chan *WebSocketResponse, 100)
    95  
    96  	wsc.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": wsc.AuthToken})
    97  
    98  	return nil
    99  }
   100  
   101  func (wsc *WebSocketClient) Close() {
   102  	wsc.Conn.Close()
   103  }
   104  
   105  func (wsc *WebSocketClient) Listen() {
   106  	go func() {
   107  		defer func() {
   108  			wsc.Conn.Close()
   109  			close(wsc.EventChannel)
   110  			close(wsc.ResponseChannel)
   111  		}()
   112  
   113  		for {
   114  			var rawMsg json.RawMessage
   115  			var err error
   116  			if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil {
   117  				if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
   118  					wsc.ListenError = NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError)
   119  				}
   120  
   121  				return
   122  			}
   123  
   124  			var event WebSocketEvent
   125  			if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
   126  				wsc.EventChannel <- &event
   127  				continue
   128  			}
   129  
   130  			var response WebSocketResponse
   131  			if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
   132  				wsc.ResponseChannel <- &response
   133  				continue
   134  			}
   135  
   136  		}
   137  	}()
   138  }
   139  
   140  func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface{}) {
   141  	req := &WebSocketRequest{}
   142  	req.Seq = wsc.Sequence
   143  	req.Action = action
   144  	req.Data = data
   145  
   146  	wsc.Sequence++
   147  
   148  	wsc.Conn.WriteJSON(req)
   149  }
   150  
   151  // UserTyping will push a user_typing event out to all connected users
   152  // who are in the specified channel
   153  func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
   154  	data := map[string]interface{}{
   155  		"channel_id": channelId,
   156  		"parent_id":  parentId,
   157  	}
   158  
   159  	wsc.SendMessage("user_typing", data)
   160  }
   161  
   162  // GetStatuses will return a map of string statuses using user id as the key
   163  func (wsc *WebSocketClient) GetStatuses() {
   164  	wsc.SendMessage("get_statuses", nil)
   165  }
   166  
   167  // GetStatusesByIds will fetch certain user statuses based on ids and return
   168  // a map of string statuses using user id as the key
   169  func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) {
   170  	data := map[string]interface{}{
   171  		"user_ids": userIds,
   172  	}
   173  	wsc.SendMessage("get_statuses_by_ids", data)
   174  }
   175  
   176  func (wsc *WebSocketClient) configurePingHandling() {
   177  	wsc.Conn.SetPingHandler(wsc.pingHandler)
   178  	wsc.pingTimeoutTimer = time.NewTimer(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS))
   179  	go wsc.pingWatchdog()
   180  }
   181  
   182  func (wsc *WebSocketClient) pingHandler(appData string) error {
   183  	if !wsc.pingTimeoutTimer.Stop() {
   184  		<-wsc.pingTimeoutTimer.C
   185  	}
   186  
   187  	wsc.pingTimeoutTimer.Reset(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS))
   188  	wsc.Conn.WriteMessage(websocket.PongMessage, []byte{})
   189  	return nil
   190  }
   191  
   192  func (wsc *WebSocketClient) pingWatchdog() {
   193  	<-wsc.pingTimeoutTimer.C
   194  	wsc.PingTimeoutChannel <- true
   195  }