1
0
Fork 0
mirror of synced 2025-04-04 13:53:35 +03:00

Compare commits

...

25 commits

Author SHA1 Message Date
f36c4080ca
Merge pull request #130 from retailcrm/dependabot/go_modules/examples/telegram/golang.org/x/net-0.36.0
Bump golang.org/x/net from 0.23.0 to 0.36.0 in /examples/telegram
2025-03-20 10:37:30 +03:00
972ee86d40
Merge pull request #131 from dendd1/master
fix useless error in reactions api
2025-03-20 10:37:13 +03:00
Суханов Данила
ff93bc00ec fix reactions api 2025-03-20 10:29:54 +03:00
dependabot[bot]
fa5c37737a
Bump golang.org/x/net from 0.23.0 to 0.36.0 in /examples/telegram
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.23.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.23.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 00:32:13 +00:00
22e26be57f
Merge pull request #129 from Neur0toxine/fix-add-remove-reaction-dto
fix ReactionRequest dto
2025-03-12 17:37:13 +03:00
752c57fdb7 fix ReactionRequest dto 2025-03-12 17:33:46 +03:00
7f8f188ded
Merge pull request #128 from dendd1/master
message reactions support
2025-03-12 10:52:38 +03:00
Суханов Данила
f2bc5ae0ea change setting name 2025-03-12 10:37:50 +03:00
Суханов Данила
5da33ab761 message reactions support 2025-03-12 10:07:50 +03:00
a3da02063e
Merge pull request #126 from retailcrm/dependabot/go_modules/examples/telegram/golang.org/x/crypto-0.31.0
Bump golang.org/x/crypto from 0.21.0 to 0.31.0 in /examples/telegram
2024-12-23 11:45:09 +03:00
a4a937114d
Merge pull request #127 from RenCurs/field-webhook-sent
add new fields for struct of webhook sent response
2024-12-23 11:44:48 +03:00
Руслан
1cb93d4986 add new fields for struct of webhook sent response 2024-12-23 11:35:37 +03:00
dependabot[bot]
74e4ab94a4
Bump golang.org/x/crypto from 0.21.0 to 0.31.0 in /examples/telegram
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-11 23:55:54 +00:00
b18055eebb
Merge pull request #125 from Neur0toxine/optimize-rate-limiter
optimize rate limiter for heavier load
2024-12-06 19:13:05 +03:00
fabaf40050 fix linter 2024-12-06 19:11:34 +03:00
5fb9f0f895 optimize rate limiter for heavier load 2024-12-06 19:03:33 +03:00
0312ddcdd2
Merge pull request #124 from Neur0toxine/allow-trailing-slash-url
allow trailing slash in MG URL
2024-11-29 14:03:41 +03:00
83fa99602b allow trailing slash in MG URL 2024-11-29 13:58:12 +03:00
919fa8bc26
Merge pull request #121 from retailcrm/dependabot/go_modules/examples/telegram/golang.org/x/net-0.23.0
Bump golang.org/x/net from 0.17.0 to 0.23.0 in /examples/telegram
2024-09-04 13:26:06 +03:00
ee80215c09
Support for the sending of order and product messages 2024-07-26 14:04:23 +03:00
addfs
5a82b30be3 отправка сообщения с типами product, order 2024-07-23 18:50:21 +03:00
addfs
c5b496e586 отправка сообщения с типами product, order 2024-07-23 18:47:58 +03:00
28e6945670
secondary external chat ids for ig comment 2024-05-03 09:34:46 +03:00
Суханов Данила
63b3e26abb secondary externalchat ids for ig comment 2024-04-27 08:53:54 +03:00
dependabot[bot]
5edb649366
Bump golang.org/x/net from 0.17.0 to 0.23.0 in /examples/telegram
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.17.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.17.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 12:34:22 +00:00
9 changed files with 570 additions and 89 deletions

View file

