1
0
Fork 0
mirror of synced 2025-04-19 08:50:56 +00:00

Compare commits

..

No commits in common. "master" and "v1.3.17" have entirely different histories.

9 changed files with 73 additions and 423 deletions

View file

@ -1,7 +1,6 @@
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
@ -33,10 +32,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.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
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
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.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/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/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.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/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/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,7 +10,6 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/google/go-querystring/query"
@ -24,7 +23,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: strings.TrimRight(url, "/"),
URL: url,
Token: token,
httpClient: client,
}
@ -586,75 +585,6 @@ 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,23 +39,6 @@ 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"
@ -83,17 +66,14 @@ 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,
@ -101,7 +81,6 @@ func (t *MGClientTest) Test_TransportChannels() {
Quoting: ChannelFeatureBoth,
Deleting: ChannelFeatureReceive,
Max: 1,
Reaction: ChannelFeatureAny,
},
Image: ChannelSettingsFilesBase{
Creating: ChannelFeatureBoth,
@ -109,7 +88,6 @@ func (t *MGClientTest) Test_TransportChannels() {
Quoting: ChannelFeatureBoth,
Deleting: ChannelFeatureReceive,
Max: 1, // nolint:gomnd
Reaction: ChannelFeatureAny,
},
Suggestions: ChannelSettingsSuggestions{
Text: ChannelFeatureBoth,
@ -120,10 +98,6 @@ func (t *MGClientTest) Test_TransportChannels() {
SendingPolicy: SendingPolicy{
NewCustomer: ChannelFeatureSendingPolicyTemplate,
},
Reactions: Reactions{
Dictionary: []string{"👏", "😁", "🤔"},
MaxCount: 3,
},
},
CreatedAt: createdAt,
UpdatedAt: &createdAt,
@ -158,29 +132,20 @@ 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,
},
},
}
@ -221,29 +186,20 @@ 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,
},
},
}
@ -301,29 +257,20 @@ 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,
},
},
}
@ -886,100 +833,6 @@ 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,135 +1,100 @@
package v1
import (
"hash/fnv"
"runtime"
"sync"
"sync/atomic"
"time"
)
// Use double the CPU count for sharding
const shardsPerCoreMultiplier = 2
// NoopLimiter implements Limiter but doesn't limit anything.
var NoopLimiter Limiter = &noopLimiter{}
type token struct {
rps uint32
lastUse int64 // Unix timestamp in nanoseconds
rps atomic.Uint32
lastUse atomic.Value
}
// Limiter interface for rate-limiting.
// Limiter implements some form of rate limiting.
type Limiter interface {
Obtain(id string)
// Obtain the right to send a request. Should lock the execution if current goroutine needs to wait.
Obtain(string)
}
// TokensBucket implements a sharded rate limiter with fixed window and tokens.
// TokensBucket implements basic Limiter with fixed window and fixed amount of tokens per window.
type TokensBucket struct {
maxRPS uint32
unusedTokenTime int64 // in nanoseconds
tokens sync.Map
unusedTokenTime time.Duration
checkTokenTime time.Duration
shards []*tokenShard
shardCount uint32
cancel atomic.Bool
sleep sleeper
}
type tokenShard struct {
tokens map[string]*token
mu sync.Mutex
}
// NewTokensBucket creates a sharded token bucket limiter.
// NewTokensBucket constructs TokensBucket with provided parameters.
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.Nanoseconds(),
unusedTokenTime: unusedTokenTime,
checkTokenTime: checkTokenTime,
shards: shards,
shardCount: shardCount,
sleep: realSleeper{},
}
go bucket.cleanupRoutine()
runtime.SetFinalizer(bucket, destructBucket)
go bucket.deleteUnusedToken()
runtime.SetFinalizer(bucket, destructBasket)
return bucket
}
// Obtain request hit. Will throttle RPS.
func (m *TokensBucket) Obtain(id string) {
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,
}
val, ok := m.tokens.Load(id)
if !ok {
token := &token{}
token.lastUse.Store(time.Now())
token.rps.Store(1)
m.tokens.Store(id, token)
return
}
sleepTime := int64(time.Second) - (now - item.lastUse)
token := val.(*token)
sleepTime := time.Second - time.Since(token.lastUse.Load().(time.Time))
if sleepTime <= 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.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)
}
token.rps.Add(1)
}
func (m *TokensBucket) getShard(id string) *tokenShard {
hash := fnv.New32a()
_, _ = hash.Write([]byte(id))
return m.shards[hash.Sum32()%m.shardCount]
}
func (m *TokensBucket) cleanupRoutine() {
ticker := time.NewTicker(m.checkTokenTime)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if m.cancel.Load() {
return
}
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) {
func destructBasket(m *TokensBucket) {
m.cancel.Store(true)
}
func (m *TokensBucket) deleteUnusedToken() {
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)
}
return false
})
m.sleep.Sleep(m.checkTokenTime)
}
}
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,22 +24,13 @@ 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.Nanoseconds(),
unusedTokenTime: unusedTokenTime,
checkTokenTime: checkTokenTime,
shards: shards,
shardCount: shardCount,
sleep: sleeper,
}
runtime.SetFinalizer(bucket, destructBucket)
runtime.SetFinalizer(bucket, destructBasket)
return bucket
}
@ -55,14 +46,12 @@ 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("w")
tb.Obtain("a")
}
wg.Done()
}()
@ -74,15 +63,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.cleanupRoutine()
go tb.deleteUnusedToken()
tb.Obtain("a")
clock.Advance(time.Minute * 2)
item, found := tb.getShard("a").tokens["a"]
item, found := tb.tokens.Load("a")
t.Require().True(found)
t.Assert().Equal(1, int(item.rps))
t.Assert().Equal(1, int(item.(*token).rps.Load()))
tb.Obtain("a")
t.Assert().Equal(2, int(item.rps))
t.Assert().Equal(2, int(item.(*token).rps.Load()))
}
type fakeSleeper struct {

View file

@ -104,7 +104,6 @@ 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"`
@ -118,12 +117,6 @@ 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.
@ -131,7 +124,6 @@ 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.
@ -146,7 +138,6 @@ 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"`
}
@ -156,7 +147,6 @@ 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"`
@ -167,7 +157,6 @@ 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"`
}
@ -357,16 +346,6 @@ 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"`
@ -450,18 +429,15 @@ 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"`
ExternalCustomerID string `json:"external_customer_id,omitempty"`
ExternalChatID string `json:"external_chat_id,omitempty"`
ExternalMessageID string `json:"external_message_id"`
Error *MessageSentError `json:"error,omitempty"`
Async bool `json:"async"`
}
// MessageSentError type.
type MessageSentError struct {
Code ErrorType `json:"code"`
Message string `json:"message"`
ExternalCode string `json:"external_code,omitempty"`
Code ErrorType `json:"code"`
Message string `json:"message"`
}
// MessageWebhookData request data.
@ -484,20 +460,6 @@ 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"`
}

View file

@ -9,8 +9,6 @@ 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"
@ -29,11 +27,6 @@ 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 ||
@ -50,15 +43,6 @@ 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,12 +15,6 @@ 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())
@ -43,32 +37,6 @@ 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,