github.com/optim-corp/cios-golang-sdk@v0.5.1/sdk/service/pubsub/messaging.go (about) 1 package srvpubsub 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "log" 8 "net/http" 9 "sync" 10 "time" 11 12 "github.com/optim-corp/cios-golang-sdk/sdk/enum" 13 14 ciosutil "github.com/optim-corp/cios-golang-sdk/util/cios" 15 16 "github.com/fcfcqloow/go-advance/check" 17 cnv "github.com/fcfcqloow/go-advance/convert" 18 "github.com/optim-corp/cios-golang-sdk/cios" 19 ciosctx "github.com/optim-corp/cios-golang-sdk/ctx" 20 sdkmodel "github.com/optim-corp/cios-golang-sdk/model" 21 22 _nethttp "net/http" 23 "net/url" 24 "strings" 25 26 "github.com/gorilla/websocket" 27 ) 28 29 type ( 30 CiosMessaging struct { 31 SubscribeFunc func([]byte) (bool, error) 32 CloseFunc func() 33 Connection *websocket.Conn 34 isUpdating bool 35 wsUrl string 36 isDebug bool 37 token string 38 closed chan bool 39 readDeadTime time.Duration 40 writeDeadTime time.Duration 41 refresh *func() sdkmodel.AccessToken 42 } 43 44 // Deprecated: should not be used 45 ConnectWebSocketOptions struct { 46 PackerFormat *string 47 SubscribeFunc *func(body []byte) (bool, error) 48 PublishStr *chan *string 49 Setting *func(*websocket.Conn) 50 Context ciosctx.RequestCtx 51 } 52 ) 53 54 func CreateCiosWsConn(isDebug bool, url, authorization string) (connection *websocket.Conn, err error) { 55 if isDebug { 56 log.Printf("Websocket URL: %s\nAuthorization: %s", url, authorization) 57 } 58 connection, _, err = (&websocket.Dialer{}).Dial(url, http.Header{"Authorization": []string{authorization}}) 59 return 60 } 61 func CreateCiosWsMessagingURL(httpUrl, channelID, mode string, packerFormat *string) string { 62 _url, err := url.Parse(strings.Replace(httpUrl, "https", "wss", 1) + "/v2/messaging") 63 if err != nil { 64 return "" 65 } 66 q := _url.Query() 67 q.Set("channel_id", channelID) 68 q.Set("mode", mode) 69 if packerFormat != nil { 70 q.Set("packer_format", *packerFormat) 71 } 72 _url.RawQuery = q.Encode() 73 return _url.String() 74 } 75 func (self *CiosPubSub) NewMessaging(channelId string, mode enum.MessagingMode, packerFormat enum.PackerFormat) *CiosMessaging { 76 ref := func() (token string) { 77 self.refresh() 78 if !check.IsNil(self.token) { 79 token = *self.token 80 } 81 return 82 } 83 if packerFormat == "" { 84 packerFormat = "payload_only" 85 } 86 instance := CiosMessaging{} 87 instance.wsUrl = CreateCiosWsMessagingURL(self.Url, channelId, string(mode), cnv.StrPtr(string(packerFormat))) 88 instance.isDebug = self.debug 89 instance.refresh = &ref 90 instance.CloseFunc = func() {} 91 return &instance 92 } 93 94 func (self *CiosMessaging) SetWriteTimeout(t time.Duration) *CiosMessaging { 95 self.writeDeadTime = t 96 return self 97 } 98 func (self *CiosMessaging) SetReadTimeout(t time.Duration) *CiosMessaging { 99 self.readDeadTime = t 100 return self 101 } 102 func (self *CiosMessaging) debug(text ...interface{}) { 103 if self.isDebug { 104 result := "" 105 for _, t := range text { 106 result += cnv.MustStr(t) + " " 107 } 108 log.Println(result) 109 } 110 } 111 func (self *CiosMessaging) OnReceive(arg func([]byte) (bool, error)) error { 112 for { 113 body, err := self.Receive() 114 if err != nil { 115 return err 116 } 117 if ok, err := arg(body); !ok || err != nil { 118 return err 119 } 120 } 121 } 122 func (self *CiosMessaging) OnClose(arg func()) { 123 self.CloseFunc = arg 124 } 125 func (self *CiosMessaging) Send(message []byte) (err error) { 126 if check.IsNil(self.Connection) { 127 return fmt.Errorf("no connection used Start()") 128 } 129 defer self.debug("Send: " + string(message)) 130 if self.writeDeadTime != 0 { 131 if err = self.Connection.SetWriteDeadline(time.Now().Add(self.writeDeadTime)); err != nil { 132 self.debug("Set Write Timeout", err) 133 return 134 } 135 } 136 if err = self.Connection.WriteMessage(websocket.TextMessage, message); err != nil && !check.IsNil(self.refresh) { 137 self.token = (*self.refresh)() 138 self.debug("Send Refresh") 139 if _connection, err := CreateCiosWsConn(self.isDebug, self.wsUrl, ciosutil.ParseAccessToken(self.token)); err == nil { 140 self.debug("Close err: ", self.Connection.Close()) 141 self.Connection = _connection 142 return self.Send(message) 143 } 144 } 145 return 146 } 147 func (self *CiosMessaging) SendStr(message string) error { 148 return self.Send([]byte(message)) 149 } 150 func (self *CiosMessaging) SendAny(message interface{}) error { 151 return self.SendStr(cnv.MustStr(message)) 152 } 153 func (self *CiosMessaging) SendJson(message interface{}) error { 154 msg, err := json.Marshal(message) 155 if err != nil { 156 return err 157 } 158 return self.Send(msg) 159 } 160 func (self *CiosMessaging) Publish(message interface{}) error { 161 return self.SendAny(message) 162 } 163 164 func (self *CiosMessaging) receive() (body []byte, err error) { 165 var messageType int 166 if check.IsNil(self.Connection) { 167 err = fmt.Errorf("no connection use Start()") 168 return 169 } 170 if self.readDeadTime != 0 { 171 if err = self.Connection.SetReadDeadline(time.Now().Add(self.readDeadTime)); err != nil { 172 self.debug("Set Read Timeout", err) 173 return 174 } 175 } 176 messageType, body, err = self.Connection.ReadMessage() 177 switch { 178 case websocket.IsCloseError(err, websocket.CloseNormalClosure): 179 err = nil 180 self.debug("Receive close err: ", fmt.Sprintf("%d, %s", messageType, cnv.MustStr(err))) 181 case websocket.IsUnexpectedCloseError(err): 182 self.debug("Receive unexpected close err: ", fmt.Sprintf("%d, %s", messageType, cnv.MustStr(err))) 183 case messageType == websocket.CloseMessage: 184 err = nil 185 self.debug("Receive CloseMessage: ", fmt.Sprintf("%d, %s", messageType, cnv.MustStr(err))) 186 case messageType == websocket.TextMessage: 187 self.debug(string(body)) 188 } 189 return 190 } 191 func (self *CiosMessaging) Receive() (body []byte, err error) { 192 body, err = self.receive() 193 if err != nil && !check.IsNil(self.refresh) { 194 self.token = (*self.refresh)() 195 self.debug("Receive Refresh") 196 if _connection, err := CreateCiosWsConn(self.isDebug, self.wsUrl, ciosutil.ParseAccessToken(self.token)); err == nil { 197 self.debug("Close err: ", self.Connection.Close()) 198 self.Connection = _connection 199 return self.Receive() 200 } 201 } 202 return 203 } 204 func (self *CiosMessaging) ReceiveStr() (string, error) { 205 res, err := self.Receive() 206 return string(res), err 207 } 208 func (self *CiosMessaging) MapReceived(stct interface{}) error { 209 res, err := self.Receive() 210 if err != nil { 211 return err 212 } 213 return cnv.UnMarshalJson(res, stct) 214 } 215 216 func (self *CiosMessaging) Start(ctx ciosctx.RequestCtx) (err error) { 217 self.closed = make(chan bool) 218 if _token, ok := ctx.Value(cios.ContextAccessToken).(string); !ok && !check.IsNil(self.refresh) { 219 self.token = (*self.refresh)() 220 } else { 221 self.token = _token 222 } 223 if self.Connection, err = CreateCiosWsConn(self.isDebug, self.wsUrl, ciosutil.ParseAccessToken(self.token)); err != nil { 224 return 225 } 226 autoRefresh := func() { 227 Refresh: 228 for { 229 self.debug("Registration Refresh Loop") 230 select { 231 case <-time.After(time.Minute * 55): 232 if check.IsNil(self.refresh) { 233 break 234 } 235 self.token = (*self.refresh)() 236 self.debug("Auto Refresh") 237 if _connection, _err := CreateCiosWsConn(self.isDebug, self.wsUrl, ciosutil.ParseAccessToken(self.token)); _err == nil { 238 self.debug("Connection Close: ", self.Connection.Close()) 239 self.Connection = _connection 240 self.debug("Reconnect Websocket") 241 } 242 case _, _ = <-self.closed: 243 self.debug("End Auto Refresh") 244 break Refresh 245 } 246 } 247 } 248 autoPing := func() { 249 Ping: 250 for { 251 self.debug("Registration Ping Loop") 252 select { 253 case <-time.After(time.Minute): 254 if !check.IsNil(self.Connection) { 255 self.debug("Ping") 256 if err := self.Connection.WriteMessage(websocket.PingMessage, nil); err != nil { 257 self.debug("Ping Err: ", err) 258 continue 259 } 260 } 261 case _, _ = <-self.closed: 262 self.debug("End Auto Ping") 263 break Ping 264 } 265 } 266 } 267 go autoRefresh() 268 go autoPing() 269 return 270 } 271 func (self *CiosMessaging) Close() (err error) { 272 self.debug("Close") 273 defer self.CloseFunc() 274 self.closed <- true 275 self.closed <- true 276 safeCloseChan(self.closed) 277 if !check.IsNil(self.Connection) { 278 return self.Connection.Close() 279 } 280 return nil 281 } 282 283 func (self *CiosPubSub) PublishMessage(ctx ciosctx.RequestCtx, id string, body interface{}, packerFormat *string) (*_nethttp.Response, error) { 284 if err := self.refresh(); err != nil { 285 return nil, err 286 } 287 request := self.ApiClient.PublishSubscribeApi.PublishMessage(self.withHost(ctx)).ChannelId(id).Body(body) 288 request.P_packerFormat = packerFormat 289 return request.Execute() 290 } 291 func (self *CiosPubSub) PublishMessagePackerOnly(ctx ciosctx.RequestCtx, id string, body interface{}) (*_nethttp.Response, error) { 292 return self.PublishMessage(ctx, id, &body, nil) 293 } 294 func (self *CiosPubSub) PublishMessageJSON(ctx ciosctx.RequestCtx, id string, body cios.PackerFormatJson) (*_nethttp.Response, error) { 295 return self.PublishMessage(ctx, id, &body, cnv.StrPtr("json")) 296 } 297 298 // Deprecated: should not be used 299 func (self *CiosPubSub) ConnectWebSocket(channelID string, done chan bool, params ConnectWebSocketOptions) (err error) { 300 if params.SubscribeFunc == nil && params.PublishStr == nil { 301 return errors.New("no publish str and subscribe func") 302 } 303 mode, _token, ok := "subscribe", "", true 304 if params.SubscribeFunc == nil && params.PublishStr != nil { 305 mode = "publish" 306 } else if params.SubscribeFunc != nil && params.PublishStr != nil { 307 mode = "pubsub" 308 } 309 if params.Context != nil { 310 _token, ok = params.Context.Value(cios.ContextAccessToken).(string) 311 } 312 if _token == "" { 313 _ = self.refresh() 314 _token = cnv.MustStr(self.token) 315 } 316 317 var ( 318 wsUrl = self.CreateMessagingURL(channelID, mode, params.PackerFormat) 319 connection, connectionErr = self.CreateCIOSWebsocketConnection(wsUrl, ciosutil.ParseAccessToken(_token)) 320 wg sync.WaitGroup 321 finish = false 322 beUpdating = false 323 beCompletedS = make(chan bool, 1) 324 beCompletedP = make(chan bool, 1) 325 debug = func(text ...interface{}) { 326 if self.debug { 327 log.Println(text...) 328 } 329 } 330 closeConnection = func() { 331 if connection != nil { 332 if _err := connection.Close(); _err != nil { 333 debug("\nClosed Connection\n") 334 } 335 } 336 } 337 cleanBeCompleted = func() { 338 for { 339 select { 340 case <-beCompletedP: 341 debug("ignore publish update") 342 continue 343 case <-beCompletedS: 344 debug("ignore subscribe update") 345 continue 346 default: 347 } 348 break 349 } 350 } 351 reconnection = func() error { 352 _ = self.refresh() 353 _connection, _err := self.CreateCIOSWebsocketConnection(wsUrl, ciosutil.ParseAccessToken(cnv.MustStr(self.token))) 354 closeConnection() 355 if _err != nil { 356 return _err 357 } 358 connection = _connection 359 return nil 360 } 361 closeLogic = func() { 362 if params.PublishStr != nil { 363 isClosed := func() (f bool) { 364 select { 365 case _, ok = <-*params.PublishStr: 366 f = !ok 367 default: 368 } 369 return 370 } 371 if !isClosed() { 372 close(*params.PublishStr) 373 } 374 } 375 beUpdating = false 376 finish = true 377 closeConnection() 378 safeSendChan(done, false) 379 safeCloseChan(done) 380 safeCloseChan(beCompletedS) 381 safeCloseChan(beCompletedP) 382 } 383 updateConnection = func() { 384 for { 385 select { 386 case <-time.After(time.Second * 270): 387 if !check.IsNil(self.refresh) { 388 cleanBeCompleted() 389 beUpdating = true 390 debug("Updating access token and connection") 391 if err = reconnection(); err != nil { 392 closeLogic() 393 beUpdating = false 394 break 395 } 396 beCompletedS <- true 397 beCompletedP <- true 398 debug("Completed reconnection") 399 beUpdating = false 400 } 401 break 402 case isClosing := <-done: 403 if isClosing { 404 closeLogic() 405 } 406 break 407 } 408 if finish { 409 break 410 } 411 } 412 } 413 subscribe = func() { 414 for { 415 if finish { 416 break 417 } 418 if _, body, _err := connection.ReadMessage(); _err != nil { 419 if beUpdating { 420 if isConnecting, ok := <-beCompletedS; ok && isConnecting { 421 continue 422 } 423 } 424 err = errors.New("close connection") 425 break 426 } else if finish, err = (*params.SubscribeFunc)(body); err != nil || finish { 427 break 428 } else { 429 debug("Receive text: " + string(body)) 430 } 431 } 432 closeLogic() 433 } 434 publish = func() { 435 for { 436 text, ok := <-(*params.PublishStr) 437 if text == nil || !ok { 438 break 439 } 440 if beUpdating { 441 <-beCompletedP 442 } 443 if _err := connection.WriteMessage(websocket.TextMessage, []byte(*text)); _err != nil { 444 if _err = connection.WriteMessage(websocket.TextMessage, []byte(*text)); _err != nil { 445 err = errors.New("close connection") 446 break 447 } 448 } 449 debug("Publish text: " + *text) 450 } 451 closeLogic() 452 } 453 ) 454 if err = connectionErr; err != nil { 455 safeCloseChan(beCompletedS) 456 safeCloseChan(beCompletedP) 457 return err 458 } 459 if params.Setting != nil { 460 (*params.Setting)(connection) 461 } 462 wg.Add(1) 463 go func() { 464 updateConnection() 465 wg.Done() 466 }() 467 if params.SubscribeFunc != nil { 468 wg.Add(1) 469 go func() { 470 subscribe() 471 wg.Done() 472 }() 473 } 474 if params.PublishStr != nil { 475 wg.Add(1) 476 go func() { 477 publish() 478 wg.Done() 479 }() 480 } 481 wg.Wait() 482 debug(err) 483 return err 484 } 485 486 // Deprecated: should not be used 487 func (self *CiosPubSub) CreateMessagingURL(channelID string, mode string, packerFormat *string) string { 488 _url, err := url.Parse(strings.Replace(self.Url, "https", "wss", 1) + "/v2/messaging") 489 if err != nil { 490 return "" 491 } 492 q := _url.Query() 493 q.Set("channel_id", channelID) 494 q.Set("mode", mode) 495 if packerFormat != nil { 496 q.Set("packer_format", *packerFormat) 497 } 498 _url.RawQuery = q.Encode() 499 return _url.String() 500 } 501 502 // Deprecated: should not be used 503 func (self *CiosPubSub) CreateCIOSWebsocketConnection(url string, authorization string) (connection *websocket.Conn, err error) { 504 if self.debug { 505 log.Printf("Websocket URL: %s\nAuthorization: %s", url, authorization) 506 } 507 connection, _, err = (&websocket.Dialer{}).Dial(url, http.Header{"Authorization": []string{authorization}}) 508 return 509 } 510 511 func safeCloseChan(ch chan bool) { 512 isClosed := func() bool { 513 select { 514 case _, ok := <-ch: 515 return !ok 516 default: 517 return false 518 } 519 } 520 if !isClosed() { 521 close(ch) 522 } 523 } 524 func safeSendChan(ch chan bool, v bool) { 525 isClosed := func() bool { 526 select { 527 case _, ok := <-ch: 528 return !ok 529 default: 530 return false 531 } 532 } 533 if !isClosed() { 534 ch <- v 535 } 536 }