@ -1,6 +1,7 @@
module github.com/retailcrm/mg-transport-api-client-go/examples/telegram
go 1.21.5
toolchain go1.23.7
require (
github.com/gin-gonic/gin v1.9.1
@ -32,10 +33,10 @@ require (
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.17.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -78,16 +78,16 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=

View file

@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/google/go-querystring/query"
@ -23,7 +24,7 @@ func New(url string, token string) *MgClient {
// NewWithClient initializes the MgClient with specified *http.Client.
func NewWithClient(url string, token string, client *http.Client) *MgClient {
return &MgClient{
URL: url,
URL: strings.TrimRight(url, "/"),
Token: token,
httpClient: client,
}
@ -585,6 +586,75 @@ func (c *MgClient) MessagesHistory(request SendHistoryMessageRequest) (MessagesR
return resp, status, err
}
// AddMessageReaction adds reactions to the message.
//
// Example:
//
// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6")
//
// status, err := client.AddMessageReaction(ReactionRequest{
// Channel: 305,
// Message: ReactionMessageReference{
// ExternalID: "uid_1",
// },
// Reaction: "😁",
// })
// if err != nil {
// log.Fatalf("request error: %s (%d)", err, status)
// }
//
// log.Printf("status: %d", status)
func (c *MgClient) AddMessageReaction(request ReactionRequest) (int, error) {
var outgoing = &bytes.Buffer{}
_ = json.NewEncoder(outgoing).Encode(request)
data, status, err := c.PostRequest("/messages/reaction", outgoing)
if err != nil {
return status, err
}
if status != http.StatusOK {
return status, NewAPIClientError(data)
}
return status, err
}
// DeleteMessagesReaction removes reactions to the message.
// The reaction field is optional. If it is passed, the specific reaction will be deleted.
// If it is not passed - the first found reaction from the user to the message will be deleted.
//
// Example:
//
// client := New("https://message-gateway.url", "cb8ccf05e38a47543ad8477d4999be73bff503ea6")
//
// status, err := client.DeleteMessagesReaction(ReactionRequest{
// ChannelID: 305,
// Message: ReactionMessageReference{
// ExternalID: "uid_1",
// },
// Reaction: "😁",
// })
// if err != nil {
// log.Fatalf("request error: %s (%d)", err, status)
// }
//
// log.Printf("status: %d", status)
func (c *MgClient) DeleteMessagesReaction(request ReactionRequest) (int, error) {
outgoing, _ := json.Marshal(&request)
data, status, err := c.DeleteRequest("/messages/reaction", outgoing)
if err != nil {
return status, err
}
if status != http.StatusOK {
return status, NewAPIClientError(data)
}
return status, err
}
// UpdateMessages edits existing message. Only text messages are supported.
//
// Example:

View file

@ -39,6 +39,23 @@ func (t *MGClientTest) transportURL(path string) string {
return "/api/transport/v1/" + strings.TrimLeft(path, "/")
}
func (t *MGClientTest) Test_URLWithTrailingSlash() {
c := New("https://mg-test.retailcrm.pro/", "mg_token")
c.Debug = true
defer gock.Off()
t.gock().
Get(t.transportURL("channels")).
Reply(http.StatusOK).
JSON([]ChannelListItem{{ID: 1}})
data, status, err := c.TransportChannels(Channels{Active: true})
t.Require().NoError(err)
t.Assert().Equal(http.StatusOK, status)
t.Assert().Len(data, 1)
}
func (t *MGClientTest) Test_TransportChannels() {
c := t.client()
chName := "WhatsApp Channel"
@ -66,14 +83,17 @@ func (t *MGClientTest) Test_TransportChannels() {
Quoting: ChannelFeatureBoth,
Deleting: ChannelFeatureReceive,
MaxCharsCount: 4096,
Reaction: ChannelFeatureAny,
},
Product: Product{
Creating: ChannelFeatureReceive,
Editing: ChannelFeatureReceive,
Reaction: ChannelFeatureAny,
},
Order: Order{
Creating: ChannelFeatureReceive,
Editing: ChannelFeatureReceive,
Reaction: ChannelFeatureAny,
},
File: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
@ -81,6 +101,7 @@ func (t *MGClientTest) Test_TransportChannels() {
Quoting: ChannelFeatureBoth,
Deleting: ChannelFeatureReceive,
Max: 1,
Reaction: ChannelFeatureAny,
},
Image: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
@ -88,6 +109,7 @@ func (t *MGClientTest) Test_TransportChannels() {
Quoting: ChannelFeatureBoth,
Deleting: ChannelFeatureReceive,
Max: 1, // nolint:gomnd
Reaction: ChannelFeatureAny,
},
Suggestions: ChannelSettingsSuggestions{
Text: ChannelFeatureBoth,
@ -98,6 +120,10 @@ func (t *MGClientTest) Test_TransportChannels() {
SendingPolicy: SendingPolicy{
NewCustomer: ChannelFeatureSendingPolicyTemplate,
},
Reactions: Reactions{
Dictionary: []string{"👏", "😁", "🤔"},
MaxCount: 3,
},
},
CreatedAt: createdAt,
UpdatedAt: &createdAt,
@ -132,20 +158,29 @@ func (t *MGClientTest) Test_ActivateTransportChannel() {
Quoting: ChannelFeatureReceive,
Deleting: ChannelFeatureBoth,
MaxCharsCount: 2000,
Reaction: ChannelFeatureAny,
},
Product: Product{
Creating: ChannelFeatureSend,
Deleting: ChannelFeatureSend,
Reaction: ChannelFeatureAny,
},
Order: Order{
Creating: ChannelFeatureBoth,
Deleting: ChannelFeatureSend,
Reaction: ChannelFeatureAny,
},
Image: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
Reaction: ChannelFeatureAny,
},
File: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
Reaction: ChannelFeatureAny,
},
Reactions: Reactions{
Dictionary: []string{"👏", "😁", "🤔"},
MaxCount: 3,
},
},
}
@ -186,20 +221,29 @@ func (t *MGClientTest) Test_ActivateNewTransportChannel() {
Editing: ChannelFeatureSend,
Quoting: ChannelFeatureBoth,
Deleting: ChannelFeatureSend,
Reaction: ChannelFeatureAny,
},
Product: Product{
Creating: ChannelFeatureSend,
Deleting: ChannelFeatureSend,
Reaction: ChannelFeatureAny,
},
Order: Order{
Creating: ChannelFeatureBoth,
Deleting: ChannelFeatureSend,
Reaction: ChannelFeatureAny,
},
Image: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
Reaction: ChannelFeatureAny,
},
File: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
Reaction: ChannelFeatureAny,
},
Reactions: Reactions{
Dictionary: []string{"👏", "😁", "🤔"},
MaxCount: 3,
},
},
}
@ -257,20 +301,29 @@ func (t *MGClientTest) Test_UpdateTransportChannel() {
Editing: ChannelFeatureBoth,
Quoting: ChannelFeatureBoth,
Deleting: ChannelFeatureBoth,
Reaction: ChannelFeatureAny,
},
Product: Product{
Creating: ChannelFeatureSend,
Deleting: ChannelFeatureSend,
Reaction: ChannelFeatureAny,
},
Order: Order{
Creating: ChannelFeatureBoth,
Deleting: ChannelFeatureSend,
Reaction: ChannelFeatureAny,
},
Image: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
Reaction: ChannelFeatureAny,
},
File: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
Reaction: ChannelFeatureAny,
},
Reactions: Reactions{
Dictionary: []string{"👏", "😁", "🤔"},
MaxCount: 3,
},
},
}
@ -605,6 +658,134 @@ func (t *MGClientTest) Test_ImageMessages() {
t.Assert().Equal(1, data.MessageID)
}
func (t *MGClientTest) Test_ProductMessages() {
c := t.client()
snd := SendData{
Message: Message{
ExternalID: "external_id",
Type: MsgTypeProduct,
Product: &MessageDataProduct{
ID: 2,
Name: "Product name",
Article: "Product article",
Url: "https://example.loca/product/1",
Img: "https://example.loca/product/1/img",
Cost: &MessageDataOrderCost{
Value: 100,
Currency: "USD",
},
Unit: "pcs",
},
},
Originator: OriginatorCustomer,
Customer: Customer{
ExternalID: "6",
Nickname: "octopus",
Firstname: "Joe",
Utm: &Utm{
Source: "test-source",
Term: "",
},
},
Channel: 1,
ExternalChatID: "24798237492374",
}
defer gock.Off()
t.gock().
Post(t.transportURL("messages")).
Filter(func(request *http.Request) bool {
data, err := ioutil.ReadAll(request.Body)
if err != nil {
return false
}
request.Body = ioutil.NopCloser(bytes.NewReader(data))
var snd SendData
t.Require().NoError(json.Unmarshal(data, &snd))
return t.Assert().Equal(uint64(2), snd.Message.Product.ID)
}).
Reply(http.StatusOK).
JSON(
MessagesResponse{
MessageID: 1,
Time: time.Now(),
},
)
data, status, err := c.Messages(snd)
t.Require().NoError(err)
t.Assert().Equal(http.StatusOK, status)
t.Assert().NotEmpty(data.Time.String())
t.Assert().Equal(1, data.MessageID)
}
func (t *MGClientTest) Test_OrderMessages() {
c := t.client()
snd := SendData{
Message: Message{
ExternalID: "external_id",
Type: MsgTypeOrder,
Order: &MessageDataOrder{
Number: "C1234",
ExternalID: 123,
Date: time.Now().String(),
Cost: &MessageDataOrderCost{
Value: 100,
Currency: "USD",
},
Discount: nil,
Status: nil,
Delivery: nil,
Payments: nil,
Items: nil,
},
},
Originator: OriginatorCustomer,
Customer: Customer{
ExternalID: "6",
Nickname: "octopus",
Firstname: "Joe",
Utm: &Utm{
Source: "test-source",
Term: "",
},
},
Channel: 1,
ExternalChatID: "24798237492374",
}
defer gock.Off()
t.gock().
Post(t.transportURL("messages")).
Filter(func(request *http.Request) bool {
data, err := ioutil.ReadAll(request.Body)
if err != nil {
return false
}
request.Body = ioutil.NopCloser(bytes.NewReader(data))
var snd SendData
t.Require().NoError(json.Unmarshal(data, &snd))
return t.Assert().Equal(int64(123), snd.Message.Order.ExternalID)
}).
Reply(http.StatusOK).
JSON(
MessagesResponse{
MessageID: 1,
Time: time.Now(),
},
)
data, status, err := c.Messages(snd)
t.Require().NoError(err)
t.Assert().Equal(http.StatusOK, status)
t.Assert().NotEmpty(data.Time.String())
t.Assert().Equal(1, data.MessageID)
}
func (t *MGClientTest) Test_UpdateMessages() {
c := t.client()
@ -705,6 +886,100 @@ func (t *MGClientTest) Test_MessagesHistory() {
t.Assert().Equal(1, data.MessageID)
}
func (t *MGClientTest) Test_AddMessageReaction() {
c := t.client()
snd := ReactionRequest{
Channel: 1,
Message: ReactionMessageReference{
ExternalID: "external_1",
},
Reaction: "😁",
}
defer gock.Off()
t.gock().
Post(t.transportURL("messages/reaction")).
Reply(http.StatusOK).
JSON(``)
status, err := c.AddMessageReaction(snd)
t.Require().NoError(err)
t.Assert().Equal(http.StatusOK, status)
t.Assert().Empty(gock.GetUnmatchedRequests())
}
func (t *MGClientTest) Test_AddMessageReaction_error() {
c := t.client()
snd := ReactionRequest{
Channel: 1,
Message: ReactionMessageReference{
ExternalID: "external_1",
},
Reaction: "😁",
}
defer gock.Off()
t.gock().
Post(t.transportURL("messages/reaction")).
Reply(http.StatusBadRequest).
JSON(`{"errors": ["problems"]}`)
status, err := c.AddMessageReaction(snd)
t.Assert().Equal(http.StatusBadRequest, status)
t.Assert().Error(err)
t.Assert().Equal("problems", err.Error())
t.Assert().Empty(gock.GetUnmatchedRequests())
}
func (t *MGClientTest) Test_DeleteMessagesReaction() {
c := t.client()
snd := ReactionRequest{
Channel: 1,
Message: ReactionMessageReference{
ExternalID: "external_1",
},
Reaction: "😁",
}
defer gock.Off()
t.gock().
Delete(t.transportURL("messages/reaction")).
Reply(http.StatusOK).
JSON(``)
status, err := c.DeleteMessagesReaction(snd)
t.Require().NoError(err)
t.Assert().Equal(http.StatusOK, status)
t.Assert().Empty(gock.GetUnmatchedRequests())
}
func (t *MGClientTest) Test_DeleteMessagesReaction_error() {
c := t.client()
snd := ReactionRequest{
Channel: 1,
Message: ReactionMessageReference{
ExternalID: "external_1",
},
Reaction: "😁",
}
defer gock.Off()
t.gock().
Delete(t.transportURL("messages/reaction")).
Reply(http.StatusBadRequest).
JSON(`{"errors": ["problems"]}`)
status, err := c.DeleteMessagesReaction(snd)
t.Assert().Equal(http.StatusBadRequest, status)
t.Assert().Error(err)
t.Assert().Equal("problems", err.Error())
t.Assert().Empty(gock.GetUnmatchedRequests())
}
func (t *MGClientTest) Test_MarkMessageReadAndDelete() {
c := t.client()

View file

@ -1,100 +1,135 @@
package v1
import (
"hash/fnv"
"runtime"
"sync"
"sync/atomic"
"time"
)
// NoopLimiter implements Limiter but doesn't limit anything.
// Use double the CPU count for sharding
const shardsPerCoreMultiplier = 2
var NoopLimiter Limiter = &noopLimiter{}
type token struct {
rps atomic.Uint32
lastUse atomic.Value
rps uint32
lastUse int64 // Unix timestamp in nanoseconds
}
// Limiter implements some form of rate limiting.
// Limiter interface for rate-limiting.
type Limiter interface {
// Obtain the right to send a request. Should lock the execution if current goroutine needs to wait.
Obtain(string)
Obtain(id string)
}
// TokensBucket implements basic Limiter with fixed window and fixed amount of tokens per window.
// TokensBucket implements a sharded rate limiter with fixed window and tokens.
type TokensBucket struct {
maxRPS uint32
tokens sync.Map
unusedTokenTime time.Duration
unusedTokenTime int64 // in nanoseconds
checkTokenTime time.Duration
shards []*tokenShard
shardCount uint32
cancel atomic.Bool
sleep sleeper
}
// NewTokensBucket constructs TokensBucket with provided parameters.
type tokenShard struct {
tokens map[string]*token
mu sync.Mutex
}
// NewTokensBucket creates a sharded token bucket limiter.
func NewTokensBucket(maxRPS uint32, unusedTokenTime, checkTokenTime time.Duration) Limiter {
shardCount := uint32(runtime.NumCPU() * shardsPerCoreMultiplier)
shards := make([]*tokenShard, shardCount)
for i := range shards {
shards[i] = &tokenShard{tokens: make(map[string]*token)}
}
bucket := &TokensBucket{
maxRPS: maxRPS,
unusedTokenTime: unusedTokenTime,
unusedTokenTime: unusedTokenTime.Nanoseconds(),
checkTokenTime: checkTokenTime,
shards: shards,
shardCount: shardCount,
sleep: realSleeper{},
}
go bucket.deleteUnusedToken()
runtime.SetFinalizer(bucket, destructBasket)
go bucket.cleanupRoutine()
runtime.SetFinalizer(bucket, destructBucket)
return bucket
}
// Obtain request hit. Will throttle RPS.
func (m *TokensBucket) Obtain(id string) {
val, ok := m.tokens.Load(id)
if !ok {
token := &token{}
token.lastUse.Store(time.Now())
token.rps.Store(1)
m.tokens.Store(id, token)
shard := m.getShard(id)
shard.mu.Lock()
defer shard.mu.Unlock()
item, exists := shard.tokens[id]
now := time.Now().UnixNano()
if !exists {
shard.tokens[id] = &token{
rps: 1,
lastUse: now,
}
return
}
token := val.(*token)
sleepTime := time.Second - time.Since(token.lastUse.Load().(time.Time))
sleepTime := int64(time.Second) - (now - item.lastUse)
if sleepTime <= 0 {
token.lastUse.Store(time.Now())
token.rps.Store(0)
} else if token.rps.Load() >= m.maxRPS {
m.sleep.Sleep(sleepTime)
token.lastUse.Store(time.Now())
token.rps.Store(0)
item.lastUse = now
atomic.StoreUint32(&item.rps, 1)
} else if atomic.LoadUint32(&item.rps) >= m.maxRPS {
m.sleep.Sleep(time.Duration(sleepTime))
item.lastUse = time.Now().UnixNano()
atomic.StoreUint32(&item.rps, 1)
} else {
atomic.AddUint32(&item.rps, 1)
}
token.rps.Add(1)
}
func destructBasket(m *TokensBucket) {
m.cancel.Store(true)
func (m *TokensBucket) getShard(id string) *tokenShard {
hash := fnv.New32a()
_, _ = hash.Write([]byte(id))
return m.shards[hash.Sum32()%m.shardCount]
}
func (m *TokensBucket) deleteUnusedToken() {
func (m *TokensBucket) cleanupRoutine() {
ticker := time.NewTicker(m.checkTokenTime)
defer ticker.Stop()
for {
if m.cancel.Load() {
return
}
m.tokens.Range(func(key, value any) bool {
id, token := key.(string), value.(*token)
if time.Since(token.lastUse.Load().(time.Time)) >= m.unusedTokenTime {
m.tokens.Delete(id)
select {
case <-ticker.C:
if m.cancel.Load() {
return
}
return false
})
m.sleep.Sleep(m.checkTokenTime)
now := time.Now().UnixNano()
for _, shard := range m.shards {
shard.mu.Lock()
for id, token := range shard.tokens {
if now-token.lastUse >= m.unusedTokenTime {
delete(shard.tokens, id)
}
}
shard.mu.Unlock()
}
}
}
}
func destructBucket(m *TokensBucket) {
m.cancel.Store(true)
}
type noopLimiter struct{}
func (l *noopLimiter) Obtain(string) {}
// sleeper sleeps. This thing is necessary for tests.
type sleeper interface {
Sleep(time.Duration)
}

View file

@ -24,13 +24,22 @@ func (t *TokensBucketTest) Test_NewTokensBucket() {
func (t *TokensBucketTest) new(
maxRPS uint32, unusedTokenTime, checkTokenTime time.Duration, sleeper sleeper) *TokensBucket {
shardCount := uint32(runtime.NumCPU() * 2) // Use double the CPU count for sharding
shards := make([]*tokenShard, shardCount)
for i := range shards {
shards[i] = &tokenShard{tokens: make(map[string]*token)}
}
bucket := &TokensBucket{
maxRPS: maxRPS,
unusedTokenTime: unusedTokenTime,
unusedTokenTime: unusedTokenTime.Nanoseconds(),
checkTokenTime: checkTokenTime,
shards: shards,
shardCount: shardCount,
sleep: sleeper,
}
runtime.SetFinalizer(bucket, destructBasket)
runtime.SetFinalizer(bucket, destructBucket)
return bucket
}
@ -46,12 +55,14 @@ func (t *TokensBucketTest) Test_Obtain_NoThrottle() {
func (t *TokensBucketTest) Test_Obtain_Sleep() {
clock := &fakeSleeper{}
tb := t.new(100, time.Hour, time.Minute, clock)
_, exists := tb.getShard("w").tokens["w"]
t.Require().False(exists)
var wg sync.WaitGroup
wg.Add(1)
go func() {
for i := 0; i < 301; i++ {
tb.Obtain("a")
tb.Obtain("w")
}
wg.Done()
}()
@ -63,15 +74,15 @@ func (t *TokensBucketTest) Test_Obtain_Sleep() {
func (t *TokensBucketTest) Test_Obtain_AddRPS() {
clock := clockwork.NewFakeClock()
tb := t.new(100, time.Hour, time.Minute, clock)
go tb.deleteUnusedToken()
go tb.cleanupRoutine()
tb.Obtain("a")
clock.Advance(time.Minute * 2)
item, found := tb.tokens.Load("a")
item, found := tb.getShard("a").tokens["a"]
t.Require().True(found)
t.Assert().Equal(1, int(item.(*token).rps.Load()))
t.Assert().Equal(1, int(item.rps))
tb.Obtain("a")
t.Assert().Equal(2, int(item.(*token).rps.Load()))
t.Assert().Equal(2, int(item.rps))
}
type fakeSleeper struct {

View file

@ -104,6 +104,7 @@ type ChannelSettings struct {
Order Order `json:"order"`
File ChannelSettingsFilesBase `json:"file"`
Image ChannelSettingsFilesBase `json:"image"`
Reactions Reactions `json:"reactions"`
CustomerExternalID string `json:"customer_external_id,omitempty"`
SendingPolicy SendingPolicy `json:"sending_policy,omitempty"`
Suggestions ChannelSettingsSuggestions `json:"suggestions,omitempty"`
@ -117,6 +118,12 @@ type Product struct {
Creating string `json:"creating,omitempty"`
Editing string `json:"editing,omitempty"`
Deleting string `json:"deleting,omitempty"`
Reaction string `json:"reaction,omitempty"`
}
type Reactions struct {
Dictionary []string `json:"dictionary,omitempty"`
MaxCount uint16 `json:"max_count,omitempty"`
}
// Order type.
@ -124,6 +131,7 @@ type Order struct {
Creating string `json:"creating,omitempty"`
Editing string `json:"editing,omitempty"`
Deleting string `json:"deleting,omitempty"`
Reaction string `json:"reaction,omitempty"`
}
// Status struct.
@ -138,6 +146,7 @@ type ChannelSettingsText struct {
Editing string `json:"editing,omitempty"`
Quoting string `json:"quoting,omitempty"`
Deleting string `json:"deleting,omitempty"`
Reaction string `json:"reaction,omitempty"`
MaxCharsCount uint16 `json:"max_chars_count,omitempty"`
}
@ -147,6 +156,7 @@ type ChannelSettingsFilesBase struct {
Editing string `json:"editing,omitempty"`
Quoting string `json:"quoting,omitempty"`
Deleting string `json:"deleting,omitempty"`
Reaction string `json:"reaction,omitempty"`
Max uint64 `json:"max_items_count,omitempty"`
NoteMaxCharsCount *uint16 `json:"note_max_chars_count,omitempty"`
MaxItemSize *uint64 `json:"max_item_size,omitempty"`
@ -157,6 +167,7 @@ type ChannelSettingsAudio struct {
Creating string `json:"creating,omitempty"`
Quoting string `json:"quoting,omitempty"`
Deleting string `json:"deleting,omitempty"`
Reaction string `json:"reaction,omitempty"`
MaxItemsCount uint64 `json:"max_items_count,omitempty"`
MaxItemSize *uint64 `json:"max_item_size,omitempty"`
}
@ -306,12 +317,14 @@ type Utm struct {
// Message struct.
type Message struct {
ExternalID string `json:"external_id"`
Type string `json:"type,omitempty"`
Text string `json:"text,omitempty"`
Note string `json:"note,omitempty"`
Items []Item `json:"items,omitempty"`
PageLink string `json:"page_link,omitempty"`
ExternalID string `json:"external_id"`
Type string `json:"type,omitempty"`
Text string `json:"text,omitempty"`
Note string `json:"note,omitempty"`
Items []Item `json:"items,omitempty"`
PageLink string `json:"page_link,omitempty"`
Product *MessageDataProduct `json:"product,omitempty"`
Order *MessageDataOrder `json:"order,omitempty"`
}
// SendMessage struct.
@ -344,6 +357,16 @@ type SendHistoryMessageRequest struct {
ReplyDeadline *time.Time `json:"reply_deadline,omitempty"`
}
type ReactionRequest struct {
Channel uint64 `json:"channel"`
Message ReactionMessageReference `json:"message"`
Reaction string `json:"reaction,omitempty"`
}
type ReactionMessageReference struct {
ExternalID string `json:"external_id"`
}
type SendMessageRequestMessage struct {
Type string `json:"type"`
ExternalID string `json:"external_id,omitempty"`
@ -356,13 +379,14 @@ type SendMessageRequestMessage struct {
// SendData struct.
type SendData struct {
Message Message `json:"message"`
Originator Originator `json:"originator,omitempty"`
Customer Customer `json:"customer"`
Channel uint64 `json:"channel"`
ExternalChatID string `json:"external_chat_id"`
Quote *SendMessageRequestQuote `json:"quote,omitempty"`
ReplyDeadline *time.Time `json:"reply_deadline,omitempty"`
Message Message `json:"message"`
Originator Originator `json:"originator,omitempty"`
Customer Customer `json:"customer"`
Channel uint64 `json:"channel"`
ExternalChatID string `json:"external_chat_id"`
Quote *SendMessageRequestQuote `json:"quote,omitempty"`
ReplyDeadline *time.Time `json:"reply_deadline,omitempty"`
SecondaryExternalChatIDs []string `json:"secondary_external_chat_ids,omitempty"`
}
// Item struct.
@ -426,9 +450,11 @@ type MessagesResponse struct {
// WebhookMessageSentResponse type
// Consider using this structure while processing webhook request.
type WebhookMessageSentResponse struct {
ExternalMessageID string `json:"external_message_id"`
Error *MessageSentError `json:"error,omitempty"`
Async bool `json:"async"`
ExternalMessageID string `json:"external_message_id"`
Error *MessageSentError `json:"error,omitempty"`
Async bool `json:"async"`
ExternalCustomerID string `json:"external_customer_id,omitempty"`
ExternalChatID string `json:"external_chat_id,omitempty"`
}
// MessageSentError type.
@ -457,6 +483,20 @@ type MessageWebhookData struct {
InAppID int32 `json:"in_app_id,omitempty"`
}
type ReactionWebhookData struct {
ExternalUserID string `json:"external_user_id"`
ExternalChatID string `json:"external_chat_id"`
ChannelID uint64 `json:"channel_id"`
ExternalMessageID string `json:"external_message_id"`
NewReaction string `json:"new_reaction,omitempty"`
OldReaction string `json:"old_reaction,omitempty"`
AllReactions []ReactionInfo `json:"all_reactions,omitempty"`
}
type ReactionInfo struct {
Reaction string `json:"reaction"`
}
type Attachments struct {
Suggestions []Suggestion `json:"suggestions,omitempty"`
}
@ -545,15 +585,16 @@ type MessageDataProduct struct {
// MessageDataOrder order data from webhook.
type MessageDataOrder struct {
Number string `json:"number"`
Url string `json:"url,omitempty"`
Date string `json:"date,omitempty"`
Cost *MessageDataOrderCost `json:"cost,omitempty"`
Discount *MessageDataOrderCost `json:"discount,omitempty"`
Status *MessageDataOrderStatus `json:"status,omitempty"`
Delivery *MessageDataOrderDelivery `json:"delivery"`
Payments []MessageDataOrderPayment `json:"payments"`
Items []MessageDataOrderItem `json:"items,omitempty"`
ExternalID int64 `json:"external_id"`
Number string `json:"number"`
URL string `json:"url,omitempty"`
Date string `json:"date,omitempty"`
Cost *MessageDataOrderCost `json:"cost,omitempty"`
Discount *MessageDataOrderCost `json:"discount,omitempty"`
Status *MessageDataOrderStatus `json:"status,omitempty"`
Delivery *MessageDataOrderDelivery `json:"delivery"`
Payments []MessageDataOrderPayment `json:"payments"`
Items []MessageDataOrderItem `json:"items,omitempty"`
}
// MessageDataOrderStatus type.

View file

@ -9,6 +9,8 @@ const (
MessageUpdateWebhookType WebhookType = "message_updated"
MessageDeleteWebhookType WebhookType = "message_deleted"
MessageReadWebhookType WebhookType = "message_read"
ReactionAddWebhookType WebhookType = "reaction_add"
ReactionDeleteWebhookType WebhookType = "reaction_delete"
TemplateCreateWebhookType WebhookType = "template_create"
TemplateUpdateWebhookType WebhookType = "template_update"
TemplateDeleteWebhookType WebhookType = "template_delete"
@ -27,6 +29,11 @@ func (w WebhookRequest) IsMessageWebhook() bool {
w.Type == MessageSendWebhookType || w.Type == MessageUpdateWebhookType
}
// IsReactionWebhook returns true if current webhook contains data related to chat reactions.
func (w WebhookRequest) IsReactionWebhook() bool {
return w.Type == ReactionAddWebhookType || w.Type == ReactionDeleteWebhookType
}
// IsTemplateWebhook returns true if current webhook contains data related to the templates changes.
func (w WebhookRequest) IsTemplateWebhook() bool {
return w.Type == TemplateCreateWebhookType ||
@ -43,6 +50,15 @@ func (w WebhookRequest) MessageWebhookData() (wd MessageWebhookData) {
return
}
// ReactionWebhookData returns the reaction data from webhook contents.
//
// Note: this call will not fail even if underlying data is not related to the reactions.
// Use IsReactionWebhook to mitigate this.
func (w WebhookRequest) ReactionWebhookData() (wd ReactionWebhookData) {
_ = 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.
//

View file

@ -15,6 +15,12 @@ func TestWebhookRequest_IsMessageWebhook(t *testing.T) {
assert.False(t, WebhookRequest{}.IsMessageWebhook())
}
func TestWebhookRequest_IsReactionWebhook(t *testing.T) {
assert.True(t, WebhookRequest{Type: ReactionAddWebhookType}.IsReactionWebhook())
assert.True(t, WebhookRequest{Type: ReactionDeleteWebhookType}.IsReactionWebhook())
assert.False(t, WebhookRequest{}.IsReactionWebhook())
}
func TestWebhookRequest_IsTemplateWebhook(t *testing.T) {
assert.True(t, WebhookRequest{Type: TemplateCreateWebhookType}.IsTemplateWebhook())
assert.True(t, WebhookRequest{Type: TemplateUpdateWebhookType}.IsTemplateWebhook())
@ -37,6 +43,32 @@ func TestWebhookData_MessageWebhookData(t *testing.T) {
assert.Equal(t, "test", wh.Content)
}
func TestWebhookData_ReactionWebhookData(t *testing.T) {
wh := WebhookRequest{
Type: ReactionAddWebhookType,
Data: mustMarshalJSON(ReactionWebhookData{
ExternalUserID: "1",
ExternalChatID: "1",
ChannelID: 1,
ExternalMessageID: "1",
NewReaction: "👍",
OldReaction: "🤔",
AllReactions: []ReactionInfo{
{
Reaction: "👏",
},
{
Reaction: "😱",
},
},
}),
}.ReactionWebhookData()
assert.Equal(t, "👍", wh.NewReaction)
assert.Equal(t, "🤔", wh.OldReaction)
assert.Equal(t, "👏", wh.AllReactions[0].Reaction)
assert.Equal(t, "😱", wh.AllReactions[1].Reaction)
}
func TestWebhookData_TemplateCreateWebhookData(t *testing.T) {
wh := WebhookRequest{
Type: TemplateCreateWebhookType,