From 933d2051abb3a7596b127cd460052dffa8cf800f Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 27 Dec 2023 18:21:46 +0300 Subject: [PATCH 01/11] wip: update godoc --- v1/client.go | 89 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/v1/client.go b/v1/client.go index deeef90..06a3906 100644 --- a/v1/client.go +++ b/v1/client.go @@ -15,12 +15,12 @@ import ( "github.com/google/go-querystring/query" ) -// New initialize client. +// New initializes the MgClient. func New(url string, token string) *MgClient { return NewWithClient(url, token, &http.Client{Timeout: time.Minute}) } -// NewWithClient initializes client with provided http client. +// NewWithClient initializes the MgClient with specified *http.Client. func NewWithClient(url string, token string, client *http.Client) *MgClient { return &MgClient{ URL: url, @@ -29,13 +29,13 @@ func NewWithClient(url string, token string, client *http.Client) *MgClient { } } -// WithLogger sets the provided logger instance into the Client +// WithLogger sets the provided logger instance into the Client. func (c *MgClient) WithLogger(logger BasicLogger) *MgClient { c.logger = logger return c } -// writeLog writes to the log. +// writeLog writes a message to the log. func (c *MgClient) writeLog(format string, v ...interface{}) { if c.logger != nil { c.logger.Printf(format, v...) @@ -45,19 +45,18 @@ func (c *MgClient) writeLog(format string, v ...interface{}) { log.Printf(format, v...) } -// TransportTemplates returns templates list +// TransportTemplates returns templates list. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := v1.New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // // data, status, err := client.TransportTemplates() -// // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("Status: %v, Templates found: %v", status, len(data)) +// log.Printf("status: %d, response: %#v", status, data) func (c *MgClient) TransportTemplates() ([]Template, int, error) { var resp []Template @@ -77,37 +76,63 @@ func (c *MgClient) TransportTemplates() ([]Template, int, error) { return resp, status, err } +func ooga() { + client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") + + status, err := client.ActivateTemplate(1, ActivateTemplateRequest{ + UpdateTemplateRequest: UpdateTemplateRequest{ + Name: "New Template", + Body: "Hello, {{1}}! Welcome to our store!", + Lang: "en", + Category: "marketing", + Example: &TemplateExample{ + Header: []string{"https://example.com/image.png"}, + Body: []string{"John"}, + }, + VerificationStatus: TemplateStatusApproved, + Header: &TemplateHeader{ + Content: HeaderContentImage{}, + }, + }, + Code: "new_template", + Type: TemplateTypeMedia, + }) + if err != nil { + log.Fatalf("request error: %s (%d)", err, status) + } + + log.Printf("status: %d", status) +} + // ActivateTemplate implements template activation // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := v1.New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // -// request := v1.ActivateTemplateRequest{ -// Code: "code", -// Name: "name", -// Type: v1.TemplateTypeText, -// Template: []v1.TemplateItem{ -// { -// Type: v1.TemplateItemTypeText, -// Text: "Hello, ", -// }, -// { -// Type: v1.TemplateItemTypeVar, -// VarType: v1.TemplateVarName, -// }, -// { -// Type: v1.TemplateItemTypeText, -// Text: "!", -// }, +// status, err := client.ActivateTemplate(1, v1.ActivateTemplateRequest{ +// UpdateTemplateRequest: v1.UpdateTemplateRequest{ +// Name: "New Template", +// Body: "Hello, {{1}}! Welcome to our store!", +// Lang: "en", +// Category: "marketing", +// Example: &v1.TemplateExample{ +// Header: []string{"https://example.com/image.png"}, +// Body: []string{"John"}, // }, -// } -// -// _, err := client.ActivateTemplate(uint64(1), request) -// +// VerificationStatus: v1.TemplateStatusApproved, +// Header: &v1.TemplateHeader{ +// Content: v1.HeaderContentImage{}, +// }, +// }, +// Code: "new_template", +// Type: v1.TemplateTypeMedia, +// }) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } +// +// log.Printf("status: %d", status) func (c *MgClient) ActivateTemplate(channelID uint64, request ActivateTemplateRequest) (int, error) { outgoing, _ := json.Marshal(&request) From 5e605c9d19a771e484ee919fc4f839770565faed Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Thu, 28 Dec 2023 14:26:49 +0300 Subject: [PATCH 02/11] update godoc for methods and some types --- v1/client.go | 542 +++++++++++++++++++++++++++++--------------------- v1/errors.go | 7 + v1/helpers.go | 2 + v1/request.go | 8 +- v1/webhook.go | 20 ++ 5 files changed, 348 insertions(+), 231 deletions(-) diff --git a/v1/client.go b/v1/client.go index 06a3906..afa6c47 100644 --- a/v1/client.go +++ b/v1/client.go @@ -49,7 +49,7 @@ func (c *MgClient) writeLog(format string, v ...interface{}) { // // Example: // -// client := v1.New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // // data, status, err := client.TransportTemplates() // if err != nil { @@ -76,57 +76,29 @@ func (c *MgClient) TransportTemplates() ([]Template, int, error) { return resp, status, err } -func ooga() { - client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") - - status, err := client.ActivateTemplate(1, ActivateTemplateRequest{ - UpdateTemplateRequest: UpdateTemplateRequest{ - Name: "New Template", - Body: "Hello, {{1}}! Welcome to our store!", - Lang: "en", - Category: "marketing", - Example: &TemplateExample{ - Header: []string{"https://example.com/image.png"}, - Body: []string{"John"}, - }, - VerificationStatus: TemplateStatusApproved, - Header: &TemplateHeader{ - Content: HeaderContentImage{}, - }, - }, - Code: "new_template", - Type: TemplateTypeMedia, - }) - if err != nil { - log.Fatalf("request error: %s (%d)", err, status) - } - - log.Printf("status: %d", status) -} - -// ActivateTemplate implements template activation +// ActivateTemplate activates template with provided structure. // // Example: // -// client := v1.New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // -// status, err := client.ActivateTemplate(1, v1.ActivateTemplateRequest{ -// UpdateTemplateRequest: v1.UpdateTemplateRequest{ +// status, err := client.ActivateTemplate(1, ActivateTemplateRequest{ +// UpdateTemplateRequest: UpdateTemplateRequest{ // Name: "New Template", // Body: "Hello, {{1}}! Welcome to our store!", // Lang: "en", // Category: "marketing", -// Example: &v1.TemplateExample{ +// Example: &TemplateExample{ // Header: []string{"https://example.com/image.png"}, // Body: []string{"John"}, // }, -// VerificationStatus: v1.TemplateStatusApproved, -// Header: &v1.TemplateHeader{ -// Content: v1.HeaderContentImage{}, +// VerificationStatus: TemplateStatusApproved, +// Header: &TemplateHeader{ +// Content: HeaderContentImage{}, // }, // }, // Code: "new_template", -// Type: v1.TemplateTypeMedia, +// Type: TemplateTypeMedia, // }) // if err != nil { // log.Fatalf("request error: %s (%d)", err, status) @@ -148,36 +120,31 @@ func (c *MgClient) ActivateTemplate(channelID uint64, request ActivateTemplateRe return status, err } -// UpdateTemplate implements template updating +// UpdateTemplate updates existing template by its code. +// // Example: // -// var client = New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // -// request := v1.Template{ -// Code: "templateCode", -// ChannelID: 1, -// Name: "templateName", -// Template: []v1.TemplateItem{ -// { -// Type: v1.TemplateItemTypeText, -// Text: "Welcome, ", -// }, -// { -// Type: v1.TemplateItemTypeVar, -// VarType: v1.TemplateVarName, -// }, -// { -// Type: v1.TemplateItemTypeText, -// Text: "!", -// }, +// status, err := client.UpdateTemplate(1, "new_template", UpdateTemplateRequest{ +// Name: "New Template", +// Body: "Hello, {{1}}! Welcome to our store!", +// Lang: "en", +// Category: "marketing", +// Example: &TemplateExample{ +// Header: []string{"https://example.com/image.png"}, +// Body: []string{"John"}, // }, -// } -// -// _, err := client.UpdateTemplate(request) -// +// VerificationStatus: TemplateStatusApproved, +// Header: &TemplateHeader{ +// Content: HeaderContentImage{}, +// }, +// }) // if err != nil { -// fmt.Printf("%#v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } +// +// log.Printf("status: %d", status) func (c *MgClient) UpdateTemplate(channelID uint64, code string, request UpdateTemplateRequest) (int, error) { outgoing, _ := json.Marshal(&request) @@ -198,17 +165,18 @@ func (c *MgClient) UpdateTemplate(channelID uint64, code string, request UpdateT return status, err } -// DeactivateTemplate implements template deactivation +// DeactivateTemplate deactivates the template by its code. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// -// _, err := client.DeactivateTemplate(3053450384, "templateCode") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // +// status, err := client.DeactivateTemplate(1, "new_template") // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } +// +// log.Printf("status: %d", status) func (c *MgClient) DeactivateTemplate(channelID uint64, templateCode string) (int, error) { data, status, err := c.DeleteRequest( fmt.Sprintf("/channels/%d/templates/%s", channelID, url.PathEscape(templateCode)), []byte{}) @@ -223,19 +191,20 @@ func (c *MgClient) DeactivateTemplate(channelID uint64, templateCode string) (in return status, err } -// TransportChannels returns channels list +// TransportChannels returns channels for current transport. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// -// data, status, err := client.TransportChannels(Channels{Active: true}) +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // +// resp, status, err := client.TransportChannels(Channels{ +// Active: true, +// }) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("Status: %v, Channels found: %v", status, len(data)) +// log.Printf("status: %d, channels: %#v", status, resp) func (c *MgClient) TransportChannels(request Channels) ([]ChannelListItem, int, error) { var resp []ChannelListItem var b []byte @@ -257,45 +226,80 @@ func (c *MgClient) TransportChannels(request Channels) ([]ChannelListItem, int, return resp, status, err } -// ActivateTransportChannel implement channel activation +// ActivateTransportChannel activates the channel with provided settings. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// uint16Ptr := func(val uint16) *uint16 { +// return &val +// } +// mbToBytes := func(val uint64) *uint64 { +// val = val * 1024 * 1024 +// return &val +// } // -// request := ActivateRequest{ +// resp, status, err := client.ActivateTransportChannel(Channel{ // Type: "telegram", // Name: "@my_shopping_bot", // Settings: ChannelSettings{ // Status: Status{ // Delivered: ChannelFeatureNone, -// Read: ChannelFeatureReceive, +// Read: ChannelFeatureReceive, // }, // Text: ChannelSettingsText{ -// Creating: ChannelFeatureBoth, -// Editing: ChannelFeatureBoth, -// Quoting: ChannelFeatureReceive, -// Deleting: ChannelFeatureSend, +// Creating: ChannelFeatureBoth, +// Editing: ChannelFeatureBoth, +// Quoting: ChannelFeatureReceive, +// Deleting: ChannelFeatureSend, // MaxCharsCount: 2000, // }, // Product: Product{ // Creating: ChannelFeatureSend, +// Editing: ChannelFeatureNone, // Deleting: ChannelFeatureSend, // }, // Order: Order{ // Creating: ChannelFeatureBoth, +// Editing: ChannelFeatureNone, // Deleting: ChannelFeatureSend, // }, +// File: ChannelSettingsFilesBase{ +// Creating: ChannelFeatureBoth, +// Editing: ChannelFeatureBoth, +// Quoting: ChannelFeatureBoth, +// Deleting: ChannelFeatureBoth, +// Max: 10, +// NoteMaxCharsCount: uint16Ptr(256), +// MaxItemSize: mbToBytes(50), +// }, +// Image: ChannelSettingsFilesBase{ +// Creating: ChannelFeatureBoth, +// Editing: ChannelFeatureBoth, +// Quoting: ChannelFeatureBoth, +// Deleting: ChannelFeatureBoth, +// Max: 10, +// NoteMaxCharsCount: uint16Ptr(256), +// MaxItemSize: mbToBytes(10), +// }, +// Suggestions: ChannelSettingsSuggestions{ +// Text: ChannelFeatureBoth, +// Phone: ChannelFeatureBoth, +// Email: ChannelFeatureBoth, +// }, +// Audio: ChannelSettingsAudio{ +// Creating: ChannelFeatureBoth, +// Quoting: ChannelFeatureBoth, +// Deleting: ChannelFeatureBoth, +// MaxItemSize: mbToBytes(10), +// }, // }, -// } -// -// data, status, err := client.ActivateTransportChannel(request) -// +// }) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("%s\n", data.CreatedAt) +// log.Printf("status: %d, channel external_id: %s", status, resp.ExternalID) func (c *MgClient) ActivateTransportChannel(request Channel) (ActivateResponse, int, error) { var resp ActivateResponse outgoing, _ := json.Marshal(&request) @@ -316,45 +320,81 @@ func (c *MgClient) ActivateTransportChannel(request Channel) (ActivateResponse, return resp, status, err } -// UpdateTransportChannel implement channel activation +// UpdateTransportChannel updates an existing channel with provided settings. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// uint16Ptr := func(val uint16) *uint16 { +// return &val +// } +// mbToBytes := func(val uint64) *uint64 { +// val = val * 1024 * 1024 +// return &val +// } // -// request := ActivateRequest{ -// ID: 3053450384, +// resp, status, err := client.UpdateTransportChannel(Channel{ +// ID: 305, // Type: "telegram", // Name: "@my_shopping_bot", // Settings: ChannelSettings{ // Status: Status{ // Delivered: ChannelFeatureNone, -// Read: ChannelFeatureReceive, +// Read: ChannelFeatureReceive, // }, // Text: ChannelSettingsText{ -// Creating: ChannelFeatureBoth, -// Editing: ChannelFeatureSend, -// Quoting: ChannelFeatureReceive, -// Deleting: ChannelFeatureBoth, +// Creating: ChannelFeatureBoth, +// Editing: ChannelFeatureBoth, +// Quoting: ChannelFeatureReceive, +// Deleting: ChannelFeatureSend, +// MaxCharsCount: 2000, // }, // Product: Product{ // Creating: ChannelFeatureSend, +// Editing: ChannelFeatureNone, // Deleting: ChannelFeatureSend, // }, // Order: Order{ // Creating: ChannelFeatureBoth, +// Editing: ChannelFeatureNone, // Deleting: ChannelFeatureSend, // }, +// File: ChannelSettingsFilesBase{ +// Creating: ChannelFeatureBoth, +// Editing: ChannelFeatureBoth, +// Quoting: ChannelFeatureBoth, +// Deleting: ChannelFeatureBoth, +// Max: 10, +// NoteMaxCharsCount: uint16Ptr(256), +// MaxItemSize: mbToBytes(50), +// }, +// Image: ChannelSettingsFilesBase{ +// Creating: ChannelFeatureBoth, +// Editing: ChannelFeatureBoth, +// Quoting: ChannelFeatureBoth, +// Deleting: ChannelFeatureBoth, +// Max: 10, +// NoteMaxCharsCount: uint16Ptr(256), +// MaxItemSize: mbToBytes(10), +// }, +// Suggestions: ChannelSettingsSuggestions{ +// Text: ChannelFeatureBoth, +// Phone: ChannelFeatureBoth, +// Email: ChannelFeatureBoth, +// }, +// Audio: ChannelSettingsAudio{ +// Creating: ChannelFeatureBoth, +// Quoting: ChannelFeatureBoth, +// Deleting: ChannelFeatureBoth, +// MaxItemSize: mbToBytes(10), +// }, // }, -// } -// -// data, status, err := client.UpdateTransportChannel(request) -// +// }) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("%s\n", data.UpdatedAt) +// log.Printf("status: %d, channel_id: %d", status, resp.ChannelID) func (c *MgClient) UpdateTransportChannel(request Channel) (UpdateResponse, int, error) { var resp UpdateResponse outgoing, _ := json.Marshal(&request) @@ -375,19 +415,18 @@ func (c *MgClient) UpdateTransportChannel(request Channel) (UpdateResponse, int, return resp, status, err } -// DeactivateTransportChannel implement channel deactivation +// DeactivateTransportChannel deactivates the channel by its ID. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// -// data, status, err := client.DeactivateTransportChannel(3053450384) +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // +// resp, status, err := client.DeactivateTransportChannel(305) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("%s\n", data.DeactivatedAt) +// log.Printf("status: %d, deactivated at: %s", status, resp.DeactivatedAt) func (c *MgClient) DeactivateTransportChannel(id uint64) (DeleteResponse, int, error) { var resp DeleteResponse var buf []byte @@ -411,35 +450,49 @@ func (c *MgClient) DeactivateTransportChannel(id uint64) (DeleteResponse, int, e return resp, status, err } -// Messages implement send message +// Messages sends new message. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// msg := SendData{ -// SendMessage{ -// Message{ -// ExternalID: "274628", -// Type: "text", -// Text: "hello!", +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// getReplyDeadline := func(after time.Duration) *time.Time { +// deadline := time.Now().Add(after) +// return &deadline +// } +// +// resp, status, err := client.Messages(SendData{ +// Message: Message{ +// ExternalID: "uid_1", +// Type: MsgTypeText, +// Text: "Hello customer!", +// PageLink: "https://example.com", +// }, +// Originator: OriginatorCustomer, +// Customer: Customer{ +// ExternalID: "client_id_1", +// Nickname: "customer", +// Firstname: "Tester", +// Lastname: "Tester", +// Avatar: "https://example.com/image.png", +// ProfileURL: "https://example.com/user/client_id_1", +// Language: "en", +// Utm: &Utm{ +// Source: "myspace.com", +// Medium: "social", +// Campaign: "something", +// Term: "fedora", +// Content: "autumn_collection", // }, -// time.Now(), // }, -// User{ -// ExternalID: "8", -// Nickname: "@octopus", -// Firstname: "Joe", -// }, -// 10, -// } -// -// data, status, err := client.Messages(msg) -// +// Channel: 305, +// ExternalChatID: "chat_id_1", +// ReplyDeadline: getReplyDeadline(24 * time.Hour), +// }) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("%s\n", data.MessageID) +// log.Printf("status: %d, message ID: %d", status, resp.MessageID) func (c *MgClient) Messages(request SendData) (MessagesResponse, int, error) { var resp MessagesResponse outgoing, _ := json.Marshal(&request) @@ -460,37 +513,49 @@ func (c *MgClient) Messages(request SendData) (MessagesResponse, int, error) { return resp, status, err } -// MessagesHistory implement history message sending. +// MessagesHistory sends history message. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// msg := v1.SendHistoryMessageRequest{ -// Message: v1.SendMessageRequestMessage{ -// Type: v1.MsgTypeText, -// ExternalID: "external_id", -// CreatedAt: v1.TimePtr(time.Now()), -// IsComment: false, -// Text: "Test message", -// }, -// ChannelID: 1, -// ExternalChatID: "chat_id", -// Customer: &v1.Customer{ -// ExternalID: "1", -// Nickname: "@john_doe", -// Firstname: "John", -// Lastname: "Doe", -// }, -// Originator: v1.OriginatorCustomer, -// ReplyDeadline: v1.TimePtr(time.Now().Add(time.Hour * 24)), +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// getModifiedNow := func(after time.Duration) *time.Time { +// deadline := time.Now().Add(after) +// return &deadline // } // -// data, status, err := client.MessagesHistory(msg) +// resp, status, err := client.MessagesHistory(SendHistoryMessageRequest{ +// Message: SendMessageRequestMessage{ +// ExternalID: "uid_1", +// Type: MsgTypeText, +// Text: "Hello customer!", +// CreatedAt: getModifiedNow(-time.Hour), +// }, +// Originator: OriginatorCustomer, +// Customer: &Customer{ +// ExternalID: "client_id_1", +// Nickname: "customer", +// Firstname: "Tester", +// Lastname: "Tester", +// Avatar: "https://example.com/image.png", +// ProfileURL: "https://example.com/user/client_id_1", +// Language: "en", +// Utm: &Utm{ +// Source: "myspace.com", +// Medium: "social", +// Campaign: "something", +// Term: "fedora", +// Content: "autumn_collection", +// }, +// }, +// ChannelID: 305, +// ExternalChatID: "chat_id_1", +// ReplyDeadline: getModifiedNow(24 * time.Hour), +// }) // if err != nil { -// fmt.Printf("[%d]: %v", status, err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("%d\n", data.MessageID) +// log.Printf("status: %d, message ID: %d", status, resp.MessageID) func (c *MgClient) MessagesHistory(request SendHistoryMessageRequest) (MessagesResponse, int, error) { var ( resp MessagesResponse @@ -514,30 +579,24 @@ func (c *MgClient) MessagesHistory(request SendHistoryMessageRequest) (MessagesR return resp, status, err } -// UpdateMessages implement edit message +// UpdateMessages edits existing message. Only text messages are supported. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// msg := UpdateData{ -// UpdateMessage{ -// Message{ -// ExternalID: "274628", -// Type: "text", -// Text: "hello hello!", -// }, -// MakeTimestamp(), +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// +// resp, status, err := client.UpdateMessages(EditMessageRequest{ +// Message: EditMessageRequestMessage{ +// ExternalID: "message_id_1", +// Text: "This is a new text!", // }, -// 10, -// } -// -// data, status, err := client.UpdateMessages(msg) -// +// Channel: 305, +// }) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("%s\n", data.MessageID) +// log.Printf("status: %d, message ID: %d", status, resp.MessageID) func (c *MgClient) UpdateMessages(request EditMessageRequest) (MessagesResponse, int, error) { var resp MessagesResponse outgoing, _ := json.Marshal(&request) @@ -558,25 +617,23 @@ func (c *MgClient) UpdateMessages(request EditMessageRequest) (MessagesResponse, return resp, status, err } -// MarkMessageRead send message read event to MG +// MarkMessageRead send message read event to MG. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// msg := MarkMessageReadRequest{ -// Message{ -// ExternalID: "274628", +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// +// _, status, err := client.MarkMessageRead(MarkMessageReadRequest{ +// Message: MarkMessageReadRequestMessage{ +// ExternalID: "message_id_1", // }, -// 10, -// } -// -// data, status, err := client.MarkMessageRead(msg) -// +// ChannelID: 305, +// }) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("%v %v\n", status, data) +// log.Printf("status: %d", status) func (c *MgClient) MarkMessageRead(request MarkMessageReadRequest) (MarkMessageReadResponse, int, error) { var resp MarkMessageReadResponse outgoing, _ := json.Marshal(&request) @@ -597,22 +654,21 @@ func (c *MgClient) MarkMessageRead(request MarkMessageReadRequest) (MarkMessageR return resp, status, err } -// AckMessage implements ack of message +// AckMessage sets success status for message or appends an error to message. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// -// request := AckMessageRequest{ -// ExternalMessageID: "274628", -// Channel: 10, -// } -// -// status, err := client.AckMessage(request) +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // +// status, err := client.AckMessage(AckMessageRequest{ +// ExternalMessageID: "message_id_1", +// Channel: 305, +// }) // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } +// +// log.Printf("status: %d", status) func (c *MgClient) AckMessage(request AckMessageRequest) (int, error) { outgoing, _ := json.Marshal(&request) @@ -632,20 +688,18 @@ func (c *MgClient) AckMessage(request AckMessageRequest) (int, error) { // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // -// request := ReadUntilRequest{ -// ExternalMessageID: "274628", -// Channel: 10, -// } -// -// resp, status, err := client.ReadUntil(request) +// resp, status, err := client.ReadUntil(MarkMessagesReadUntilRequest{ +// CustomerExternalID: "customer_id_1", +// ChannelID: 305, +// Until: time.Now().Add(-time.Hour), +// }) // if err != nil { -// fmt.Printf("%v", err) -// } -// if resp != nil { -// fmt.Printf("Marked these as read: %s", resp.IDs) +// log.Fatalf("request error: %s (%d)", err, status) // } +// +// log.Printf("status: %d, marked messages: %+v", status, resp.IDs) func (c *MgClient) ReadUntil(request MarkMessagesReadUntilRequest) (*MarkMessagesReadUntilResponse, int, error) { outgoing, _ := json.Marshal(&request) @@ -664,27 +718,23 @@ func (c *MgClient) ReadUntil(request MarkMessagesReadUntilRequest) (*MarkMessage return resp, status, nil } -// DeleteMessage implement delete message +// DeleteMessage removes the message. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // -// msg := DeleteData{ -// Message{ -// ExternalID: "274628", -// }, -// 10, -// } +// resp, status, err := client.DeleteMessage(DeleteData{ +// Message: Message{ +// ExternalID: "message_id_1", +// }, +// Channel: 305, +// }) +// if err != nil { +// log.Fatalf("request error: %s (%d)", err, status) +// } // -// previousChatMessage, status, err := client.DeleteMessage(msg) -// if err != nil { -// fmt.Printf("%v", err) -// } -// -// if previousChatMessage != nil { -// fmt.Printf("Previous chat message id = %d", previousChatMessage.MessageID) -// } +// log.Printf("status: %d, message ID: %d", status, resp.MessageID) func (c *MgClient) DeleteMessage(request DeleteData) (*MessagesResponse, int, error) { outgoing, _ := json.Marshal(&request) @@ -707,19 +757,18 @@ func (c *MgClient) DeleteMessage(request DeleteData) (*MessagesResponse, int, er return previousChatMessage, status, nil } -// GetFile implement get file url +// GetFile returns file information by its ID. // // Example: // -// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") -// -// data, status, err := client.GetFile("file_ID") +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") // +// resp, status, err := client.GetFile("file_id") // if err != nil { -// fmt.Printf("%v", err) +// log.Fatalf("request error: %s (%d)", err, status) // } // -// fmt.Printf("%s\n", data.MessageID) +// log.Printf("status: %d, file URL: %s", status, resp.Url) func (c *MgClient) GetFile(request string) (FullFileResponse, int, error) { var resp FullFileResponse var b []byte @@ -741,7 +790,29 @@ func (c *MgClient) GetFile(request string) (FullFileResponse, int, error) { return resp, status, err } -// UploadFile upload file. +// UploadFile uploads a file. +// +// Example: +// +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// +// file, err := os.Open("/tmp/file.png") +// if err != nil { +// log.Fatalf("cannot open file for reading: %s", err) +// } +// defer func() { _ = file.Close() }() +// +// data, err := io.ReadAll(file) +// if err != nil { +// log.Fatalf("cannot read file data: %s", err) +// } +// +// resp, status, err := client.UploadFile(bytes.NewReader(data)) +// if err != nil { +// log.Fatalf("request error: %s (%d)", err, status) +// } +// +// log.Printf("status: %d, file ID: %s", status, resp.ID) func (c *MgClient) UploadFile(request io.Reader) (UploadFileResponse, int, error) { var resp UploadFileResponse @@ -761,7 +832,20 @@ func (c *MgClient) UploadFile(request io.Reader) (UploadFileResponse, int, error return resp, status, err } -// UploadFileByURL upload file by url. +// UploadFileByURL uploads a file from provided URL. +// +// Example: +// +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// +// resp, status, err := client.UploadFileByURL(UploadFileByUrlRequest{ +// Url: "https://example.com/file.png", +// }) +// if err != nil { +// log.Fatalf("request error: %s (%d)", err, status) +// } +// +// log.Printf("status: %d, file ID: %s", status, resp.ID) func (c *MgClient) UploadFileByURL(request UploadFileByUrlRequest) (UploadFileResponse, int, error) { var resp UploadFileResponse outgoing, _ := json.Marshal(&request) @@ -782,7 +866,11 @@ func (c *MgClient) UploadFileByURL(request UploadFileByUrlRequest) (UploadFileRe return resp, status, err } -// MakeTimestamp returns current unix timestamp. +// MakeTimestamp returns current unix timestamp in milliseconds. +// +// Example: +// +// fmt.Printf("UNIX timestamp in milliseconds: %d", MakeTimestamp()) func MakeTimestamp() int64 { return time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond)) } diff --git a/v1/errors.go b/v1/errors.go index d8923fd..f25d16b 100644 --- a/v1/errors.go +++ b/v1/errors.go @@ -13,20 +13,24 @@ var defaultErrorMessage = "http client error" var internalServerError = "internal server error" var marshalError = "cannot unmarshal response body" +// MGErrors contains a list of errors as sent by MessageGateway. type MGErrors struct { Errors []string } +// HTTPClientError is a common error type used in the client. type HTTPClientError struct { ErrorMsg string BaseError error Response io.Reader } +// Unwrap returns underlying error. Its presence usually indicates a problem with the network. func (err *HTTPClientError) Unwrap() error { return err.BaseError } +// Error message will contain either an error from MG or underlying error message. func (err *HTTPClientError) Error() string { message := defaultErrorMessage @@ -39,10 +43,12 @@ func (err *HTTPClientError) Error() string { return message } +// NewCriticalHTTPError wraps *http.Client error. func NewCriticalHTTPError(err error) error { return &HTTPClientError{BaseError: err} } +// NewAPIClientError wraps MG error. func NewAPIClientError(responseBody []byte) error { var data MGErrors var message string @@ -62,6 +68,7 @@ func NewAPIClientError(responseBody []byte) error { return &HTTPClientError{ErrorMsg: message} } +// NewServerError wraps an unexpected API error (e.g. 5xx). func NewServerError(response *http.Response) error { var serverError *HTTPClientError diff --git a/v1/helpers.go b/v1/helpers.go index d60cac2..b1af775 100644 --- a/v1/helpers.go +++ b/v1/helpers.go @@ -23,10 +23,12 @@ func buildLimitedRawResponse(resp *http.Response) ([]byte, error) { return body, nil } +// BoolPtr returns provided boolean as pointer. Can be used while editing the integration module activity. func BoolPtr(v bool) *bool { return &v } +// TimePtr returns provided time.Time's pointer. func TimePtr(v time.Time) *time.Time { return &v } diff --git a/v1/request.go b/v1/request.go index b72f135..64162a4 100644 --- a/v1/request.go +++ b/v1/request.go @@ -10,7 +10,7 @@ import ( var prefix = "/api/transport/v1" -// GetRequest implements GET Request. +// GetRequest performs GET request to the provided route. func (c *MgClient) GetRequest(url string, parameters []byte) ([]byte, int, error) { return makeRequest( "GET", @@ -20,7 +20,7 @@ func (c *MgClient) GetRequest(url string, parameters []byte) ([]byte, int, error ) } -// PostRequest implements POST Request. +// PostRequest performs POST request to the provided route. func (c *MgClient) PostRequest(url string, parameters io.Reader) ([]byte, int, error) { return makeRequest( "POST", @@ -30,7 +30,7 @@ func (c *MgClient) PostRequest(url string, parameters io.Reader) ([]byte, int, e ) } -// PutRequest implements PUT Request. +// PutRequest performs PUT request to the provided route. func (c *MgClient) PutRequest(url string, parameters []byte) ([]byte, int, error) { return makeRequest( "PUT", @@ -40,7 +40,7 @@ func (c *MgClient) PutRequest(url string, parameters []byte) ([]byte, int, error ) } -// DeleteRequest implements DELETE Request. +// DeleteRequest performs DELETE request to the provided route. func (c *MgClient) DeleteRequest(url string, parameters []byte) ([]byte, int, error) { return makeRequest( "DELETE", diff --git a/v1/webhook.go b/v1/webhook.go index 6b11e4f..c9e369d 100644 --- a/v1/webhook.go +++ b/v1/webhook.go @@ -27,27 +27,47 @@ func (w WebhookRequest) IsMessageWebhook() bool { w.Type == MessageSendWebhookType || w.Type == MessageUpdateWebhookType } +// IsTemplateWebhook returns true if current webhook contains data related to the templates changes. func (w WebhookRequest) IsTemplateWebhook() bool { return w.Type == TemplateCreateWebhookType || w.Type == TemplateUpdateWebhookType || w.Type == TemplateDeleteWebhookType } +// MessageWebhookData returns the message data from webhook contents. +// +// Note: this call will not fail even if underlying data is not related to the messages. +// Use IsMessageWebhook to mitigate this. func (w WebhookRequest) MessageWebhookData() (wd MessageWebhookData) { _ = json.Unmarshal(w.Data, &wd) return } +// TemplateCreateWebhookData returns new template data from webhook contents. +// This method is used if current webhook was initiated because user created a template. +// +// Note: this call will not fail even if underlying data is not related to the templates. +// Use IsTemplateWebhook or direct Type comparison (Type == TemplateCreateWebhookType) to mitigate this. func (w WebhookRequest) TemplateCreateWebhookData() (wd TemplateCreateWebhookData) { _ = json.Unmarshal(w.Data, &wd) return } +// TemplateUpdateWebhookData returns existing template data from webhook contents. +// This method is used if current webhook was initiated because user updated a template. +// +// Note: this call will not fail even if underlying data is not related to the templates. +// Use IsTemplateWebhook or direct Type comparison (Type == TemplateUpdateWebhookData) to mitigate this. func (w WebhookRequest) TemplateUpdateWebhookData() (wd TemplateUpdateWebhookData) { _ = json.Unmarshal(w.Data, &wd) return } +// TemplateDeleteWebhookData returns existing template data from webhook contents. +// This method is used if current webhook was initiated because user deleted a template. +// +// Note: this call will not fail even if underlying data is not related to the templates. +// Use IsTemplateWebhook or direct Type comparison (Type == TemplateDeleteWebhookType) to mitigate this. func (w WebhookRequest) TemplateDeleteWebhookData() (wd TemplateDeleteWebhookData) { _ = json.Unmarshal(w.Data, &wd) return From 26684fb114d5dd20b4539a813e09873d24fb84e9 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Thu, 28 Dec 2023 19:32:03 +0300 Subject: [PATCH 03/11] wip: usage examples --- examples/telegram/.gitignore | 1 + examples/telegram/config.go | 42 ++++++++++ examples/telegram/config.json.dist | 7 ++ examples/telegram/go.mod | 41 +++++++++ examples/telegram/go.sum | 106 +++++++++++++++++++++++ examples/telegram/main.go | 10 +++ examples/telegram/register.go | 104 +++++++++++++++++++++++ examples/telegram/server.go | 130 +++++++++++++++++++++++++++++ examples/telegram/services.go | 12 +++ examples/telegram/tg.go | 27 ++++++ 10 files changed, 480 insertions(+) create mode 100644 examples/telegram/.gitignore create mode 100644 examples/telegram/config.go create mode 100644 examples/telegram/config.json.dist create mode 100644 examples/telegram/go.mod create mode 100644 examples/telegram/go.sum create mode 100644 examples/telegram/main.go create mode 100644 examples/telegram/register.go create mode 100644 examples/telegram/server.go create mode 100644 examples/telegram/services.go create mode 100644 examples/telegram/tg.go diff --git a/examples/telegram/.gitignore b/examples/telegram/.gitignore new file mode 100644 index 0000000..0cffcb3 --- /dev/null +++ b/examples/telegram/.gitignore @@ -0,0 +1 @@ +config.json \ No newline at end of file diff --git a/examples/telegram/config.go b/examples/telegram/config.go new file mode 100644 index 0000000..ca9ad61 --- /dev/null +++ b/examples/telegram/config.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "github.com/go-playground/validator/v10" + "log" + "os" + "strings" +) + +var AppConfig Config + +type Config struct { + Listen string `json:"listen"` + BaseURL string `json:"baseUrl" validate:"required,url"` + System string `json:"system" validate:"required,url"` + APIKey string `json:"apiKey" validate:"required"` + TGBotToken string `json:"tgBotToken" validate:"required"` +} + +func LoadConfig(src string) { + file, err := os.Open(src) + if err != nil { + panic(err) + } + defer func() { _ = file.Close() }() + if err := json.NewDecoder(file).Decode(&AppConfig); err != nil { + panic(err) + } + validate := validator.New(validator.WithRequiredStructEnabled()) + err = validate.Struct(AppConfig) + if err != nil { + log.Fatalln(err) + } + if strings.HasSuffix(AppConfig.BaseURL, "/") { + AppConfig.BaseURL = AppConfig.BaseURL[:len(AppConfig.BaseURL)-1] + } + if AppConfig.Listen == "" { + AppConfig.Listen = ":8080" + } + log.Println("loaded configuration from", src) +} diff --git a/examples/telegram/config.json.dist b/examples/telegram/config.json.dist new file mode 100644 index 0000000..f93c4a7 --- /dev/null +++ b/examples/telegram/config.json.dist @@ -0,0 +1,7 @@ +{ + "addr": ":8080", + "baseUrl": "https://demo.localhost.run", + "system": "https://test.retailcrm.pro", + "apiKey": "apiKey", + "tgBotToken": "telegram" +} \ No newline at end of file diff --git a/examples/telegram/go.mod b/examples/telegram/go.mod new file mode 100644 index 0000000..f050597 --- /dev/null +++ b/examples/telegram/go.mod @@ -0,0 +1,41 @@ +module github.com/retailcrm/mg-transport-api-client-go/examples/telegram + +go 1.21.5 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/go-playground/validator/v10 v10.16.0 + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/retailcrm/api-client-go/v2 v2.1.12 + github.com/retailcrm/mg-transport-api-client-go v1.3.8 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.8.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/telegram/go.sum b/examples/telegram/go.sum new file mode 100644 index 0000000..1d62e5d --- /dev/null +++ b/examples/telegram/go.sum @@ -0,0 +1,106 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/retailcrm/api-client-go/v2 v2.1.12 h1:eiFfSvxgjOyWEYEhg44XoCGf7ud30jVLxGr8t9/3YKc= +github.com/retailcrm/api-client-go/v2 v2.1.12/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ= +github.com/retailcrm/mg-transport-api-client-go v1.3.8 h1:tHR6ePZONmjYHaEQx28yGEVWQh4jVTa0K/dmq++15YY= +github.com/retailcrm/mg-transport-api-client-go v1.3.8/go.mod h1:gDe/tj7t3Hr/uwIFSBVgGAmP85PoLajVl1A+skBo1Ro= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/examples/telegram/main.go b/examples/telegram/main.go new file mode 100644 index 0000000..336b681 --- /dev/null +++ b/examples/telegram/main.go @@ -0,0 +1,10 @@ +package main + +func main() { + LoadConfig("config.json") + RegisterSystem() + InitTGBotAPI() + SetTGWebhook() + RegisterChannel() + Listen() +} diff --git a/examples/telegram/register.go b/examples/telegram/register.go new file mode 100644 index 0000000..2b27ecc --- /dev/null +++ b/examples/telegram/register.go @@ -0,0 +1,104 @@ +package main + +import ( + retailcrm "github.com/retailcrm/api-client-go/v2" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" + "log" +) + +func RegisterSystem() { + client := retailcrm.New(AppConfig.System, AppConfig.APIKey) + resp, _, err := client.IntegrationModuleEdit(retailcrm.IntegrationModule{ + Code: "telegram-bot-integration-example", + IntegrationCode: "telegram-bot-integration-example", + Active: v1.BoolPtr(true), + Name: "Telegram Bot Integration Example", + ClientID: "telegram-bot-integration-example", + BaseURL: AppConfig.BaseURL, + AccountURL: AppConfig.BaseURL, + Integrations: &retailcrm.Integrations{ + MgTransport: &retailcrm.MgTransport{ + WebhookURL: AppConfig.BaseURL + "/api/v1/webhook", + }, + }, + }) + if err != nil { + log.Fatalln("cannot edit integration module:", err) + } + MG = v1.New(resp.Info.MgTransportInfo.EndpointURL, resp.Info.MgTransportInfo.Token) + log.Println("updated integration module") +} + +func RegisterChannel() { + channels, _, err := MG.TransportChannels(v1.Channels{}) + if err != nil { + log.Fatalln("cannot get channels:", err) + } + channel := v1.Channel{ + Type: "telegram", + Name: "@" + TG.Self.UserName, + Settings: getChannelSettings(), + } + for _, ch := range channels { + if ch.Name != nil && *ch.Name == "@"+TG.Self.UserName { + channel.ID = ch.ID + _, _, err := MG.UpdateTransportChannel(channel) + if err != nil { + log.Fatalln("cannot update channel:", err) + } + Channel = &channel + log.Println("updated MG channel") + return + } + } + resp, _, err := MG.ActivateTransportChannel(channel) + if err != nil { + log.Fatalln("cannot activate channel:", err) + } + log.Println("activated MG channel with id:", resp.ChannelID) + channel.ID = resp.ChannelID + Channel = &channel +} + +func getChannelSettings() v1.ChannelSettings { + return v1.ChannelSettings{ + Status: v1.Status{ + Delivered: v1.ChannelFeatureNone, + Read: v1.ChannelFeatureNone, + }, + Text: v1.ChannelSettingsText{ + Creating: v1.ChannelFeatureBoth, + Editing: v1.ChannelFeatureNone, + Quoting: v1.ChannelFeatureNone, + Deleting: v1.ChannelFeatureNone, + MaxCharsCount: 2000, + }, + Product: v1.Product{ + Creating: v1.ChannelFeatureNone, + Editing: v1.ChannelFeatureNone, + Deleting: v1.ChannelFeatureNone, + }, + Order: v1.Order{ + Creating: v1.ChannelFeatureNone, + Editing: v1.ChannelFeatureNone, + Deleting: v1.ChannelFeatureNone, + }, + File: v1.ChannelSettingsFilesBase{ + Creating: v1.ChannelFeatureNone, + Editing: v1.ChannelFeatureNone, + Quoting: v1.ChannelFeatureNone, + Deleting: v1.ChannelFeatureNone, + }, + Image: v1.ChannelSettingsFilesBase{ + Creating: v1.ChannelFeatureNone, + Editing: v1.ChannelFeatureNone, + Quoting: v1.ChannelFeatureNone, + Deleting: v1.ChannelFeatureNone, + }, + Audio: v1.ChannelSettingsAudio{ + Creating: v1.ChannelFeatureNone, + Quoting: v1.ChannelFeatureNone, + Deleting: v1.ChannelFeatureNone, + }, + } +} diff --git a/examples/telegram/server.go b/examples/telegram/server.go new file mode 100644 index 0000000..4017fda --- /dev/null +++ b/examples/telegram/server.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "errors" + "fmt" + "github.com/gin-gonic/gin" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" +) + +const ChatIDPrefix = "tg_" + +func Listen() { + router := gin.Default() + router.POST("/api/v1/mg", MGWebhookHandler) + router.POST("/api/v1/tg", TGWebhookHandler) + + srv := &http.Server{ + Addr: AppConfig.Listen, + Handler: router, + } + go func() { + log.Printf("listening on %s", AppConfig.Listen) + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + quit := make(chan os.Signal) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("shutting down:", err) + } + select { + case <-ctx.Done(): + log.Println("quitting...") + } +} + +func MGWebhookHandler(c *gin.Context) { + var wh v1.WebhookRequest + if err := c.ShouldBindJSON(&wh); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, + v1.NewTransportErrorResponse(v1.MessageErrorGeneral, "invalid webhook data")) + return + } + + if wh.Type != v1.MessageSendWebhookType { + c.AbortWithStatusJSON(http.StatusUnprocessableEntity, + v1.NewTransportErrorResponse(v1.MessageErrorGeneral, "unsupported webhook type")) + return + } + + whMsg := wh.MessageWebhookData() + if strings.HasPrefix(whMsg.ExternalChatID, ChatIDPrefix) { + c.AbortWithStatusJSON(http.StatusUnprocessableEntity, + v1.NewTransportErrorResponse(v1.MessageErrorGeneral, "unexpected chat ID")) + return + } + + chatID, err := strconv.ParseInt(whMsg.ExternalChatID[len(ChatIDPrefix):], 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnprocessableEntity, + v1.NewTransportErrorResponse(v1.MessageErrorGeneral, "unparsable chat ID")) + return + } + + resp, err := TG.Send(tgbotapi.NewMessage(chatID, whMsg.Content)) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnprocessableEntity, + v1.NewTransportErrorResponse(v1.MessageErrorGeneral, err.Error())) + return + } + c.JSON(http.StatusOK, v1.NewSentMessageResponse(strconv.Itoa(resp.MessageID))) +} + +func TGWebhookHandler(c *gin.Context) { + var update tgbotapi.Update + if err := c.ShouldBindJSON(&update); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) + return + } + + if update.Message == nil { + c.AbortWithStatus(http.StatusOK) + return + } + + _, _, err := MG.Messages(v1.SendData{ + Message: v1.Message{ + ExternalID: newExternalMessageID(update.Message.MessageID), + Type: v1.MsgTypeText, + Text: update.Message.Text, + }, + Originator: v1.OriginatorCustomer, + Customer: v1.Customer{ + ExternalID: strconv.FormatInt(update.Message.From.ID, 10), + Nickname: update.Message.From.UserName, + Firstname: update.Message.From.FirstName, + Lastname: update.Message.From.LastName, + ProfileURL: fmt.Sprintf("https://t.me/%s", update.Message.From.UserName), + Language: update.Message.From.LanguageCode, + }, + Channel: Channel.ID, + ExternalChatID: ChatIDPrefix + strconv.FormatInt(update.Message.Chat.ID, 10), + }) + if err != nil { + log.Printf("error: cannot send message: %s", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "cannot send message"}) + return + } + c.JSON(http.StatusOK, gin.H{}) +} + +func newExternalMessageID(messageID int) string { + return fmt.Sprintf("%d-%d", TG.Self.ID, messageID) +} diff --git a/examples/telegram/services.go b/examples/telegram/services.go new file mode 100644 index 0000000..795d4cf --- /dev/null +++ b/examples/telegram/services.go @@ -0,0 +1,12 @@ +package main + +import ( + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" +) + +var ( + Channel *v1.Channel + MG *v1.MgClient + TG *tgbotapi.BotAPI +) diff --git a/examples/telegram/tg.go b/examples/telegram/tg.go new file mode 100644 index 0000000..66f7eaf --- /dev/null +++ b/examples/telegram/tg.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "log" +) + +func InitTGBotAPI() { + botAPI, err := tgbotapi.NewBotAPI(AppConfig.TGBotToken) + if err != nil { + log.Fatalln("cannot initialize TG Bot API:", err) + } + TG = botAPI + log.Printf("initialized Telegram Bot API for bot @%s", botAPI.Self.UserName) +} + +func SetTGWebhook() { + wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/api/v1/tg", AppConfig.BaseURL)) + if err != nil { + log.Fatalln("cannot initialize webhook data:", err) + } + _, err = TG.Request(wh) + if err != nil { + log.Fatalln("cannot register TG webhook:", err) + } +} From 7b482b17bc163ba1584a5fa8de62e6789e2fb886 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 29 Dec 2023 11:07:45 +0300 Subject: [PATCH 04/11] webhooks example --- examples/doc.go | 8 +++ examples/webhooks/main.go | 102 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 examples/doc.go create mode 100644 examples/webhooks/main.go diff --git a/examples/doc.go b/examples/doc.go new file mode 100644 index 0000000..82ec027 --- /dev/null +++ b/examples/doc.go @@ -0,0 +1,8 @@ +// Package examples provides a set of code samples that show how to use the library properly. +// +// Currently, there are two examples available: +// +// - webhooks - basic app that can display incoming webhooks. +// +// - telegram - very simple example of bidirectional Telegram transport (only text messages are supported). +package examples diff --git a/examples/webhooks/main.go b/examples/webhooks/main.go new file mode 100644 index 0000000..fdee9b6 --- /dev/null +++ b/examples/webhooks/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" + "log" + "net/http" + "os" + "strconv" + "time" +) + +// H is a basic hashmap type. +type H map[string]interface{} + +func main() { + addr := os.Getenv("ADDR") + if addr == "" { + addr = ":8080" + } + log.Println("listening on", addr) + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/webhook", HandleWebhook) + err := http.ListenAndServe(addr, mux) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen %s err: %s", addr, err) + } +} + +func HandleWebhook(rw http.ResponseWriter, req *http.Request) { + // You should authenticate the request. Usually it's done by some sort of token header. + var wh v1.WebhookRequest + if err := readJSON(req, &wh); err != nil { + serveError(rw, http.StatusBadRequest, err) + return + } + + switch wh.Type { + case v1.MessageSendWebhookType: + HandleSendWebhook(rw, wh.MessageWebhookData()) + case v1.MessageReadWebhookType: + HandleReadWebhook(rw, wh.MessageWebhookData()) + case v1.MessageDeleteWebhookType: + HandleDeleteWebhook(rw, wh.MessageWebhookData()) + case v1.TemplateCreateWebhookType: + HandleTemplateCreate(rw, wh.TemplateCreateWebhookData()) + case v1.TemplateUpdateWebhookType: + HandleTemplateUpdate(rw, wh.TemplateUpdateWebhookData()) + case v1.TemplateDeleteWebhookType: + HandleTemplateDelete(rw, wh.TemplateDeleteWebhookData()) + default: + serveError(rw, http.StatusUnprocessableEntity, fmt.Errorf("unknown webhook type: %s", wh.Type)) + } +} + +func HandleSendWebhook(rw http.ResponseWriter, msg v1.MessageWebhookData) { + log.Printf("incoming message: %#v", msg) + serveJSON(rw, http.StatusOK, v1.NewSentMessageResponse(strconv.FormatInt(time.Now().UnixNano(), 10))) +} + +func HandleReadWebhook(rw http.ResponseWriter, msg v1.MessageWebhookData) { + log.Printf("incoming message read status: %#v", msg) + serveJSON(rw, http.StatusOK, H{}) +} + +func HandleDeleteWebhook(rw http.ResponseWriter, msg v1.MessageWebhookData) { + log.Printf("incoming message removal: %#v", msg) + serveJSON(rw, http.StatusOK, H{}) +} + +func HandleTemplateCreate(rw http.ResponseWriter, tpl v1.TemplateCreateWebhookData) { + log.Printf("new template: %#v", tpl) + serveJSON(rw, http.StatusOK, H{}) +} + +func HandleTemplateUpdate(rw http.ResponseWriter, tpl v1.TemplateUpdateWebhookData) { + log.Printf("updated template: %#v", tpl) + serveJSON(rw, http.StatusOK, H{}) +} + +func HandleTemplateDelete(rw http.ResponseWriter, tpl v1.TemplateDeleteWebhookData) { + log.Printf("template removal: %#v", tpl) + serveJSON(rw, http.StatusOK, H{}) +} + +func readJSON(req *http.Request, out any) error { + defer func() { _ = req.Body.Close() }() + return json.NewDecoder(req.Body).Decode(out) +} + +func serveJSON(rw http.ResponseWriter, st int, data any) { + resp, _ := json.Marshal(data) + rw.Header().Set("Content-Type", "application/json; charset=utf-8") + rw.WriteHeader(st) + _, _ = fmt.Fprintln(rw, string(resp)) +} + +func serveError(rw http.ResponseWriter, st int, err error) { + serveJSON(rw, st, H{"error": err.Error()}) +} From f2a74857b7a07cfb33e012067df226fd406e29ad Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 29 Dec 2023 11:19:45 +0300 Subject: [PATCH 05/11] fix tg example --- examples/telegram/register.go | 2 +- examples/telegram/server.go | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/telegram/register.go b/examples/telegram/register.go index 2b27ecc..246a70e 100644 --- a/examples/telegram/register.go +++ b/examples/telegram/register.go @@ -18,7 +18,7 @@ func RegisterSystem() { AccountURL: AppConfig.BaseURL, Integrations: &retailcrm.Integrations{ MgTransport: &retailcrm.MgTransport{ - WebhookURL: AppConfig.BaseURL + "/api/v1/webhook", + WebhookURL: AppConfig.BaseURL + "/api/v1/mg", }, }, }) diff --git a/examples/telegram/server.go b/examples/telegram/server.go index 4017fda..2cab55c 100644 --- a/examples/telegram/server.go +++ b/examples/telegram/server.go @@ -39,10 +39,11 @@ func Listen() { signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + log.Println("shutting down, please wait...") + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { - log.Fatal("shutting down:", err) + log.Fatal("shutdown error:", err) } select { case <-ctx.Done(): @@ -65,7 +66,7 @@ func MGWebhookHandler(c *gin.Context) { } whMsg := wh.MessageWebhookData() - if strings.HasPrefix(whMsg.ExternalChatID, ChatIDPrefix) { + if !strings.HasPrefix(whMsg.ExternalChatID, ChatIDPrefix) { c.AbortWithStatusJSON(http.StatusUnprocessableEntity, v1.NewTransportErrorResponse(v1.MessageErrorGeneral, "unexpected chat ID")) return From 90a50feccb994a767714a39abfba36777a52a47e Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 29 Dec 2023 11:24:23 +0300 Subject: [PATCH 06/11] wip: package doc --- v1/doc.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ v1/errors.go | 6 ++++ 2 files changed, 99 insertions(+) create mode 100644 v1/doc.go diff --git a/v1/doc.go b/v1/doc.go new file mode 100644 index 0000000..310839d --- /dev/null +++ b/v1/doc.go @@ -0,0 +1,93 @@ +// Package v1 provides Go API Client implementation for MessageGateway Transport API. +// +// You can use v1.New or v1.NewWithClient to initialize API client. github.com/retailcrm/mg-transport-api-client-go/examples +// package contains some examples on how to use this library properly. +// +// Basic usage example: +// +// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") +// getReplyDeadline := func(after time.Duration) *time.Time { +// deadline := time.Now().Add(after) +// return &deadline +// } +// +// resp, status, err := client.Messages(SendData{ +// Message: Message{ +// ExternalID: "uid_1", +// Type: MsgTypeText, +// Text: "Hello customer!", +// PageLink: "https://example.com", +// }, +// Originator: OriginatorCustomer, +// Customer: Customer{ +// ExternalID: "client_id_1", +// Nickname: "customer", +// Firstname: "Tester", +// Lastname: "Tester", +// Avatar: "https://example.com/image.png", +// ProfileURL: "https://example.com/user/client_id_1", +// Language: "en", +// Utm: &Utm{ +// Source: "myspace.com", +// Medium: "social", +// Campaign: "something", +// Term: "fedora", +// Content: "autumn_collection", +// }, +// }, +// Channel: 305, +// ExternalChatID: "chat_id_1", +// ReplyDeadline: getReplyDeadline(24 * time.Hour), +// }) +// if err != nil { +// log.Fatalf("request error: %s (%d)", err, status) +// } +// +// log.Printf("status: %d, message ID: %d", status, resp.MessageID) +package v1 + +import ( + "log" + "time" +) + +func ooga() { + client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") + getReplyDeadline := func(after time.Duration) *time.Time { + deadline := time.Now().Add(after) + return &deadline + } + resp, status, err := client.Messages(SendData{ + Message: Message{ + ExternalID: "uid_1", + Type: MsgTypeText, + Text: "Hello customer!", + PageLink: "https://example.com", + }, + Originator: OriginatorCustomer, + Customer: Customer{ + ExternalID: "client_id_1", + Nickname: "customer", + Firstname: "Tester", + Lastname: "Tester", + Avatar: "https://example.com/image.png", + ProfileURL: "https://example.com/user/client_id_1", + Language: "en", + Utm: &Utm{ + Source: "myspace.com", + Medium: "social", + Campaign: "something", + Term: "fedora", + Content: "autumn_collection", + }, + }, + Channel: 305, + ExternalChatID: "chat_id_1", + ReplyDeadline: getReplyDeadline(24 * time.Hour), + }) + if err != nil { + log.Fatalf("request error: %s (%d)", err, status) + } + + log.Printf("status: %d, message ID: %d", status, resp.MessageID) +} diff --git a/v1/errors.go b/v1/errors.go index f25d16b..e23db09 100644 --- a/v1/errors.go +++ b/v1/errors.go @@ -82,3 +82,9 @@ func NewServerError(response *http.Response) error { return err } + +func AsClientError(err error) *HTTPClientError { + for { + + } +} From a02b1fe8855a4d3f3dd0346a775eb0eb1de550ed Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 29 Dec 2023 11:30:09 +0300 Subject: [PATCH 07/11] package godoc --- v1/doc.go | 60 ++++++++++------------------------------------------ v1/errors.go | 8 ++++++- 2 files changed, 18 insertions(+), 50 deletions(-) diff --git a/v1/doc.go b/v1/doc.go index 310839d..7162f3c 100644 --- a/v1/doc.go +++ b/v1/doc.go @@ -10,7 +10,6 @@ // deadline := time.Now().Add(after) // return &deadline // } -// // resp, status, err := client.Messages(SendData{ // Message: Message{ // ExternalID: "uid_1", @@ -37,57 +36,20 @@ // }, // Channel: 305, // ExternalChatID: "chat_id_1", -// ReplyDeadline: getReplyDeadline(24 * time.Hour), +// ReplyDeadline: getReplyDeadline(24 * time.Hour), // }) // if err != nil { -// log.Fatalf("request error: %s (%d)", err, status) +// if clientErr := AsClientError(err); clientErr != nil { +// if clientErr.BaseError != nil { +// log.Fatalf("cannot perform the request: %s", clientErr.BaseError) +// } +// if clientErr.ErrorMsg != "" { +// log.Fatalf("MG error: %s", clientErr.ErrorMsg) +// } +// } +// +// log.Fatalf("general error: %s (%d)", err, status) // } // // log.Printf("status: %d, message ID: %d", status, resp.MessageID) package v1 - -import ( - "log" - "time" -) - -func ooga() { - client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6") - getReplyDeadline := func(after time.Duration) *time.Time { - deadline := time.Now().Add(after) - return &deadline - } - resp, status, err := client.Messages(SendData{ - Message: Message{ - ExternalID: "uid_1", - Type: MsgTypeText, - Text: "Hello customer!", - PageLink: "https://example.com", - }, - Originator: OriginatorCustomer, - Customer: Customer{ - ExternalID: "client_id_1", - Nickname: "customer", - Firstname: "Tester", - Lastname: "Tester", - Avatar: "https://example.com/image.png", - ProfileURL: "https://example.com/user/client_id_1", - Language: "en", - Utm: &Utm{ - Source: "myspace.com", - Medium: "social", - Campaign: "something", - Term: "fedora", - Content: "autumn_collection", - }, - }, - Channel: 305, - ExternalChatID: "chat_id_1", - ReplyDeadline: getReplyDeadline(24 * time.Hour), - }) - if err != nil { - log.Fatalf("request error: %s (%d)", err, status) - } - - log.Printf("status: %d, message ID: %d", status, resp.MessageID) -} diff --git a/v1/errors.go b/v1/errors.go index e23db09..2f7dd3e 100644 --- a/v1/errors.go +++ b/v1/errors.go @@ -85,6 +85,12 @@ func NewServerError(response *http.Response) error { func AsClientError(err error) *HTTPClientError { for { - + if err == nil { + return nil + } + if typed, ok := err.(*HTTPClientError); ok { + return typed + } + err = errors.Unwrap(err) } } From 512aa5c6e8875d42eca69f5ac60d3504846e5e24 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 29 Dec 2023 11:33:05 +0300 Subject: [PATCH 08/11] update test matrix --- .github/workflows/ci.yml | 4 ++-- examples/webhooks/main.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1143c9..c51bddc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - name: Set up latest Go 1.x version uses: actions/setup-go@v2 with: - go-version: '1.17' + go-version: '1.21' - name: Get dependencies run: | go mod tidy @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.13', '1.14', '1.15', '1.16', '1.17'] + go-version: ['1.13', '1.14', '1.15', '1.16', '1.17', '1.18', '1.19', '1.20', '1.21'] steps: - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v2 diff --git a/examples/webhooks/main.go b/examples/webhooks/main.go index fdee9b6..ec4a40a 100644 --- a/examples/webhooks/main.go +++ b/examples/webhooks/main.go @@ -85,12 +85,12 @@ func HandleTemplateDelete(rw http.ResponseWriter, tpl v1.TemplateDeleteWebhookDa serveJSON(rw, http.StatusOK, H{}) } -func readJSON(req *http.Request, out any) error { +func readJSON(req *http.Request, out interface{}) error { defer func() { _ = req.Body.Close() }() return json.NewDecoder(req.Body).Decode(out) } -func serveJSON(rw http.ResponseWriter, st int, data any) { +func serveJSON(rw http.ResponseWriter, st int, data interface{}) { resp, _ := json.Marshal(data) rw.Header().Set("Content-Type", "application/json; charset=utf-8") rw.WriteHeader(st) From 2ba427dfc3ae4c3318e56f093455d5eda90a6030 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 29 Dec 2023 11:37:01 +0300 Subject: [PATCH 09/11] test for a new func --- v1/errors_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/v1/errors_test.go b/v1/errors_test.go index 2c4bb2f..81cb600 100644 --- a/v1/errors_test.go +++ b/v1/errors_test.go @@ -64,3 +64,9 @@ func TestNewServerError(t *testing.T) { assert.NotNil(t, err.Response) } } + +func TestAsClientError(t *testing.T) { + assert.Nil(t, AsClientError(nil)) + assert.Nil(t, AsClientError(errors.New("arbitrary"))) + assert.NotNil(t, AsClientError(NewCriticalHTTPError(errors.New("arbitrary")))) +} From f72ec25093b62604cb6a120a2a08a7b5c4f6825f Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 29 Dec 2023 11:44:18 +0300 Subject: [PATCH 10/11] lint fixes & update golangci-lint --- .github/workflows/ci.yml | 2 +- .golangci.yml | 11 ++--------- v1/doc.go | 5 +++-- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c51bddc..2569ecd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Lint code with golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.36 + version: v1.55.2 only-new-issues: true tests: name: Tests diff --git a/.golangci.yml b/.golangci.yml index ed8c8bd..02a09bc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,16 +9,13 @@ output: linters: disable-all: true enable: - - deadcode - errcheck - gosimple - govet - ineffassign - staticcheck - - structcheck - unused - unparam - - varcheck - bodyclose - dogsled - dupl @@ -32,11 +29,9 @@ linters: - gocyclo - godot - goimports - - golint + - revive - gomnd - gosec - - ifshort - - interfacer - lll - makezero - maligned @@ -44,10 +39,10 @@ linters: - nestif - prealloc - predeclared - - scopelint - sqlclosecheck - unconvert - whitespace + - unused linters-settings: govet: @@ -143,8 +138,6 @@ linters-settings: local-prefixes: github.com/retailcrm/mg-transport-api-client-go lll: line-length: 120 - maligned: - suggest-new: true misspell: locale: US nestif: diff --git a/v1/doc.go b/v1/doc.go index 7162f3c..cc9ae67 100644 --- a/v1/doc.go +++ b/v1/doc.go @@ -1,7 +1,8 @@ // Package v1 provides Go API Client implementation for MessageGateway Transport API. // -// You can use v1.New or v1.NewWithClient to initialize API client. github.com/retailcrm/mg-transport-api-client-go/examples -// package contains some examples on how to use this library properly. +// You can use v1.New or v1.NewWithClient to initialize API client. +// The package github.com/retailcrm/mg-transport-api-client-go/examples contains some examples on how to +// use this library properly. // // Basic usage example: // From b4fa85eba0972dc794645891400d2b827e375e54 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 29 Dec 2023 11:46:18 +0300 Subject: [PATCH 11/11] more lint fixes --- .golangci.yml | 1 - v1/errors.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 02a09bc..24fc177 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -34,7 +34,6 @@ linters: - gosec - lll - makezero - - maligned - misspell - nestif - prealloc diff --git a/v1/errors.go b/v1/errors.go index 2f7dd3e..8179505 100644 --- a/v1/errors.go +++ b/v1/errors.go @@ -88,7 +88,7 @@ func AsClientError(err error) *HTTPClientError { if err == nil { return nil } - if typed, ok := err.(*HTTPClientError); ok { + if typed, ok := err.(*HTTPClientError); ok { //nolint:errorlint return typed } err = errors.Unwrap(err)