From bea301dd062128bd84d077fc4e741b2caf0ccdfc Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Sat, 2 Nov 2019 23:50:16 +0700 Subject: [PATCH 01/19] add token checking expiration checking --- jwt.go | 20 ++++++++++++++++++++ smooch.go | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/jwt.go b/jwt.go index f8a33a8..79f1b44 100644 --- a/jwt.go +++ b/jwt.go @@ -16,3 +16,23 @@ func GenerateJWT(scope string, keyID string, secret string) (string, error) { return token.SignedString([]byte(secret)) } + +// isJWTExpired will check whether Smooch JWT is expired or not. +func isJWTExpired(jwtToken string, secret string) (bool, error) { + _, err := jwt.ParseWithClaims(jwtToken, jwt.MapClaims{}, func(t *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + + if err == nil { + return false, nil + } + + switch err.(type) { + case *jwt.ValidationError: + vErr := err.(*jwt.ValidationError) + if vErr.Errors == jwt.ValidationErrorExpired { + return true, nil + } + } + return false, err +} diff --git a/smooch.go b/smooch.go index f531871..5dc224c 100644 --- a/smooch.go +++ b/smooch.go @@ -17,6 +17,8 @@ import ( var ( ErrUserIDEmpty = errors.New("user id is empty") + ErrKeyIDEmpty = errors.New("key id is empty") + ErrSecretEmpty = errors.New("secret is empty") ErrMessageNil = errors.New("message is nil") ErrMessageRoleEmpty = errors.New("message.Role is empty") ErrMessageTypeEmpty = errors.New("message.Type is empty") @@ -52,6 +54,7 @@ type WebhookEventHandler func(payload *Payload) type Client interface { Handler() http.Handler + IsJWTExpired() (bool, error) AddWebhookEventHandler(handler WebhookEventHandler) Send(userID string, message *Message) (*ResponsePayload, error) VerifyRequest(r *http.Request) bool @@ -63,6 +66,8 @@ type Client interface { type smoochClient struct { mux *http.ServeMux appID string + keyID string + secret string jwtToken string verifySecret string logger Logger @@ -72,6 +77,14 @@ type smoochClient struct { } func New(o Options) (*smoochClient, error) { + if o.KeyID == "" { + return nil, ErrKeyIDEmpty + } + + if o.Secret == "" { + return nil, ErrSecretEmpty + } + if o.VerifySecret == "" { return nil, ErrVerifySecretEmpty } @@ -109,6 +122,8 @@ func New(o Options) (*smoochClient, error) { sc := &smoochClient{ mux: o.Mux, appID: o.AppID, + keyID: o.KeyID, + secret: o.Secret, verifySecret: o.VerifySecret, logger: o.Logger, region: region, @@ -124,6 +139,11 @@ func (sc *smoochClient) Handler() http.Handler { return sc.mux } +// IsJWTExpired will check whether Smooch JWT is expired or not. +func (sc *smoochClient) IsJWTExpired() (bool, error) { + return isJWTExpired(sc.jwtToken, sc.secret) +} + func (sc *smoochClient) AddWebhookEventHandler(handler WebhookEventHandler) { sc.webhookEventHandlers = append(sc.webhookEventHandlers, handler) } From 0f402017863a33fa53ed1ce624c54d0327e18fc6 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Sun, 3 Nov 2019 00:01:11 +0700 Subject: [PATCH 02/19] add RenewToken() --- smooch.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/smooch.go b/smooch.go index 5dc224c..4fe2276 100644 --- a/smooch.go +++ b/smooch.go @@ -13,6 +13,7 @@ import ( "os" "path" "strings" + "sync" ) var ( @@ -55,6 +56,7 @@ type WebhookEventHandler func(payload *Payload) type Client interface { Handler() http.Handler IsJWTExpired() (bool, error) + RenewToken() (string, error) AddWebhookEventHandler(handler WebhookEventHandler) Send(userID string, message *Message) (*ResponsePayload, error) VerifyRequest(r *http.Request) bool @@ -74,6 +76,7 @@ type smoochClient struct { region string webhookEventHandlers []WebhookEventHandler httpClient *http.Client + mtx sync.Mutex } func New(o Options) (*smoochClient, error) { @@ -144,6 +147,20 @@ func (sc *smoochClient) IsJWTExpired() (bool, error) { return isJWTExpired(sc.jwtToken, sc.secret) } +// RenewToken will generate new Smooch JWT token. +func (sc *smoochClient) RenewToken() (string, error) { + sc.mtx.Lock() + defer sc.mtx.Unlock() + + jwtToken, err := GenerateJWT("app", sc.keyID, sc.secret) + if err != nil { + return "", err + } + + sc.jwtToken = jwtToken + return jwtToken, nil +} + func (sc *smoochClient) AddWebhookEventHandler(handler WebhookEventHandler) { sc.webhookEventHandlers = append(sc.webhookEventHandlers, handler) } From fa7d02dc5156193874613e6e6d8d12f67031ce6d Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Tue, 5 Nov 2019 19:08:33 +0700 Subject: [PATCH 03/19] update readme --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6004751..2c5b655 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ This is a Go library for making bots with Smooch service. +_**Note** : This a modified version version of [EddyTravels/smooch](https://github.com/EddyTravels/smooch) library with additional features. Please refer to the original repo for the original features._ + +## Additional Feature + +- Redis support as a centralized storage to store JWT token for supporting autoscaling environment. + ## Tips Smooch documentation: https://docs.smooch.io/rest/ @@ -9,7 +15,7 @@ Smooch documentation: https://docs.smooch.io/rest/ ## Installing ``` -$ go get -u github.com/EddyTravels/smooch +$ go get -u github.com/kitabisa/smooch ``` ## Example @@ -18,7 +24,7 @@ $ go get -u github.com/EddyTravels/smooch import ( "os" - "github.com/EddyTravels/smooch" + "github.com/kitabisa/smooch" ) func main() { From 7460f980742b3a65b14aade2562c763c4790de1e Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Fri, 8 Nov 2019 11:01:22 +0700 Subject: [PATCH 04/19] add redis storage --- README.md | 4 +++- go.mod | 6 +++++- go.sum | 5 +++++ jwt.go | 5 +++++ smooch.go | 44 ++++++++++++++++++++++++++++++++++++++++---- storage/redis.go | 40 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 storage/redis.go diff --git a/README.md b/README.md index 2c5b655..4fd0bbf 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ _**Note** : This a modified version version of [EddyTravels/smooch](https://gith ## Additional Feature -- Redis support as a centralized storage to store JWT token for supporting autoscaling environment. +- Token expiration & its checking. +- Renew token functionality whenever token is expired. +- Redis support as a centralized storage to store JWT token for supporting autoscaling environment. Use redigo as redis library. ## Tips diff --git a/go.mod b/go.mod index 6cd34a4..f02ee01 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,10 @@ -module github.com/EddyTravels/smooch +module github.com/kitabisa/smooch require ( + github.com/EddyTravels/smooch v0.1.0 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gomodule/redigo v2.0.0+incompatible github.com/stretchr/testify v1.3.0 ) + +go 1.13 diff --git a/go.sum b/go.sum index d7b73c7..560e037 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,12 @@ +github.com/EddyTravels/smooch v0.1.0 h1:dbG4EK3otBtqL3JyGhJ6PVLMh42DI/M5pIFiRP4ATAo= +github.com/EddyTravels/smooch v0.1.0/go.mod h1:KIl3bCuRqlVyuIUVX5MPeQYexUbMcp+tU7UOZIeD7zc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/kitabisa/smooch v0.1.0 h1:dS+ouObVdoNFVZWMIqULMr/VbQoYhsBuSUdmp0VjbcM= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/jwt.go b/jwt.go index 79f1b44..37ff37c 100644 --- a/jwt.go +++ b/jwt.go @@ -17,6 +17,11 @@ func GenerateJWT(scope string, keyID string, secret string) (string, error) { return token.SignedString([]byte(secret)) } +// getJWTExpiration will get jwt expiration time +func getJWTExpiration(jwtToken string, secret string) (int64, error) { + // TODO .... +} + // isJWTExpired will check whether Smooch JWT is expired or not. func isJWTExpired(jwtToken string, secret string) (bool, error) { _, err := jwt.ParseWithClaims(jwtToken, jwt.MapClaims{}, func(t *jwt.Token) (interface{}, error) { diff --git a/smooch.go b/smooch.go index 4fe2276..1e494a1 100644 --- a/smooch.go +++ b/smooch.go @@ -14,12 +14,16 @@ import ( "path" "strings" "sync" + + "github.com/gomodule/redigo/redis" + "github.com/kitabisa/smooch/storage" ) var ( ErrUserIDEmpty = errors.New("user id is empty") ErrKeyIDEmpty = errors.New("key id is empty") ErrSecretEmpty = errors.New("secret is empty") + ErrRedisNil = errors.New("redis pool is nil") ErrMessageNil = errors.New("message is nil") ErrMessageRoleEmpty = errors.New("message.Role is empty") ErrMessageTypeEmpty = errors.New("message.Type is empty") @@ -49,6 +53,7 @@ type Options struct { Logger Logger Region string HttpClient *http.Client + RedisPool *redis.Pool } type WebhookEventHandler func(payload *Payload) @@ -70,13 +75,13 @@ type smoochClient struct { appID string keyID string secret string - jwtToken string verifySecret string logger Logger region string webhookEventHandlers []WebhookEventHandler httpClient *http.Client mtx sync.Mutex + redisPool *redis.Pool } func New(o Options) (*smoochClient, error) { @@ -92,6 +97,10 @@ func New(o Options) (*smoochClient, error) { return nil, ErrVerifySecretEmpty } + if o.RedisPool == nil { + return nil, ErrRedisNil + } + if o.Mux == nil { o.Mux = http.NewServeMux() } @@ -131,7 +140,18 @@ func New(o Options) (*smoochClient, error) { logger: o.Logger, region: region, httpClient: o.HttpClient, - jwtToken: jwtToken, + redisPool: o.RedisPool, + } + + // save token to redis + jwtExpiration, err := getJWTExpiration(jwtToken, sc.secret) + if err != nil { + return nil, err + } + + err = storage.SaveTokenToRedis(sc.redisPool, jwtToken, jwtExpiration) + if err != nil { + return nil, err } sc.mux.HandleFunc(o.WebhookURL, sc.handle) @@ -144,7 +164,14 @@ func (sc *smoochClient) Handler() http.Handler { // IsJWTExpired will check whether Smooch JWT is expired or not. func (sc *smoochClient) IsJWTExpired() (bool, error) { - return isJWTExpired(sc.jwtToken, sc.secret) + jwtToken, err := storage.GetTokenFromRedis(sc.redisPool) + if err != nil { + if err == redis.ErrNil { + return true, nil + } + return "", err + } + return isJWTExpired(jwtToken, sc.secret) } // RenewToken will generate new Smooch JWT token. @@ -157,7 +184,16 @@ func (sc *smoochClient) RenewToken() (string, error) { return "", err } - sc.jwtToken = jwtToken + jwtExpiration := getJWTExpiration(jwtToken, sc.secret) + if err != nil { + return "", err + } + + err = storage.SaveTokenToRedis(sc.redisPool, jwtToken, jwtExpiration) + if err != nil { + return "", err + } + return jwtToken, nil } diff --git a/storage/redis.go b/storage/redis.go new file mode 100644 index 0000000..da65172 --- /dev/null +++ b/storage/redis.go @@ -0,0 +1,40 @@ +package storage + +import ( + "github.com/gomodule/redigo/redis" +) + +// RedisStorage defines struct property for redis storage +type RedisStorage struct { + pool *redis.Pool + jwtKey string +} + +// NewRedisStorage initializes new instance of redis storage +func NewRedisStorage(p *redis.Pool) *RedisStorage { + return &RedisStorage{ + pool: p, + jwtKey: "smooch-jwt-token", + } +} + +// SaveTokenToRedis will save jwt token to redis +func (rs *RedisStorage) SaveTokenToRedis(token string, ttl int64) error { + conn := rs.pool.Get() + defer conn.Close() + + _, err := conn.Do("SETEX", rs.jwtKey, ttl, token) + return err +} + +// GetTokenFromRedis will retrieve jwt token from redis +func (rs *RedisStorage) GetTokenFromRedis() (string, error) { + conn := rs.pool.Get() + defer conn.Close() + + val, err := redis.String(conn.Do("GET", rs.jwtKey)) + if err != nil { + return "", err + } + return val, nil +} From 803fa1e2aba33e90d4dc2b576a4e61b8827ac540 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Fri, 8 Nov 2019 16:15:45 +0700 Subject: [PATCH 05/19] improve redis storage & add getJWTExpiration() --- README.md | 1 + jwt.go | 18 +++++++++++++++++- smooch.go | 39 ++++++++++++++++++--------------------- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 4fd0bbf..e59bbe5 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ func main() { KeyID: os.Getenv("SMOOCH_KEY_ID"), Secret: os.Getenv("SMOOCH_SECRET"), VerifySecret: os.Getenv("SMOOCH_VERIFY_SECRET"), + RedisPool: redisPool, }) if err != nil { diff --git a/jwt.go b/jwt.go index 37ff37c..72076c3 100644 --- a/jwt.go +++ b/jwt.go @@ -1,12 +1,18 @@ package smooch import ( + "time" + jwt "github.com/dgrijalva/jwt-go" ) +// JWTExpiration defines how many seconds jwt token is valid +const JWTExpiration = 3600 + func GenerateJWT(scope string, keyID string, secret string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "scope": scope, + "exp": JWTExpiration, }) token.Header = map[string]interface{}{ "alg": "HS256", @@ -19,7 +25,17 @@ func GenerateJWT(scope string, keyID string, secret string) (string, error) { // getJWTExpiration will get jwt expiration time func getJWTExpiration(jwtToken string, secret string) (int64, error) { - // TODO .... + claims := jwt.MapClaims{} + + _, err := jwt.ParseWithClaims(jwtToken, &claims, func(t *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + if err != nil { + return -1, err + } + + expiredIn := claims["exp"].(int64) - time.Now().Unix() + return expiredIn, nil } // isJWTExpired will check whether Smooch JWT is expired or not. diff --git a/smooch.go b/smooch.go index 1e494a1..0fe736b 100644 --- a/smooch.go +++ b/smooch.go @@ -28,6 +28,7 @@ var ( ErrMessageRoleEmpty = errors.New("message.Role is empty") ErrMessageTypeEmpty = errors.New("message.Type is empty") ErrVerifySecretEmpty = errors.New("verify secret is empty") + ErrDecodeToken = errors.New("error decode token") ) const ( @@ -81,7 +82,7 @@ type smoochClient struct { webhookEventHandlers []WebhookEventHandler httpClient *http.Client mtx sync.Mutex - redisPool *redis.Pool + RedisStorage *storage.RedisStorage } func New(o Options) (*smoochClient, error) { @@ -126,11 +127,6 @@ func New(o Options) (*smoochClient, error) { region = RegionEU } - jwtToken, err := GenerateJWT("app", o.KeyID, o.Secret) - if err != nil { - return nil, err - } - sc := &smoochClient{ mux: o.Mux, appID: o.AppID, @@ -140,16 +136,16 @@ func New(o Options) (*smoochClient, error) { logger: o.Logger, region: region, httpClient: o.HttpClient, - redisPool: o.RedisPool, + RedisStorage: storage.NewRedisStorage(o.RedisPool), } - // save token to redis - jwtExpiration, err := getJWTExpiration(jwtToken, sc.secret) + jwtToken, err := GenerateJWT("app", o.KeyID, o.Secret) if err != nil { return nil, err } - err = storage.SaveTokenToRedis(sc.redisPool, jwtToken, jwtExpiration) + // save token to redis + err = sc.RedisStorage.SaveTokenToRedis(jwtToken, JWTExpiration) if err != nil { return nil, err } @@ -164,12 +160,12 @@ func (sc *smoochClient) Handler() http.Handler { // IsJWTExpired will check whether Smooch JWT is expired or not. func (sc *smoochClient) IsJWTExpired() (bool, error) { - jwtToken, err := storage.GetTokenFromRedis(sc.redisPool) + jwtToken, err := sc.RedisStorage.GetTokenFromRedis() if err != nil { if err == redis.ErrNil { return true, nil } - return "", err + return false, err } return isJWTExpired(jwtToken, sc.secret) } @@ -184,12 +180,7 @@ func (sc *smoochClient) RenewToken() (string, error) { return "", err } - jwtExpiration := getJWTExpiration(jwtToken, sc.secret) - if err != nil { - return "", err - } - - err = storage.SaveTokenToRedis(sc.redisPool, jwtToken, jwtExpiration) + err = sc.RedisStorage.SaveTokenToRedis(jwtToken, JWTExpiration) if err != nil { return "", err } @@ -399,6 +390,9 @@ func (sc *smoochClient) createRequest( buf *bytes.Buffer, header http.Header) (*http.Request, error) { + var req *http.Request + var err error + if header == nil { header = http.Header{} } @@ -406,10 +400,13 @@ func (sc *smoochClient) createRequest( if header.Get(contentTypeHeaderKey) == "" { header.Set(contentTypeHeaderKey, contentTypeJSON) } - header.Set(authorizationHeaderKey, fmt.Sprintf("Bearer %s", sc.jwtToken)) - var req *http.Request - var err error + jwtToken, err := sc.RedisStorage.GetTokenFromRedis() + if err != nil { + return nil, err + } + header.Set(authorizationHeaderKey, fmt.Sprintf("Bearer %s", jwtToken)) + if buf == nil { req, err = http.NewRequest(method, url, nil) } else { From 860bebd2ed8115372ce8acf031553888a484745a Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Mon, 11 Nov 2019 14:03:42 +0700 Subject: [PATCH 06/19] add token expiration checking --- smooch.go | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/smooch.go b/smooch.go index 0fe736b..5374dd3 100644 --- a/smooch.go +++ b/smooch.go @@ -139,15 +139,12 @@ func New(o Options) (*smoochClient, error) { RedisStorage: storage.NewRedisStorage(o.RedisPool), } - jwtToken, err := GenerateJWT("app", o.KeyID, o.Secret) + _, err := sc.RedisStorage.GetTokenFromRedis() if err != nil { - return nil, err - } - - // save token to redis - err = sc.RedisStorage.SaveTokenToRedis(jwtToken, JWTExpiration) - if err != nil { - return nil, err + _, err := sc.RenewToken() + if err != nil { + return nil, err + } } sc.mux.HandleFunc(o.WebhookURL, sc.handle) @@ -392,6 +389,7 @@ func (sc *smoochClient) createRequest( var req *http.Request var err error + var jwtToken string if header == nil { header = http.Header{} @@ -401,10 +399,23 @@ func (sc *smoochClient) createRequest( header.Set(contentTypeHeaderKey, contentTypeJSON) } - jwtToken, err := sc.RedisStorage.GetTokenFromRedis() + isExpired, err := sc.IsJWTExpired() if err != nil { return nil, err } + + if isExpired { + jwtToken, err = sc.RenewToken() + if err != nil { + return nil, err + } + } else { + jwtToken, err = sc.RedisStorage.GetTokenFromRedis() + if err != nil { + return nil, err + } + } + header.Set(authorizationHeaderKey, fmt.Sprintf("Bearer %s", jwtToken)) if buf == nil { From cffba983232324d0104029e527d59cd3a2526bb5 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Mon, 11 Nov 2019 16:42:01 +0700 Subject: [PATCH 07/19] update go module --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index f02ee01..1e7ae4c 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,6 @@ module github.com/kitabisa/smooch require ( - github.com/EddyTravels/smooch v0.1.0 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gomodule/redigo v2.0.0+incompatible github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum index 560e037..4aab776 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/EddyTravels/smooch v0.1.0 h1:dbG4EK3otBtqL3JyGhJ6PVLMh42DI/M5pIFiRP4ATAo= -github.com/EddyTravels/smooch v0.1.0/go.mod h1:KIl3bCuRqlVyuIUVX5MPeQYexUbMcp+tU7UOZIeD7zc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= From 29cef0529ac37fb6eb8d408270ee15dd3779c927 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Fri, 29 Nov 2019 15:08:05 +0700 Subject: [PATCH 08/19] remove verify secret --- README.md | 1 - go.sum | 1 + smooch.go | 50 ++++++++++++++++---------------------------- smooch_test.go | 56 ++++++-------------------------------------------- 4 files changed, 25 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index e59bbe5..05e45f4 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ func main() { AppID: os.Getenv("SMOOCH_APP_ID"), KeyID: os.Getenv("SMOOCH_KEY_ID"), Secret: os.Getenv("SMOOCH_SECRET"), - VerifySecret: os.Getenv("SMOOCH_VERIFY_SECRET"), RedisPool: redisPool, }) diff --git a/go.sum b/go.sum index 4aab776..bb38a45 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,7 @@ github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp github.com/kitabisa/smooch v0.1.0 h1:dS+ouObVdoNFVZWMIqULMr/VbQoYhsBuSUdmp0VjbcM= 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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/smooch.go b/smooch.go index 5374dd3..ec515ad 100644 --- a/smooch.go +++ b/smooch.go @@ -20,15 +20,14 @@ import ( ) var ( - ErrUserIDEmpty = errors.New("user id is empty") - ErrKeyIDEmpty = errors.New("key id is empty") - ErrSecretEmpty = errors.New("secret is empty") - ErrRedisNil = errors.New("redis pool is nil") - ErrMessageNil = errors.New("message is nil") - ErrMessageRoleEmpty = errors.New("message.Role is empty") - ErrMessageTypeEmpty = errors.New("message.Type is empty") - ErrVerifySecretEmpty = errors.New("verify secret is empty") - ErrDecodeToken = errors.New("error decode token") + ErrUserIDEmpty = errors.New("user id is empty") + ErrKeyIDEmpty = errors.New("key id is empty") + ErrSecretEmpty = errors.New("secret is empty") + ErrRedisNil = errors.New("redis pool is nil") + ErrMessageNil = errors.New("message is nil") + ErrMessageRoleEmpty = errors.New("message.Role is empty") + ErrMessageTypeEmpty = errors.New("message.Type is empty") + ErrDecodeToken = errors.New("error decode token") ) const ( @@ -45,16 +44,15 @@ const ( ) type Options struct { - AppID string - KeyID string - Secret string - VerifySecret string - WebhookURL string - Mux *http.ServeMux - Logger Logger - Region string - HttpClient *http.Client - RedisPool *redis.Pool + AppID string + KeyID string + Secret string + WebhookURL string + Mux *http.ServeMux + Logger Logger + Region string + HttpClient *http.Client + RedisPool *redis.Pool } type WebhookEventHandler func(payload *Payload) @@ -65,7 +63,6 @@ type Client interface { RenewToken() (string, error) AddWebhookEventHandler(handler WebhookEventHandler) Send(userID string, message *Message) (*ResponsePayload, error) - VerifyRequest(r *http.Request) bool GetAppUser(userID string) (*AppUser, error) UploadFileAttachment(filepath string, upload AttachmentUpload) (*Attachment, error) UploadAttachment(r io.Reader, upload AttachmentUpload) (*Attachment, error) @@ -76,7 +73,6 @@ type smoochClient struct { appID string keyID string secret string - verifySecret string logger Logger region string webhookEventHandlers []WebhookEventHandler @@ -94,10 +90,6 @@ func New(o Options) (*smoochClient, error) { return nil, ErrSecretEmpty } - if o.VerifySecret == "" { - return nil, ErrVerifySecretEmpty - } - if o.RedisPool == nil { return nil, ErrRedisNil } @@ -132,7 +124,6 @@ func New(o Options) (*smoochClient, error) { appID: o.AppID, keyID: o.KeyID, secret: o.Secret, - verifySecret: o.VerifySecret, logger: o.Logger, region: region, httpClient: o.HttpClient, @@ -231,11 +222,6 @@ func (sc *smoochClient) Send(userID string, message *Message) (*ResponsePayload, return &responsePayload, nil } -func (sc *smoochClient) VerifyRequest(r *http.Request) bool { - givenSecret := r.Header.Get("X-Api-Key") - return sc.verifySecret == givenSecret -} - func (sc *smoochClient) GetAppUser(userID string) (*AppUser, error) { url := sc.getURL( fmt.Sprintf("/v1.1/apps/%s/appusers/%s", sc.appID, userID), @@ -332,7 +318,7 @@ func (sc *smoochClient) DeleteAttachment(attachment *Attachment) error { func (sc *smoochClient) handle(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - if r.Method != http.MethodPost || !sc.VerifyRequest(r) { + if r.Method != http.MethodPost { w.WriteHeader(http.StatusBadRequest) return } diff --git a/smooch_test.go b/smooch_test.go index 6756cfa..2dca361 100644 --- a/smooch_test.go +++ b/smooch_test.go @@ -221,8 +221,7 @@ func TestSendOKResponse(t *testing.T) { } sc, err := New(Options{ - VerifySecret: "very-secure-test-secret", - HttpClient: NewTestClient(fn), + HttpClient: NewTestClient(fn), }) assert.NoError(t, err) @@ -288,8 +287,7 @@ func TestSendErrorResponse(t *testing.T) { } sc, err := New(Options{ - VerifySecret: "very-secure-test-secret", - HttpClient: NewTestClient(fn), + HttpClient: NewTestClient(fn), }) assert.NoError(t, err) @@ -310,9 +308,7 @@ func TestSendErrorResponse(t *testing.T) { } func TestHandlerOK(t *testing.T) { - sc, err := New(Options{ - VerifySecret: "very-secure-test-secret", - }) + sc, err := New(Options{}) assert.NoError(t, err) handlerInvokeCounter := 0 @@ -342,43 +338,6 @@ func TestHandlerOK(t *testing.T) { assert.Equal(t, 2, handlerInvokeCounter) } -func TestVerifyRequest(t *testing.T) { - sc, err := New(Options{ - VerifySecret: "very-secure-test-secret", - }) - r := &http.Request{} - assert.NoError(t, err) - assert.False(t, sc.VerifyRequest(r)) - - r = &http.Request{ - Header: http.Header{}, - } - r.Header.Set("X-Api-Key", "very-secure-test-secret") - assert.NoError(t, err) - assert.True(t, sc.VerifyRequest(r)) - - sc, err = New(Options{ - VerifySecret: "very-secure-test-secret", - }) - assert.NoError(t, err) - - headers := http.Header{} - headers.Set("X-Api-Key", "very-secure-test-secret-wrong") - r = &http.Request{ - Header: headers, - } - assert.NoError(t, err) - assert.False(t, sc.VerifyRequest(r)) - - headers = http.Header{} - headers.Set("X-Api-Key", "very-secure-test-secret") - r = &http.Request{ - Header: headers, - } - assert.NoError(t, err) - assert.True(t, sc.VerifyRequest(r)) -} - func TestGetAppUser(t *testing.T) { fn := func(req *http.Request) *http.Response { @@ -393,8 +352,7 @@ func TestGetAppUser(t *testing.T) { } sc, err := New(Options{ - VerifySecret: "very-secure-test-secret", - HttpClient: NewTestClient(fn), + HttpClient: NewTestClient(fn), }) assert.NoError(t, err) @@ -448,8 +406,7 @@ func TestUploadAttachment(t *testing.T) { } sc, err := New(Options{ - VerifySecret: "very-secure-test-secret", - HttpClient: NewTestClient(fn), + HttpClient: NewTestClient(fn), }) assert.NoError(t, err) @@ -483,8 +440,7 @@ func TestDeleteAttachment(t *testing.T) { } sc, err := New(Options{ - VerifySecret: "very-secure-test-secret", - HttpClient: NewTestClient(fn), + HttpClient: NewTestClient(fn), }) assert.NoError(t, err) From 25afcaa888ce6b6cbf2ca4636b3c0269566cd730 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Sat, 30 Nov 2019 23:13:53 +0700 Subject: [PATCH 09/19] fix jwt expired --- jwt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwt.go b/jwt.go index 72076c3..96241f2 100644 --- a/jwt.go +++ b/jwt.go @@ -12,7 +12,7 @@ const JWTExpiration = 3600 func GenerateJWT(scope string, keyID string, secret string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "scope": scope, - "exp": JWTExpiration, + "exp": time.Now().Unix() + JWTExpiration, }) token.Header = map[string]interface{}{ "alg": "HS256", From c22546c371d7847e8097697f1247c86d9d882fc8 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Mon, 16 Dec 2019 15:47:35 +0700 Subject: [PATCH 10/19] add send HSM method --- smooch.go | 30 +++++++++++++++++++++++++ types.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/smooch.go b/smooch.go index ec515ad..6e16efa 100644 --- a/smooch.go +++ b/smooch.go @@ -63,7 +63,10 @@ type Client interface { RenewToken() (string, error) AddWebhookEventHandler(handler WebhookEventHandler) Send(userID string, message *Message) (*ResponsePayload, error) + SendHSM(userID string, hsmMessage *HsmMessage) (*ResponsePayload, error) GetAppUser(userID string) (*AppUser, error) + PreCreateAppUser(userID, surname, givenName string) (*AppUser, error) + LinkAppUserToChannel(channelType, confirmationType, phoneNumber string) (*AppUser, error) UploadFileAttachment(filepath string, upload AttachmentUpload) (*Attachment, error) UploadAttachment(r io.Reader, upload AttachmentUpload) (*Attachment, error) } @@ -222,6 +225,33 @@ func (sc *smoochClient) Send(userID string, message *Message) (*ResponsePayload, return &responsePayload, nil } +// SendHSM will send message using Whatsapp HSM template +func (sc *smoochClient) SendHSM(userID string, hsmMessage *HsmMessage) (*ResponsePayload, error) { + url := sc.getURL( + fmt.Sprintf("/v1.1/apps/%s/appusers/%s/messages", sc.appID, userID), + nil, + ) + + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(hsmMessage) + if err != nil { + return nil, err + } + + req, err := sc.createRequest(http.MethodPost, url, buf, nil) + if err != nil { + return nil, err + } + + var responsePayload ResponsePayload + err = sc.sendRequest(req, &responsePayload) + if err != nil { + return nil, err + } + + return &responsePayload, nil +} + func (sc *smoochClient) GetAppUser(userID string) (*AppUser, error) { url := sc.getURL( fmt.Sprintf("/v1.1/apps/%s/appusers/%s", sc.appID, userID), diff --git a/types.go b/types.go index a60df4b..ed28521 100644 --- a/types.go +++ b/types.go @@ -107,6 +107,7 @@ type AppUser struct { Email string `json:"email,omitempty"` GivenName string `json:"givenName,omitempty"` Surname string `json:"surname,omitempty"` + HasPaymentInfo bool `json:"hasPaymentInfo,omitmepty"` } type AppUserClient struct { @@ -199,6 +200,70 @@ func (m *Message) MarshalJSON() ([]byte, error) { return json.Marshal(aux) } +// HsmLanguage defines hsm language payload +type HsmLanguage struct { + Policy string `json:"policy"` + Code string `json:"code"` +} + +// HsmLocalizableParams defines hsm localizable params data +type HsmLocalizableParams struct { + Default interface{} `json:"default"` +} + +// HsmPayload defines payload for hsm +type HsmPayload struct { + Namespace string `json:"namespace"` + ElementName string `json:"element_name"` + Language HsmLanguage `json:"language"` + LocalizableParams []HsmLocalizableParams `json:"localizable_params"` +} + +// HsmMessageBody defines property for HSM message +type HsmMessageBody struct { + Type MessageType `json:"type"` + Hsm HsmPayload `json:"hsm"` +} + +// HsmMessage defines struct payload for Whatsapp HSM message +type HsmMessage struct { + Role Role `json:"role"` + MessageSchema string `json:"messageSchema"` + Message HsmMessageBody `json:"message"` + Received time.Time `json:"received,omitempty"` +} + +// UnmarshalJSON will unmarshall whatsapp HSM message +func (hm *HsmMessage) UnmarshalJSON(data []byte) error { + type Alias HsmMessage + aux := &struct { + Received float64 `json:"received"` + *Alias + }{ + Alias: (*Alias)(hm), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + seconds := int64(aux.Received) + ns := (int64(aux.Received*1000) - seconds*1000) * nsMultiplier + hm.Received = time.Unix(seconds, ns) + return nil +} + +// MarshalJSON will marshall whatsapp HSM message +func (hm *HsmMessage) MarshalJSON() ([]byte, error) { + type Alias HsmMessage + aux := &struct { + Received float64 `json:"received"` + *Alias + }{ + Alias: (*Alias)(hm), + } + aux.Received = float64(hm.Received.UnixNano()) / nsMultiplier + return json.Marshal(aux) +} + type MenuPayload struct { Menu Menu `json:"menu"` } From eac239f8852e12cc2ebcb551c9817b59e113e8c1 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Mon, 16 Dec 2019 19:28:41 +0700 Subject: [PATCH 11/19] add pre create user & link user to channel method --- smooch.go | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++---- types.go | 34 ++++++++++++++++ 2 files changed, 143 insertions(+), 8 deletions(-) diff --git a/smooch.go b/smooch.go index 6e16efa..4a954b4 100644 --- a/smooch.go +++ b/smooch.go @@ -20,14 +20,19 @@ import ( ) var ( - ErrUserIDEmpty = errors.New("user id is empty") - ErrKeyIDEmpty = errors.New("key id is empty") - ErrSecretEmpty = errors.New("secret is empty") - ErrRedisNil = errors.New("redis pool is nil") - ErrMessageNil = errors.New("message is nil") - ErrMessageRoleEmpty = errors.New("message.Role is empty") - ErrMessageTypeEmpty = errors.New("message.Type is empty") - ErrDecodeToken = errors.New("error decode token") + ErrUserIDEmpty = errors.New("user id is empty") + ErrSurnameEmpty = errors.New("surname is empty") + ErrGivenNameEmpty = errors.New("givenName is empty") + ErrPhonenumberEmpty = errors.New("phonenumber is empty") + ErrChannelTypeEmpty = errors.New("channel type is empty") + ErrConfirmationTypeEmpty = errors.New("confirmation type is empty") + ErrKeyIDEmpty = errors.New("key id is empty") + ErrSecretEmpty = errors.New("secret is empty") + ErrRedisNil = errors.New("redis pool is nil") + ErrMessageNil = errors.New("message is nil") + ErrMessageRoleEmpty = errors.New("message.Role is empty") + ErrMessageTypeEmpty = errors.New("message.Type is empty") + ErrDecodeToken = errors.New("error decode token") ) const ( @@ -272,6 +277,102 @@ func (sc *smoochClient) GetAppUser(userID string) (*AppUser, error) { return response.AppUser, nil } +// PreCreateAppUser will register user to smooch +func (sc *smoochClient) PreCreateAppUser(userID, surname, givenName string) (*AppUser, error) { + url := sc.getURL( + fmt.Sprintf("/v1.1/apps/%s/appusers", sc.appID), + nil, + ) + + if userID == "" { + return nil, ErrUserIDEmpty + } + + if surname == "" { + return nil, ErrSurnameEmpty + } + + if givenName == "" { + return nil, ErrGivenNameEmpty + } + + payload := PreCreateAppUserPayload{ + UserID: userID, + Surname: surname, + GivenName: givenName, + } + + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, err + } + + req, err := sc.createRequest(http.MethodPost, url, buf, nil) + if err != nil { + return nil, err + } + + var response PreCreateAppUserResponse + err = sc.sendRequest(req, &response) + if err != nil { + return nil, err + } + + return response.AppUser, nil +} + +// LinkAppUserToChannel will link user to specifiied channel +func (sc *smoochClient) LinkAppUserToChannel(userID, channelType, confirmationType, phoneNumber string) (*AppUser, error) { + url := sc.getURL( + fmt.Sprintf("/v1.1/apps/%s/appusers/%s/channels", sc.appID, userID), + nil, + ) + + if userID == "" { + return nil, ErrUserIDEmpty + } + + if channelType == "" { + return nil, ErrChannelTypeEmpty + } + + if confirmationType == "" { + return nil, ErrConfirmationTypeEmpty + } + + if phoneNumber == "" { + return nil, ErrPhonenumberEmpty + } + + payload := LinkAppUserToChannelPayload{ + Type: channelType, + Confirmation: LinkAppConfirmationData{ + Type: confirmationType, + }, + PhoneNumber: phoneNumber, + } + + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, err + } + + req, err := sc.createRequest(http.MethodPost, url, buf, nil) + if err != nil { + return nil, err + } + + var response LinkAppUserToChannelResponse + err = sc.sendRequest(req, &response) + if err != nil { + return nil, err + } + + return response.AppUser, nil +} + func (sc *smoochClient) UploadFileAttachment(filepath string, upload AttachmentUpload) (*Attachment, error) { r, err := os.Open(filepath) if err != nil { diff --git a/types.go b/types.go index ed28521..a2637bb 100644 --- a/types.go +++ b/types.go @@ -17,6 +17,7 @@ const ( MessageTypeLocation = MessageType("location") MessageTypeCarousel = MessageType("carousel") MessageTypeList = MessageType("list") + MessageTypeHSM = MessageType("hsm") ActionTypePostback = ActionType("postback") ActionTypeReply = ActionType("reply") @@ -47,6 +48,10 @@ const ( TriggerMessageDeliveryChannel = "message:delivery:channel" TriggerMessageDeliveryUser = "message:delivery:user" + ConfirmationTypeImmediate = "immediate" + ConfirmationTypeUserActivity = "userActivity" + ConfirmationTypePrompt = "prompt" + ImageRatioHorizontal = ImageRatio("horizontal") ImageRatioSquare = ImageRatio("square") @@ -304,6 +309,35 @@ type GetAppUserResponse struct { AppUser *AppUser `json:"appUser,omitempty"` } +// PreCreateAppUserPayload defines payload for pre-create app user request +type PreCreateAppUserPayload struct { + UserID string `json:"userId"` + Surname string `json:"surname"` + GivenName string `json:"givenName"` +} + +// PreCreateAppUserResponse defines response for pre-create use request +type PreCreateAppUserResponse struct { + AppUser *AppUser `json:"appUser,omitempty"` +} + +// LinkAppConfirmationData defines +type LinkAppConfirmationData struct { + Type string `json:"type"` +} + +// LinkAppUserToChannelPayload will link app user to specified channel +type LinkAppUserToChannelPayload struct { + Type string `json:"type"` + Confirmation LinkAppConfirmationData `json:"confirmation"` + PhoneNumber string `json:"phoneNumber"` +} + +// LinkAppUserToChannelResponse defines reponse for link app user to channel request +type LinkAppUserToChannelResponse struct { + AppUser *AppUser `json:"appUser,omitempty"` +} + type AttachmentUpload struct { MIMEType string Access string From 80aeaaf6033f00cb359034fdef42049c565d83c4 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Tue, 17 Dec 2019 11:35:27 +0700 Subject: [PATCH 12/19] add basic auth support --- README.md | 30 +++++++++++++++++++++++++ smooch.go | 67 ++++++++++++++++++++++++++++++++++--------------------- types.go | 3 +++ 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 05e45f4..4c9c787 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,10 @@ _**Note** : This a modified version version of [EddyTravels/smooch](https://gith ## Additional Feature - Token expiration & its checking. +- Pre-create app user and link app user to specified channel functionality. +- Send message in whatsapp HSM format. - Renew token functionality whenever token is expired. +- Support smooch basic auth and JWT auth. - Redis support as a centralized storage to store JWT token for supporting autoscaling environment. Use redigo as redis library. ## Tips @@ -22,6 +25,32 @@ $ go get -u github.com/kitabisa/smooch ## Example +Using basic authentication : + + +``` +import ( + "os" + + "github.com/kitabisa/smooch" +) + +func main() { + smoochClient, err := smooch.New(smooch.Options{ + Auth: smooch.AuthBasic, + AppID: os.Getenv("SMOOCH_APP_ID"), + KeyID: os.Getenv("SMOOCH_KEY_ID"), + Secret: os.Getenv("SMOOCH_SECRET"), + }) + + if err != nil { + panic(err) + } +} +``` + +Using JWT authentication : + ``` import ( "os" @@ -31,6 +60,7 @@ import ( func main() { smoochClient, err := smooch.New(smooch.Options{ + Auth: smooch.AuthJWT, AppID: os.Getenv("SMOOCH_APP_ID"), KeyID: os.Getenv("SMOOCH_KEY_ID"), Secret: os.Getenv("SMOOCH_SECRET"), diff --git a/smooch.go b/smooch.go index 4a954b4..420795a 100644 --- a/smooch.go +++ b/smooch.go @@ -33,6 +33,7 @@ var ( ErrMessageRoleEmpty = errors.New("message.Role is empty") ErrMessageTypeEmpty = errors.New("message.Type is empty") ErrDecodeToken = errors.New("error decode token") + ErrWrongAuth = errors.New("error wrong authentication") ) const ( @@ -49,6 +50,7 @@ const ( ) type Options struct { + Auth string AppID string KeyID string Secret string @@ -78,6 +80,7 @@ type Client interface { type smoochClient struct { mux *http.ServeMux + auth string appID string keyID string secret string @@ -127,22 +130,29 @@ func New(o Options) (*smoochClient, error) { region = RegionEU } + if o.Auth != AuthBasic && o.Auth != AuthJWT { + return nil, ErrWrongAuth + } + sc := &smoochClient{ - mux: o.Mux, - appID: o.AppID, - keyID: o.KeyID, - secret: o.Secret, - logger: o.Logger, - region: region, - httpClient: o.HttpClient, - RedisStorage: storage.NewRedisStorage(o.RedisPool), + mux: o.Mux, + appID: o.AppID, + keyID: o.KeyID, + secret: o.Secret, + logger: o.Logger, + region: region, + httpClient: o.HttpClient, } - _, err := sc.RedisStorage.GetTokenFromRedis() - if err != nil { - _, err := sc.RenewToken() + if sc.auth == AuthJWT { + sc.RedisStorage = storage.NewRedisStorage(o.RedisPool) + + _, err := sc.RedisStorage.GetTokenFromRedis() if err != nil { - return nil, err + _, err := sc.RenewToken() + if err != nil { + return nil, err + } } } @@ -516,35 +526,42 @@ func (sc *smoochClient) createRequest( header.Set(contentTypeHeaderKey, contentTypeJSON) } - isExpired, err := sc.IsJWTExpired() - if err != nil { - return nil, err - } - - if isExpired { - jwtToken, err = sc.RenewToken() + if sc.auth == AuthJWT { + isExpired, err := sc.IsJWTExpired() if err != nil { return nil, err } - } else { - jwtToken, err = sc.RedisStorage.GetTokenFromRedis() - if err != nil { - return nil, err + + if isExpired { + jwtToken, err = sc.RenewToken() + if err != nil { + return nil, err + } + } else { + jwtToken, err = sc.RedisStorage.GetTokenFromRedis() + if err != nil { + return nil, err + } } - } - header.Set(authorizationHeaderKey, fmt.Sprintf("Bearer %s", jwtToken)) + header.Set(authorizationHeaderKey, fmt.Sprintf("Bearer %s", jwtToken)) + } if buf == nil { req, err = http.NewRequest(method, url, nil) } else { req, err = http.NewRequest(method, url, buf) } + if err != nil { return nil, err } req.Header = header + if sc.auth == AuthBasic { + req.SetBasicAuth(sc.keyID, sc.secret) + } + return req, nil } diff --git a/types.go b/types.go index a2637bb..ecd80b6 100644 --- a/types.go +++ b/types.go @@ -27,6 +27,9 @@ const ( ActionTypeLink = ActionType("link") ActionTypeWebview = ActionType("webview") + AuthBasic = "basic_auth" + AuthJWT = "jwt" + SourceTypeWeb = "web" SourceTypeIOS = "ios" SourceTypeAndroid = "android" From 7021226345f7c569b42185abe334a748a156680e Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Tue, 17 Dec 2019 11:52:05 +0700 Subject: [PATCH 13/19] add basic auth support --- smooch.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/smooch.go b/smooch.go index 420795a..8df37f4 100644 --- a/smooch.go +++ b/smooch.go @@ -101,10 +101,6 @@ func New(o Options) (*smoochClient, error) { return nil, ErrSecretEmpty } - if o.RedisPool == nil { - return nil, ErrRedisNil - } - if o.Mux == nil { o.Mux = http.NewServeMux() } @@ -135,6 +131,7 @@ func New(o Options) (*smoochClient, error) { } sc := &smoochClient{ + auth: o.Auth, mux: o.Mux, appID: o.AppID, keyID: o.KeyID, @@ -145,6 +142,10 @@ func New(o Options) (*smoochClient, error) { } if sc.auth == AuthJWT { + if o.RedisPool == nil { + return nil, ErrRedisNil + } + sc.RedisStorage = storage.NewRedisStorage(o.RedisPool) _, err := sc.RedisStorage.GetTokenFromRedis() From 5368fd48e4b0e9541fb94d65d0a574dbbfdedca7 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Tue, 17 Dec 2019 14:43:11 +0700 Subject: [PATCH 14/19] add response data for http status & response flag --- error.go | 16 ++++++-- smooch.go | 118 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 74 insertions(+), 60 deletions(-) diff --git a/error.go b/error.go index e707cfb..8b1049e 100644 --- a/error.go +++ b/error.go @@ -6,6 +6,12 @@ import ( "net/http" ) +// ResponseData defines data for every response +type ResponseData struct { + HTTPCode int + Flag string +} + type SmoochError struct { message string code int @@ -19,11 +25,11 @@ func (e *SmoochError) Error() string { return e.message } -func checkSmoochError(r *http.Response) error { +func checkSmoochError(r *http.Response) (*ResponseData, error) { var errorPayload ErrorPayload decodeErr := json.NewDecoder(r.Body).Decode(&errorPayload) if decodeErr != nil { - return decodeErr + return nil, decodeErr } err := &SmoochError{ @@ -35,5 +41,9 @@ func checkSmoochError(r *http.Response) error { code: r.StatusCode, } - return err + respData := &ResponseData{ + HTTPCode: r.StatusCode, + Flag: errorPayload.Details.Code, + } + return respData, err } diff --git a/smooch.go b/smooch.go index 8df37f4..664bbc8 100644 --- a/smooch.go +++ b/smooch.go @@ -199,21 +199,21 @@ func (sc *smoochClient) AddWebhookEventHandler(handler WebhookEventHandler) { sc.webhookEventHandlers = append(sc.webhookEventHandlers, handler) } -func (sc *smoochClient) Send(userID string, message *Message) (*ResponsePayload, error) { +func (sc *smoochClient) Send(userID string, message *Message) (*ResponsePayload, *ResponseData, error) { if userID == "" { - return nil, ErrUserIDEmpty + return nil, nil, ErrUserIDEmpty } if message == nil { - return nil, ErrMessageNil + return nil, nil, ErrMessageNil } if message.Role == "" { - return nil, ErrMessageRoleEmpty + return nil, nil, ErrMessageRoleEmpty } if message.Type == "" { - return nil, ErrMessageTypeEmpty + return nil, nil, ErrMessageTypeEmpty } url := sc.getURL( @@ -224,25 +224,25 @@ func (sc *smoochClient) Send(userID string, message *Message) (*ResponsePayload, buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(message) if err != nil { - return nil, err + return nil, nil, err } req, err := sc.createRequest(http.MethodPost, url, buf, nil) if err != nil { - return nil, err + return nil, nil, err } var responsePayload ResponsePayload - err = sc.sendRequest(req, &responsePayload) + respData, err := sc.sendRequest(req, &responsePayload) if err != nil { - return nil, err + return nil, respData, err } - return &responsePayload, nil + return &responsePayload, respData, nil } // SendHSM will send message using Whatsapp HSM template -func (sc *smoochClient) SendHSM(userID string, hsmMessage *HsmMessage) (*ResponsePayload, error) { +func (sc *smoochClient) SendHSM(userID string, hsmMessage *HsmMessage) (*ResponsePayload, *ResponseData, error) { url := sc.getURL( fmt.Sprintf("/v1.1/apps/%s/appusers/%s/messages", sc.appID, userID), nil, @@ -251,24 +251,24 @@ func (sc *smoochClient) SendHSM(userID string, hsmMessage *HsmMessage) (*Respons buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(hsmMessage) if err != nil { - return nil, err + return nil, nil, err } req, err := sc.createRequest(http.MethodPost, url, buf, nil) if err != nil { - return nil, err + return nil, nil, err } var responsePayload ResponsePayload - err = sc.sendRequest(req, &responsePayload) + respData, err := sc.sendRequest(req, &responsePayload) if err != nil { - return nil, err + return nil, respData, err } - return &responsePayload, nil + return &responsePayload, respData, nil } -func (sc *smoochClient) GetAppUser(userID string) (*AppUser, error) { +func (sc *smoochClient) GetAppUser(userID string) (*AppUser, *ResponseData, error) { url := sc.getURL( fmt.Sprintf("/v1.1/apps/%s/appusers/%s", sc.appID, userID), nil, @@ -276,35 +276,35 @@ func (sc *smoochClient) GetAppUser(userID string) (*AppUser, error) { req, err := sc.createRequest(http.MethodGet, url, nil, nil) if err != nil { - return nil, err + return nil, nil, err } var response GetAppUserResponse - err = sc.sendRequest(req, &response) + respData, err := sc.sendRequest(req, &response) if err != nil { - return nil, err + return nil, respData, err } - return response.AppUser, nil + return response.AppUser, respData, nil } // PreCreateAppUser will register user to smooch -func (sc *smoochClient) PreCreateAppUser(userID, surname, givenName string) (*AppUser, error) { +func (sc *smoochClient) PreCreateAppUser(userID, surname, givenName string) (*AppUser, *ResponseData, error) { url := sc.getURL( fmt.Sprintf("/v1.1/apps/%s/appusers", sc.appID), nil, ) if userID == "" { - return nil, ErrUserIDEmpty + return nil, nil, ErrUserIDEmpty } if surname == "" { - return nil, ErrSurnameEmpty + return nil, nil, ErrSurnameEmpty } if givenName == "" { - return nil, ErrGivenNameEmpty + return nil, nil, ErrGivenNameEmpty } payload := PreCreateAppUserPayload{ @@ -316,44 +316,44 @@ func (sc *smoochClient) PreCreateAppUser(userID, surname, givenName string) (*Ap buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(payload) if err != nil { - return nil, err + return nil, nil, err } req, err := sc.createRequest(http.MethodPost, url, buf, nil) if err != nil { - return nil, err + return nil, nil, err } var response PreCreateAppUserResponse - err = sc.sendRequest(req, &response) + respData, err := sc.sendRequest(req, &response) if err != nil { - return nil, err + return nil, respData, err } - return response.AppUser, nil + return response.AppUser, respData, nil } // LinkAppUserToChannel will link user to specifiied channel -func (sc *smoochClient) LinkAppUserToChannel(userID, channelType, confirmationType, phoneNumber string) (*AppUser, error) { +func (sc *smoochClient) LinkAppUserToChannel(userID, channelType, confirmationType, phoneNumber string) (*AppUser, *ResponseData, error) { url := sc.getURL( fmt.Sprintf("/v1.1/apps/%s/appusers/%s/channels", sc.appID, userID), nil, ) if userID == "" { - return nil, ErrUserIDEmpty + return nil, nil, ErrUserIDEmpty } if channelType == "" { - return nil, ErrChannelTypeEmpty + return nil, nil, ErrChannelTypeEmpty } if confirmationType == "" { - return nil, ErrConfirmationTypeEmpty + return nil, nil, ErrConfirmationTypeEmpty } if phoneNumber == "" { - return nil, ErrPhonenumberEmpty + return nil, nil, ErrPhonenumberEmpty } payload := LinkAppUserToChannelPayload{ @@ -367,34 +367,34 @@ func (sc *smoochClient) LinkAppUserToChannel(userID, channelType, confirmationTy buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(payload) if err != nil { - return nil, err + return nil, nil, err } req, err := sc.createRequest(http.MethodPost, url, buf, nil) if err != nil { - return nil, err + return nil, nil, err } var response LinkAppUserToChannelResponse - err = sc.sendRequest(req, &response) + respData, err := sc.sendRequest(req, &response) if err != nil { - return nil, err + return nil, respData, err } - return response.AppUser, nil + return response.AppUser, respData, nil } -func (sc *smoochClient) UploadFileAttachment(filepath string, upload AttachmentUpload) (*Attachment, error) { +func (sc *smoochClient) UploadFileAttachment(filepath string, upload AttachmentUpload) (*Attachment, *ResponseData, error) { r, err := os.Open(filepath) if err != nil { - return nil, err + return nil, nil, err } defer r.Close() return sc.UploadAttachment(r, upload) } -func (sc *smoochClient) UploadAttachment(r io.Reader, upload AttachmentUpload) (*Attachment, error) { +func (sc *smoochClient) UploadAttachment(r io.Reader, upload AttachmentUpload) (*Attachment, *ResponseData, error) { queryParams := url.Values{ "access": []string{upload.Access}, @@ -421,19 +421,19 @@ func (sc *smoochClient) UploadAttachment(r io.Reader, upload AttachmentUpload) ( req, err := sc.createMultipartRequest(url, formData) if err != nil { - return nil, err + return nil, nil, err } var response Attachment - err = sc.sendRequest(req, &response) + respData, err := sc.sendRequest(req, &response) if err != nil { - return nil, err + return nil, respData, err } - return &response, nil + return &response, respData, nil } -func (sc *smoochClient) DeleteAttachment(attachment *Attachment) error { +func (sc *smoochClient) DeleteAttachment(attachment *Attachment) (*ResponseData, error) { url := sc.getURL( fmt.Sprintf("/v1.1/apps/%s/attachments", sc.appID), nil, @@ -442,20 +442,20 @@ func (sc *smoochClient) DeleteAttachment(attachment *Attachment) error { buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(attachment) if err != nil { - return err + return nil, err } req, err := sc.createRequest(http.MethodPost, url, buf, nil) if err != nil { - return err + return nil, err } - err = sc.sendRequest(req, nil) + respData, err := sc.sendRequest(req, nil) if err != nil { - return err + return respData, err } - return nil + return respData, nil } func (sc *smoochClient) handle(w http.ResponseWriter, r *http.Request) { @@ -610,10 +610,10 @@ func (sc *smoochClient) createMultipartRequest( return req, nil } -func (sc *smoochClient) sendRequest(req *http.Request, v interface{}) error { +func (sc *smoochClient) sendRequest(req *http.Request, v interface{}) (*ResponseData, error) { response, err := sc.httpClient.Do(req) if err != nil { - return err + return nil, err } defer response.Body.Close() @@ -621,10 +621,14 @@ func (sc *smoochClient) sendRequest(req *http.Request, v interface{}) error { if v != nil { err := json.NewDecoder(response.Body).Decode(&v) if err != nil { - return err + return nil, err } } - return nil + + respData := &ResponseData{ + HTTPCode: response.StatusCode, + } + return respData, nil } return checkSmoochError(response) } From 1854333c0fe52b9dc0ea0e4a67c6da302a0ed009 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Tue, 17 Dec 2019 14:57:00 +0700 Subject: [PATCH 15/19] update unit test --- error_test.go | 2 +- smooch_test.go | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/error_test.go b/error_test.go index 121abb2..78d3549 100644 --- a/error_test.go +++ b/error_test.go @@ -27,7 +27,7 @@ func TestCheckSmoochError(t *testing.T) { Body: r, } - err := checkSmoochError(response) + _, err := checkSmoochError(response) assert.Error(t, err) assert.EqualError(t, err, "StatusCode: 401 Code: unauthorized Message: Authorization is required") } diff --git a/smooch_test.go b/smooch_test.go index 2dca361..d490884 100644 --- a/smooch_test.go +++ b/smooch_test.go @@ -226,17 +226,17 @@ func TestSendOKResponse(t *testing.T) { assert.NoError(t, err) message := &Message{} - response, err := sc.Send("", message) + response, _, err := sc.Send("", message) assert.Nil(t, response) assert.Error(t, err) assert.EqualError(t, err, ErrUserIDEmpty.Error()) - response, err = sc.Send("TestUser", nil) + response, _, err = sc.Send("TestUser", nil) assert.Nil(t, response) assert.Error(t, err) assert.EqualError(t, err, ErrMessageNil.Error()) - response, err = sc.Send("TestUser", message) + response, _, err = sc.Send("TestUser", message) assert.Nil(t, response) assert.Error(t, err) assert.EqualError(t, err, ErrMessageRoleEmpty.Error()) @@ -244,7 +244,7 @@ func TestSendOKResponse(t *testing.T) { message = &Message{ Role: RoleAppUser, } - response, err = sc.Send("TestUser", message) + response, _, err = sc.Send("TestUser", message) assert.Nil(t, response) assert.Error(t, err) assert.EqualError(t, err, ErrMessageTypeEmpty.Error()) @@ -253,7 +253,7 @@ func TestSendOKResponse(t *testing.T) { Role: RoleAppUser, Type: MessageTypeText, } - response, err = sc.Send("TestUser", message) + response, _, err = sc.Send("TestUser", message) assert.NotNil(t, response) assert.NoError(t, err) @@ -295,7 +295,7 @@ func TestSendErrorResponse(t *testing.T) { Role: RoleAppUser, Type: MessageTypeText, } - response, err := sc.Send("TestUser", message) + response, _, err := sc.Send("TestUser", message) assert.Nil(t, response) assert.Error(t, err) @@ -356,7 +356,7 @@ func TestGetAppUser(t *testing.T) { }) assert.NoError(t, err) - appUser, err := sc.GetAppUser("123") + appUser, _, err := sc.GetAppUser("123") assert.NotNil(t, appUser) assert.NoError(t, err) @@ -410,7 +410,7 @@ func TestUploadAttachment(t *testing.T) { }) assert.NoError(t, err) - r, err := sc.UploadFileAttachment("fixtures/smooch.png", NewAttachmentUpload("image/png")) + r, _, err := sc.UploadFileAttachment("fixtures/smooch.png", NewAttachmentUpload("image/png")) assert.NotNil(t, r) assert.NoError(t, err) @@ -420,7 +420,7 @@ func TestUploadAttachment(t *testing.T) { r.MediaURL, ) - r, err = sc.UploadFileAttachment("fixtures/smooch-not-exists.png", NewAttachmentUpload("image/png")) + r, _, err = sc.UploadFileAttachment("fixtures/smooch-not-exists.png", NewAttachmentUpload("image/png")) assert.Nil(t, r) assert.Error(t, err) } @@ -444,7 +444,7 @@ func TestDeleteAttachment(t *testing.T) { }) assert.NoError(t, err) - err = sc.DeleteAttachment(&Attachment{ + _, err = sc.DeleteAttachment(&Attachment{ MediaURL: "https://media.smooch.io/conversation/c7f6e6d6c3a637261bd9656f/a77caae4cbbd263a0938eba00016b7c8/test.png", MediaType: "", }) From 9344dc9b47fa6ffd6962809dfb0fe7303a13d695 Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Tue, 17 Dec 2019 16:59:43 +0700 Subject: [PATCH 16/19] export struct --- smooch.go | 124 +++++++++++++++++++++++++++--------------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/smooch.go b/smooch.go index 664bbc8..a1e1be0 100644 --- a/smooch.go +++ b/smooch.go @@ -78,21 +78,21 @@ type Client interface { UploadAttachment(r io.Reader, upload AttachmentUpload) (*Attachment, error) } -type smoochClient struct { - mux *http.ServeMux - auth string - appID string - keyID string - secret string - logger Logger - region string - webhookEventHandlers []WebhookEventHandler - httpClient *http.Client - mtx sync.Mutex +type SmoochClient struct { + Mux *http.ServeMux + Auth string + AppID string + KeyID string + Secret string + Logger Logger + Region string + WebhookEventHandlers []WebhookEventHandler + HttpClient *http.Client + Mtx sync.Mutex RedisStorage *storage.RedisStorage } -func New(o Options) (*smoochClient, error) { +func New(o Options) (*SmoochClient, error) { if o.KeyID == "" { return nil, ErrKeyIDEmpty } @@ -130,18 +130,18 @@ func New(o Options) (*smoochClient, error) { return nil, ErrWrongAuth } - sc := &smoochClient{ - auth: o.Auth, - mux: o.Mux, - appID: o.AppID, - keyID: o.KeyID, - secret: o.Secret, - logger: o.Logger, - region: region, - httpClient: o.HttpClient, + sc := &SmoochClient{ + Auth: o.Auth, + Mux: o.Mux, + AppID: o.AppID, + KeyID: o.KeyID, + Secret: o.Secret, + Logger: o.Logger, + Region: region, + HttpClient: o.HttpClient, } - if sc.auth == AuthJWT { + if sc.Auth == AuthJWT { if o.RedisPool == nil { return nil, ErrRedisNil } @@ -157,16 +157,16 @@ func New(o Options) (*smoochClient, error) { } } - sc.mux.HandleFunc(o.WebhookURL, sc.handle) + sc.Mux.HandleFunc(o.WebhookURL, sc.handle) return sc, nil } -func (sc *smoochClient) Handler() http.Handler { - return sc.mux +func (sc *SmoochClient) Handler() http.Handler { + return sc.Mux } // IsJWTExpired will check whether Smooch JWT is expired or not. -func (sc *smoochClient) IsJWTExpired() (bool, error) { +func (sc *SmoochClient) IsJWTExpired() (bool, error) { jwtToken, err := sc.RedisStorage.GetTokenFromRedis() if err != nil { if err == redis.ErrNil { @@ -174,15 +174,15 @@ func (sc *smoochClient) IsJWTExpired() (bool, error) { } return false, err } - return isJWTExpired(jwtToken, sc.secret) + return isJWTExpired(jwtToken, sc.Secret) } // RenewToken will generate new Smooch JWT token. -func (sc *smoochClient) RenewToken() (string, error) { - sc.mtx.Lock() - defer sc.mtx.Unlock() +func (sc *SmoochClient) RenewToken() (string, error) { + sc.Mtx.Lock() + defer sc.Mtx.Unlock() - jwtToken, err := GenerateJWT("app", sc.keyID, sc.secret) + jwtToken, err := GenerateJWT("app", sc.KeyID, sc.Secret) if err != nil { return "", err } @@ -195,11 +195,11 @@ func (sc *smoochClient) RenewToken() (string, error) { return jwtToken, nil } -func (sc *smoochClient) AddWebhookEventHandler(handler WebhookEventHandler) { - sc.webhookEventHandlers = append(sc.webhookEventHandlers, handler) +func (sc *SmoochClient) AddWebhookEventHandler(handler WebhookEventHandler) { + sc.WebhookEventHandlers = append(sc.WebhookEventHandlers, handler) } -func (sc *smoochClient) Send(userID string, message *Message) (*ResponsePayload, *ResponseData, error) { +func (sc *SmoochClient) Send(userID string, message *Message) (*ResponsePayload, *ResponseData, error) { if userID == "" { return nil, nil, ErrUserIDEmpty } @@ -217,7 +217,7 @@ func (sc *smoochClient) Send(userID string, message *Message) (*ResponsePayload, } url := sc.getURL( - fmt.Sprintf("/v1.1/apps/%s/appusers/%s/messages", sc.appID, userID), + fmt.Sprintf("/v1.1/apps/%s/appusers/%s/messages", sc.AppID, userID), nil, ) @@ -242,9 +242,9 @@ func (sc *smoochClient) Send(userID string, message *Message) (*ResponsePayload, } // SendHSM will send message using Whatsapp HSM template -func (sc *smoochClient) SendHSM(userID string, hsmMessage *HsmMessage) (*ResponsePayload, *ResponseData, error) { +func (sc *SmoochClient) SendHSM(userID string, hsmMessage *HsmMessage) (*ResponsePayload, *ResponseData, error) { url := sc.getURL( - fmt.Sprintf("/v1.1/apps/%s/appusers/%s/messages", sc.appID, userID), + fmt.Sprintf("/v1.1/apps/%s/appusers/%s/messages", sc.AppID, userID), nil, ) @@ -268,9 +268,9 @@ func (sc *smoochClient) SendHSM(userID string, hsmMessage *HsmMessage) (*Respons return &responsePayload, respData, nil } -func (sc *smoochClient) GetAppUser(userID string) (*AppUser, *ResponseData, error) { +func (sc *SmoochClient) GetAppUser(userID string) (*AppUser, *ResponseData, error) { url := sc.getURL( - fmt.Sprintf("/v1.1/apps/%s/appusers/%s", sc.appID, userID), + fmt.Sprintf("/v1.1/apps/%s/appusers/%s", sc.AppID, userID), nil, ) @@ -289,9 +289,9 @@ func (sc *smoochClient) GetAppUser(userID string) (*AppUser, *ResponseData, erro } // PreCreateAppUser will register user to smooch -func (sc *smoochClient) PreCreateAppUser(userID, surname, givenName string) (*AppUser, *ResponseData, error) { +func (sc *SmoochClient) PreCreateAppUser(userID, surname, givenName string) (*AppUser, *ResponseData, error) { url := sc.getURL( - fmt.Sprintf("/v1.1/apps/%s/appusers", sc.appID), + fmt.Sprintf("/v1.1/apps/%s/appusers", sc.AppID), nil, ) @@ -334,9 +334,9 @@ func (sc *smoochClient) PreCreateAppUser(userID, surname, givenName string) (*Ap } // LinkAppUserToChannel will link user to specifiied channel -func (sc *smoochClient) LinkAppUserToChannel(userID, channelType, confirmationType, phoneNumber string) (*AppUser, *ResponseData, error) { +func (sc *SmoochClient) LinkAppUserToChannel(userID, channelType, confirmationType, phoneNumber string) (*AppUser, *ResponseData, error) { url := sc.getURL( - fmt.Sprintf("/v1.1/apps/%s/appusers/%s/channels", sc.appID, userID), + fmt.Sprintf("/v1.1/apps/%s/appusers/%s/channels", sc.AppID, userID), nil, ) @@ -384,7 +384,7 @@ func (sc *smoochClient) LinkAppUserToChannel(userID, channelType, confirmationTy return response.AppUser, respData, nil } -func (sc *smoochClient) UploadFileAttachment(filepath string, upload AttachmentUpload) (*Attachment, *ResponseData, error) { +func (sc *SmoochClient) UploadFileAttachment(filepath string, upload AttachmentUpload) (*Attachment, *ResponseData, error) { r, err := os.Open(filepath) if err != nil { return nil, nil, err @@ -394,7 +394,7 @@ func (sc *smoochClient) UploadFileAttachment(filepath string, upload AttachmentU return sc.UploadAttachment(r, upload) } -func (sc *smoochClient) UploadAttachment(r io.Reader, upload AttachmentUpload) (*Attachment, *ResponseData, error) { +func (sc *SmoochClient) UploadAttachment(r io.Reader, upload AttachmentUpload) (*Attachment, *ResponseData, error) { queryParams := url.Values{ "access": []string{upload.Access}, @@ -410,7 +410,7 @@ func (sc *smoochClient) UploadAttachment(r io.Reader, upload AttachmentUpload) ( } url := sc.getURL( - fmt.Sprintf("/v1.1/apps/%s/attachments", sc.appID), + fmt.Sprintf("/v1.1/apps/%s/attachments", sc.AppID), queryParams, ) @@ -433,9 +433,9 @@ func (sc *smoochClient) UploadAttachment(r io.Reader, upload AttachmentUpload) ( return &response, respData, nil } -func (sc *smoochClient) DeleteAttachment(attachment *Attachment) (*ResponseData, error) { +func (sc *SmoochClient) DeleteAttachment(attachment *Attachment) (*ResponseData, error) { url := sc.getURL( - fmt.Sprintf("/v1.1/apps/%s/attachments", sc.appID), + fmt.Sprintf("/v1.1/apps/%s/attachments", sc.AppID), nil, ) @@ -458,7 +458,7 @@ func (sc *smoochClient) DeleteAttachment(attachment *Attachment) (*ResponseData, return respData, nil } -func (sc *smoochClient) handle(w http.ResponseWriter, r *http.Request) { +func (sc *SmoochClient) handle(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method != http.MethodPost { w.WriteHeader(http.StatusBadRequest) @@ -468,7 +468,7 @@ func (sc *smoochClient) handle(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) - sc.logger.Errorw("request body read failed", "err", err) + sc.Logger.Errorw("request body read failed", "err", err) return } @@ -476,7 +476,7 @@ func (sc *smoochClient) handle(w http.ResponseWriter, r *http.Request) { err = json.Unmarshal(body, &payload) if err != nil { w.WriteHeader(http.StatusUnprocessableEntity) - sc.logger.Errorw("could not decode response", "err", err) + sc.Logger.Errorw("could not decode response", "err", err) return } @@ -485,15 +485,15 @@ func (sc *smoochClient) handle(w http.ResponseWriter, r *http.Request) { sc.dispatch(&payload) } -func (sc *smoochClient) dispatch(p *Payload) { - for _, handler := range sc.webhookEventHandlers { +func (sc *SmoochClient) dispatch(p *Payload) { + for _, handler := range sc.WebhookEventHandlers { handler(p) } } -func (sc *smoochClient) getURL(endpoint string, values url.Values) string { +func (sc *SmoochClient) getURL(endpoint string, values url.Values) string { rootURL := usRootURL - if sc.region == RegionEU { + if sc.Region == RegionEU { rootURL = euRootURL } @@ -509,7 +509,7 @@ func (sc *smoochClient) getURL(endpoint string, values url.Values) string { return u.String() } -func (sc *smoochClient) createRequest( +func (sc *SmoochClient) createRequest( method string, url string, buf *bytes.Buffer, @@ -527,7 +527,7 @@ func (sc *smoochClient) createRequest( header.Set(contentTypeHeaderKey, contentTypeJSON) } - if sc.auth == AuthJWT { + if sc.Auth == AuthJWT { isExpired, err := sc.IsJWTExpired() if err != nil { return nil, err @@ -559,14 +559,14 @@ func (sc *smoochClient) createRequest( } req.Header = header - if sc.auth == AuthBasic { - req.SetBasicAuth(sc.keyID, sc.secret) + if sc.Auth == AuthBasic { + req.SetBasicAuth(sc.KeyID, sc.Secret) } return req, nil } -func (sc *smoochClient) createMultipartRequest( +func (sc *SmoochClient) createMultipartRequest( url string, values map[string]io.Reader) (*http.Request, error) { buf := new(bytes.Buffer) @@ -610,8 +610,8 @@ func (sc *smoochClient) createMultipartRequest( return req, nil } -func (sc *smoochClient) sendRequest(req *http.Request, v interface{}) (*ResponseData, error) { - response, err := sc.httpClient.Do(req) +func (sc *SmoochClient) sendRequest(req *http.Request, v interface{}) (*ResponseData, error) { + response, err := sc.HttpClient.Do(req) if err != nil { return nil, err } From 4ad827f97ea30f493d5848ed14587391c98c9dcb Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Tue, 17 Dec 2019 18:14:17 +0700 Subject: [PATCH 17/19] add mapstructure --- go.mod | 1 + go.sum | 2 ++ types.go | 18 +++++++++--------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 1e7ae4c..83b9531 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/kitabisa/smooch require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gomodule/redigo v2.0.0+incompatible + github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/stretchr/testify v1.3.0 ) diff --git a/go.sum b/go.sum index bb38a45..0496525 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/kitabisa/smooch v0.1.0 h1:dS+ouObVdoNFVZWMIqULMr/VbQoYhsBuSUdmp0VjbcM= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= diff --git a/types.go b/types.go index ecd80b6..da49ad7 100644 --- a/types.go +++ b/types.go @@ -210,27 +210,27 @@ func (m *Message) MarshalJSON() ([]byte, error) { // HsmLanguage defines hsm language payload type HsmLanguage struct { - Policy string `json:"policy"` - Code string `json:"code"` + Policy string `json:"policy" mapstructure:"policy"` + Code string `json:"code" mapstructure:"code"` } // HsmLocalizableParams defines hsm localizable params data type HsmLocalizableParams struct { - Default interface{} `json:"default"` + Default interface{} `json:"default" mapstructure:"default"` } // HsmPayload defines payload for hsm type HsmPayload struct { - Namespace string `json:"namespace"` - ElementName string `json:"element_name"` - Language HsmLanguage `json:"language"` - LocalizableParams []HsmLocalizableParams `json:"localizable_params"` + Namespace string `json:"namespace" mapstructure:"namespace"` + ElementName string `json:"element_name" mapstructure:"element_name"` + Language HsmLanguage `json:"language" mapstructure:"language"` + LocalizableParams []HsmLocalizableParams `json:"localizable_params" mapstructure:"localizable_params"` } // HsmMessageBody defines property for HSM message type HsmMessageBody struct { - Type MessageType `json:"type"` - Hsm HsmPayload `json:"hsm"` + Type MessageType `json:"type" mapstructure:"type"` + Hsm HsmPayload `json:"hsm" mapstructure:"hsm"` } // HsmMessage defines struct payload for Whatsapp HSM message From 8b7ce6c8088dffad2bf4129f2ab07c35e105a64e Mon Sep 17 00:00:00 2001 From: Alvin Rizki <4lvin.rizki@gmail.com> Date: Mon, 6 Jan 2020 17:58:50 +0700 Subject: [PATCH 18/19] allow blank surname --- smooch.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/smooch.go b/smooch.go index a1e1be0..6bb44ef 100644 --- a/smooch.go +++ b/smooch.go @@ -299,10 +299,6 @@ func (sc *SmoochClient) PreCreateAppUser(userID, surname, givenName string) (*Ap return nil, nil, ErrUserIDEmpty } - if surname == "" { - return nil, nil, ErrSurnameEmpty - } - if givenName == "" { return nil, nil, ErrGivenNameEmpty } From 927a61bac52b370b6c1a342bea34d468464bfdfc Mon Sep 17 00:00:00 2001 From: Ardit Date: Mon, 17 Feb 2020 17:37:06 +0700 Subject: [PATCH 19/19] Updating Pull Request Template 2020-02-17 --- pull_request_template.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pull_request_template.md diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..45426a3 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,23 @@ +## What does this PR do? +_Place what this pull request changes and anything affected. If your PR block or require another PR, also need to mention here_ + +## Why are we doing this? Any context or related work? +_You may put your JIRA card link here_ + +## Where should a reviewer start? +_optional -- if your changes affected so much files, it is encouraged to give helper for reviewer_ + +## Screenshots +_optional -- You may put the database, sequence or any diagram needed_ + +## Manual testing steps? +_Steps to do tests. including all possible that can hape_ + +## Database changes +_optional -- If there's database changes, put it here_ + +## Config changes +_optional -- If there's config changes, put it here_ + +## Deployment instructions +_optional -- Better to put it if there's some 'special case' for deployment_