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 }