github.com/retailcrm/mg-bot-helper@v0.0.0-20201229112329-a17255681a84/src/worker.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "strconv" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/getsentry/raven-go" 13 "github.com/gorilla/websocket" 14 "github.com/nicksnyder/go-i18n/v2/i18n" 15 "github.com/op/go-logging" 16 "github.com/retailcrm/api-client-go/errs" 17 v5 "github.com/retailcrm/api-client-go/v5" 18 v1 "github.com/retailcrm/mg-bot-api-client-go/v1" 19 "golang.org/x/text/language" 20 ) 21 22 const ( 23 CommandPayment = "/payment" 24 CommandDelivery = "/delivery" 25 CommandProduct = "/product" 26 ) 27 28 var ( 29 events = []string{v1.WsEventMessageNew} 30 msgLen = 2000 31 emoji = []string{"0️⃣ ", "1️⃣ ", "2️⃣ ", "3️⃣ ", "4️⃣ ", "5️⃣ ", "6️⃣ ", "7️⃣ ", "8️⃣ ", "9️⃣ "} 32 botCommands = []string{CommandPayment, CommandDelivery, CommandProduct} 33 botCredentials = []string{ 34 "/api/integration-modules/{code}", 35 "/api/integration-modules/{code}/edit", 36 "/api/reference/payment-types", 37 "/api/reference/delivery-types", 38 "/api/store/products", 39 } 40 ) 41 42 type Worker struct { 43 connection *Connection 44 mutex sync.RWMutex 45 localizer *i18n.Localizer 46 47 sentry *raven.Client 48 logger *logging.Logger 49 50 mgClient *v1.MgClient 51 crmClient *v5.Client 52 53 close bool 54 } 55 56 func NewWorker(conn *Connection, sentry *raven.Client, logger *logging.Logger) *Worker { 57 crmClient := v5.New(conn.APIURL, conn.APIKEY) 58 mgClient := v1.New(conn.MGURL, conn.MGToken) 59 if config.Debug { 60 crmClient.Debug = true 61 mgClient.Debug = true 62 } 63 64 return &Worker{ 65 connection: conn, 66 sentry: sentry, 67 logger: logger, 68 localizer: getLang(conn.Lang), 69 mgClient: mgClient, 70 crmClient: crmClient, 71 close: false, 72 } 73 } 74 75 func (w *Worker) UpdateWorker(conn *Connection) { 76 w.mutex.RLock() 77 defer w.mutex.RUnlock() 78 79 w.localizer = getLang(conn.Lang) 80 w.connection = conn 81 } 82 83 func (w *Worker) sendSentry(err error) { 84 tags := map[string]string{ 85 "crm": w.connection.APIURL, 86 "active": strconv.FormatBool(w.connection.Active), 87 "lang": w.connection.Lang, 88 "currency": w.connection.Currency, 89 "updated_at": w.connection.UpdatedAt.String(), 90 } 91 92 w.logger.Errorf("ws url: %s\nmgClient: %v\nerr: %v", w.crmClient.URL, w.mgClient, err) 93 go w.sentry.CaptureError(err, tags) 94 } 95 96 type WorkersManager struct { 97 mutex sync.RWMutex 98 workers map[string]*Worker 99 } 100 101 func NewWorkersManager() *WorkersManager { 102 return &WorkersManager{ 103 workers: map[string]*Worker{}, 104 } 105 } 106 107 func (wm *WorkersManager) setWorker(conn *Connection) { 108 wm.mutex.Lock() 109 defer wm.mutex.Unlock() 110 111 if conn.Active { 112 worker, ok := wm.workers[conn.ClientID] 113 if ok { 114 worker.UpdateWorker(conn) 115 } else { 116 wm.workers[conn.ClientID] = NewWorker(conn, sentry, logger) 117 go wm.workers[conn.ClientID].UpWS() 118 } 119 } 120 } 121 122 func (wm *WorkersManager) stopWorker(conn *Connection) { 123 wm.mutex.Lock() 124 defer wm.mutex.Unlock() 125 126 worker, ok := wm.workers[conn.ClientID] 127 if ok { 128 worker.close = true 129 delete(wm.workers, conn.ClientID) 130 } 131 } 132 133 func (w *Worker) UpWS() { 134 data, header, err := w.mgClient.WsMeta(events) 135 if err != nil { 136 w.sendSentry(err) 137 return 138 } 139 140 ROOT: 141 for { 142 if w.close { 143 if config.Debug { 144 w.logger.Debug("stop ws:", w.connection.APIURL) 145 } 146 return 147 } 148 ws, _, err := websocket.DefaultDialer.Dial(data, header) 149 if err != nil { 150 w.sendSentry(err) 151 time.Sleep(1000 * time.Millisecond) 152 continue ROOT 153 } 154 155 if config.Debug { 156 w.logger.Info("start ws: ", w.crmClient.URL) 157 } 158 159 for { 160 var wsEvent v1.WsEvent 161 err = ws.ReadJSON(&wsEvent) 162 if err != nil { 163 w.sendSentry(err) 164 if websocket.IsUnexpectedCloseError(err) { 165 continue ROOT 166 } 167 continue 168 } 169 170 if w.close { 171 if config.Debug { 172 w.logger.Debug("stop ws:", w.connection.APIURL) 173 } 174 return 175 } 176 177 var eventData v1.WsEventMessageNewData 178 err = json.Unmarshal(wsEvent.Data, &eventData) 179 if err != nil { 180 w.sendSentry(err) 181 continue 182 } 183 184 if eventData.Message.Type != "command" { 185 continue 186 } 187 188 msg, msgProd, err := w.execCommand(eventData.Message.Content) 189 if err != nil { 190 w.sendSentry(err) 191 msg = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_key"}) 192 } 193 194 msgSend := v1.MessageSendRequest{ 195 Scope: v1.MessageScopePrivate, 196 ChatID: eventData.Message.ChatID, 197 } 198 199 if msg != "" { 200 msgSend.Type = v1.MsgTypeText 201 msgSend.Content = msg 202 } else if msgProd.ID != 0 { 203 msgSend.Type = v1.MsgTypeProduct 204 msgSend.Product = &msgProd 205 } 206 207 if msgSend.Type != "" { 208 d, status, err := w.mgClient.MessageSend(msgSend) 209 if err != nil { 210 w.logger.Warningf("MessageSend status: %d\nMessageSend err: %v\nMessageSend data: %v", status, err, d) 211 continue 212 } 213 } 214 } 215 time.Sleep(500 * time.Millisecond) 216 } 217 } 218 219 func checkErrors(err errs.Failure) error { 220 if err.RuntimeErr != nil { 221 return err.RuntimeErr 222 } 223 224 if err.ApiErr != "" { 225 return errors.New(err.ApiErr) 226 } 227 228 return nil 229 } 230 231 func parseCommand(ci string) (co string, params v5.ProductsRequest, err error) { 232 s := strings.Split(ci, " ") 233 234 for _, cmd := range botCommands { 235 if s[0] == cmd { 236 if len(s) > 1 && cmd == CommandProduct { 237 params.Filter = v5.ProductsFilter{ 238 Name: ci[len(CommandProduct)+1:], 239 } 240 } 241 co = s[0] 242 break 243 } 244 } 245 246 return 247 } 248 249 func (w *Worker) execCommand(message string) (resMes string, msgProd v1.MessageProduct, err error) { 250 var s []string 251 252 command, params, err := parseCommand(message) 253 if err != nil { 254 return 255 } 256 257 switch command { 258 case CommandPayment: 259 res, _, er := w.crmClient.PaymentTypes() 260 err = checkErrors(er) 261 if err != nil { 262 logger.Errorf("%s - Cannot retrieve payment types, error: %s", w.crmClient.URL, err.Error()) 263 return 264 } 265 for _, v := range res.PaymentTypes { 266 if v.Active { 267 s = append(s, v.Name) 268 } 269 } 270 if len(s) > 0 { 271 resMes = fmt.Sprintf("%s\n\n", w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "payment_options"})) 272 } 273 case CommandDelivery: 274 res, _, er := w.crmClient.DeliveryTypes() 275 err = checkErrors(er) 276 if err != nil { 277 logger.Errorf("%s - Cannot retrieve delivery types, error: %s", w.crmClient.URL, err.Error()) 278 return 279 } 280 for _, v := range res.DeliveryTypes { 281 if v.Active { 282 s = append(s, v.Name) 283 } 284 } 285 if len(s) > 0 { 286 resMes = fmt.Sprintf("%s\n\n", w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "delivery_options"})) 287 } 288 case CommandProduct: 289 if params.Filter.Name == "" { 290 resMes = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "set_name_or_article"}) 291 return 292 } 293 294 res, _, er := w.crmClient.Products(params) 295 err = checkErrors(er) 296 if err != nil { 297 logger.Errorf("%s - Cannot retrieve product, error: %s", w.crmClient.URL, err.Error()) 298 return 299 } 300 301 if len(res.Products) > 0 { 302 for _, vp := range res.Products { 303 if vp.Active { 304 vo := searchOffer(vp.Offers, params.Filter.Name) 305 msgProd = v1.MessageProduct{ 306 ID: uint64(vo.ID), 307 Name: vo.Name, 308 Article: vo.Article, 309 Url: vp.URL, 310 Img: vp.ImageURL, 311 Cost: &v1.MessageOrderCost{ 312 Value: vo.Price, 313 Currency: w.connection.Currency, 314 }, 315 } 316 317 if vp.Quantity > 0 { 318 msgProd.Quantity = &v1.MessageOrderQuantity{ 319 Value: vp.Quantity, 320 Unit: vo.Unit.Sym, 321 } 322 } 323 324 if len(vo.Images) > 0 { 325 msgProd.Img = vo.Images[0] 326 } 327 328 return 329 } 330 } 331 332 } 333 default: 334 return 335 } 336 337 if len(s) == 0 { 338 resMes = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found"}) 339 return 340 } 341 342 if len(s) > 1 { 343 for k, v := range s { 344 var a string 345 for _, iv := range strings.Split(strconv.Itoa(k+1), "") { 346 t, _ := strconv.Atoi(iv) 347 a += emoji[t] 348 } 349 s[k] = fmt.Sprintf("%v %v", a, v) 350 } 351 } 352 353 str := strings.Join(s, "\n") 354 resMes += str 355 356 if len(resMes) > msgLen { 357 resMes = resMes[:msgLen] 358 } 359 360 return 361 } 362 363 func searchOffer(offers []v5.Offer, filter string) (offer v5.Offer) { 364 for _, o := range offers { 365 if o.Article == filter { 366 offer = o 367 } 368 } 369 370 if offer.ID == 0 { 371 for _, o := range offers { 372 if o.Name == filter { 373 offer = o 374 } 375 } 376 } 377 378 if offer.ID == 0 { 379 offer = offers[0] 380 } 381 382 return 383 } 384 385 func SetBotCommand(botURL, botToken string) (code int, err error) { 386 var client = v1.New(botURL, botToken) 387 388 _, code, err = client.CommandEdit(v1.CommandEditRequest{ 389 Name: getTextCommand(CommandPayment), 390 Description: getLocalizedMessage("get_payment"), 391 }) 392 393 _, code, err = client.CommandEdit(v1.CommandEditRequest{ 394 Name: getTextCommand(CommandDelivery), 395 Description: getLocalizedMessage("get_delivery"), 396 }) 397 398 _, code, err = client.CommandEdit(v1.CommandEditRequest{ 399 Name: getTextCommand(CommandProduct), 400 Description: getLocalizedMessage("get_product"), 401 }) 402 403 return 404 } 405 406 func getTextCommand(command string) string { 407 return strings.Replace(command, "/", "", -1) 408 } 409 410 func getLang(lang string) *i18n.Localizer { 411 tag, _ := language.MatchStrings(matcher, lang) 412 413 return i18n.NewLocalizer(bundle, tag.String()) 414 }