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  }