commit 646caf2cab61546fcefbee4e210df8f58d58d223 Author: Neur0toxine Date: Thu Oct 14 18:25:30 2021 +0300 initial diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..15b672c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{less,css,yml,json}] +indent_size = 2 + +[{*.md,go.mod,go.sum}] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..d714cb3 --- /dev/null +++ b/.env.dist @@ -0,0 +1,4 @@ +API_URL=https://test.retailcrm.pro +API_KEY=key +MESSAGE_SCOPE=public +TEXT_OPTIONS="Text 1,Text 2,Text 3,Text 4,Text 5" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..347065b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor +.env +main +.idea diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3515ae3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Neur0toxine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd7144f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +This bot will automatically respond to any Simla message with preconfigured quick replies. +Build: `go build -o main ./...` +Usage: copy `.env.dist` to `.env` then replace placeholders with your data. After that you can run the bot. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8931421 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module bot-quick-replies + +go 1.17 + +require ( + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/joho/godotenv v1.4.0 // indirect + github.com/retailcrm/api-client-go v1.3.8 // indirect + github.com/retailcrm/mg-bot-api-client-go v1.2.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aca1ea5 --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/retailcrm/api-client-go v1.3.8 h1:oWyxmm2YB2bEz/F+0XrP0k6QhOVv9gICKv4A/3sDWS0= +github.com/retailcrm/api-client-go v1.3.8/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c= +github.com/retailcrm/mg-bot-api-client-go v1.2.9 h1:eUDrD20ysNCb4oAOmbJ4BTkXcHKZKU4Fq+aVKOx7Ttc= +github.com/retailcrm/mg-bot-api-client-go v1.2.9/go.mod h1:kWDUiT5pvUtWZrb/mpkJDgddU5ezW8EuwduJKJ35lPs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/h2non/gock.v1 v1.1.0/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1fc7a81 --- /dev/null +++ b/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "log" + "os" + "strings" + + "github.com/joho/godotenv" + v1 "github.com/retailcrm/mg-bot-api-client-go/v1" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file:", err) + } + + apiURL := os.Getenv("API_URL") + apiKey := os.Getenv("API_KEY") + + if apiURL == "" { + log.Fatal("API_URL environment variable must be set") + } + if apiKey == "" { + log.Fatal("API_KEY environment variable must be set") + } + + botCode := os.Getenv("BOT_CODE") + botName := os.Getenv("BOT_NAME") + msgScope := os.Getenv("MESSAGE_SCOPE") + options := os.Getenv("TEXT_OPTIONS") + + if botCode == "" { + botCode = "test-quick-reply-bot" + } + if botName == "" { + botName = "Test Quick Reply Bot" + } + if msgScope == "" { + msgScope = v1.MessageScopePrivate + } + if options == "" { + options = "Text reply" + } + + endpoint, token, err := updateIntegrationModule(apiURL, apiKey, botCode, botName) + if err != nil { + log.Fatal("Error updating integration module: ", err) + } + + log.Println("Integration module has been updated. Endpoint: ", endpoint, ", token: ", token) + + if err := NewWebsocketListener(endpoint, token, msgScope, strings.Split(options, ",")).Listen(); err != nil { + log.Fatal(err) + } +} diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..30f5495 --- /dev/null +++ b/settings.go @@ -0,0 +1,62 @@ +package main + +import ( + "errors" + "fmt" + "strings" + + "github.com/retailcrm/api-client-go/errs" + "github.com/retailcrm/api-client-go/v5" +) + +func buildIntegrationModule(code, name string) v5.IntegrationModule { + return v5.IntegrationModule{ + Code: code, + IntegrationCode: code, + Active: true, + Name: name, + ClientID: code, + BaseURL: "https://example.com", + Integrations: &v5.Integrations{ + MgBot: &v5.MgBot{}, + }, + } +} + +func updateIntegrationModule(apiURL, apiKey, code, name string) (string, string, error) { + client := v5.New(apiURL, apiKey) + resp, _, err := client.IntegrationModuleEdit(buildIntegrationModule(code, name)) + if err != nil { + if nErr := normalizeAPIError(err); nErr != nil { + return "", "", nErr + } + } + return resp.Info.MgBotInfo.EndpointUrl, resp.Info.MgBotInfo.Token, nil +} + +func normalizeAPIError(err *errs.Failure) error { + if err == nil { + return nil + } + + if err.Error() != "" { + return errors.New(err.Error()) + } + + if err.ApiError() != "" { + return errors.New(err.ApiError()) + } + + if len(err.ApiErrors()) > 0 { + var sb strings.Builder + sb.Grow(128) + + for field, value := range err.ApiErrors() { + sb.WriteString(fmt.Sprintf("[%s: %s]", field, value)) + } + + return errors.New(sb.String()) + } + + return nil +} diff --git a/websocket.go b/websocket.go new file mode 100644 index 0000000..cdf3838 --- /dev/null +++ b/websocket.go @@ -0,0 +1,97 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/gorilla/websocket" + v1 "github.com/retailcrm/mg-bot-api-client-go/v1" +) + +type WebsocketListener struct { + mg *v1.MgClient + ws *websocket.Conn + scope string + suggestions []v1.Suggestion +} + +func NewWebsocketListener(endpoint, token, scope string, textOptions []string) *WebsocketListener { + suggestions := []v1.Suggestion{ + { + Type: v1.SuggestionTypePhone, + Title: "Phone", + }, + { + Type: v1.SuggestionTypeEmail, + Title: "E-Mail", + }, + } + + for _, s := range textOptions { + suggestions = append(suggestions, v1.Suggestion{ + Type: v1.SuggestionTypeText, + Title: s, + }) + } + + return &WebsocketListener{ + mg: v1.New(endpoint, token), + scope: scope, + suggestions: suggestions, + } +} + +func (l *WebsocketListener) Listen() error { + data, header, err := l.mg.WsMeta([]string{v1.WsEventMessageNew}) + if err != nil { + return fmt.Errorf("cannot get meta for connection: %w", err) + } + + ws, _, err := websocket.DefaultDialer.Dial(data, header) + if err != nil { + return fmt.Errorf("cannot estabilish WebSocket connection to %s: %w", data, err) + } + + log.Println("Listening for the new messages...") + + for { + var wsEvent v1.WsEvent + if err := ws.ReadJSON(&wsEvent); err != nil { + log.Fatal("unexpected websocket error:", err) + } + + var event v1.WsEventMessageNewData + err = json.Unmarshal(wsEvent.Data, &event) + if err != nil { + log.Printf("cannot unmarshal payload: %s\n", err) + continue + } + + if event.Message == nil { + log.Print("invalid payload - nil message") + continue + } + + if event.Message.From != nil && event.Message.From.Type != "customer" { + continue + } + + log.Printf("Received message from %s with id=%d\n", event.Message.From.Name, event.Message.ID) + + _, _, err := l.mg.MessageSend(v1.MessageSendRequest{ + Type: v1.MsgTypeText, + Content: "The quick brown fox jumps over the lazy dog.", + // Items: nil, + Scope: l.scope, + ChatID: event.Message.ChatID, + QuoteMessageId: event.Message.ID, + TransportAttachments: &v1.TransportAttachments{ + Suggestions: l.suggestions, + }, + }) + if err != nil { + log.Printf("error: cannot respond to the message: %s\n", err) + } + } +}