commit 34cba277b99251bff34fd355861f19c86ef79d0c Author: Alex Lushpai Date: Wed Sep 4 15:22:27 2019 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..1202d96 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## MG Transport Library + +This library provides different functions like error-reporting, loggingm localization, etc. in order to make it easier to create transports diff --git a/core/config.go b/core/config.go new file mode 100644 index 0000000..c736162 --- /dev/null +++ b/core/config.go @@ -0,0 +1,169 @@ +package core + +import ( + "io/ioutil" + "path/filepath" + + "github.com/op/go-logging" + "gopkg.in/yaml.v2" +) + +// ConfigInterface settings data structure +type ConfigInterface interface { + GetVersion() string + GetSentryDSN() string + GetLogLevel() logging.Level + GetDebug() bool + GetHTTPConfig() HTTPServerConfig + GetDBConfig() DatabaseConfig + GetAWSConfig() ConfigAWS + GetTransportInfo() InfoInterface + GetUpdateInterval() int + IsDebug() bool +} + +// InfoInterface transport settings data structure +type InfoInterface interface { + GetName() string + GetCode() string + GetLogoPath() string +} + +// Config struct +type Config struct { + Version string `yaml:"version"` + LogLevel logging.Level `yaml:"log_level"` + Database DatabaseConfig `yaml:"database"` + SentryDSN string `yaml:"sentry_dsn"` + HTTPServer HTTPServerConfig `yaml:"http_server"` + Debug bool `yaml:"debug"` + UpdateInterval int `yaml:"update_interval"` + ConfigAWS ConfigAWS `yaml:"config_aws"` + TransportInfo Info `yaml:"transport_info"` +} + +// Info struct +type Info struct { + Name string `yaml:"name"` + Code string `yaml:"code"` + LogoPath string `yaml:"logo_path"` +} + +// ConfigAWS struct +type ConfigAWS struct { + AccessKeyID string `yaml:"access_key_id"` + SecretAccessKey string `yaml:"secret_access_key"` + Region string `yaml:"region"` + Bucket string `yaml:"bucket"` + FolderName string `yaml:"folder_name"` + ContentType string `yaml:"content_type"` +} + +// DatabaseConfig struct +type DatabaseConfig struct { + Connection string `yaml:"connection"` + Logging bool `yaml:"logging"` + TablePrefix string `yaml:"table_prefix"` + MaxOpenConnections int `yaml:"max_open_connections"` + MaxIdleConnections int `yaml:"max_idle_connections"` + ConnectionLifetime int `yaml:"connection_lifetime"` +} + +// HTTPServerConfig struct +type HTTPServerConfig struct { + Host string `yaml:"host"` + Listen string `yaml:"listen"` +} + +// NewConfig reads configuration file and returns config instance +// Usage: +// NewConfig("config.yml") +func NewConfig(path string) *Config { + return (&Config{}).LoadConfig(path) +} + +// LoadConfig read & load configuration file +func (c *Config) LoadConfig(path string) *Config { + var err error + + path, err = filepath.Abs(path) + if err != nil { + panic(err) + } + + source, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + + if err = yaml.Unmarshal(source, c); err != nil { + panic(err) + } + + return c +} + +// GetSentryDSN sentry connection dsn +func (c Config) GetSentryDSN() string { + return c.SentryDSN +} + +// GetVersion transport version +func (c Config) GetVersion() string { + return c.Version +} + +// GetLogLevel log level +func (c Config) GetLogLevel() logging.Level { + return c.LogLevel +} + +// GetTransportInfo transport basic data +func (c Config) GetTransportInfo() InfoInterface { + return c.TransportInfo +} + +// GetDebug debug flag +func (c Config) GetDebug() bool { + return c.Debug +} + +// GetAWSConfig AWS configuration +func (c Config) GetAWSConfig() ConfigAWS { + return c.ConfigAWS +} + +// GetDBConfig database configuration +func (c Config) GetDBConfig() DatabaseConfig { + return c.Database +} + +// GetHTTPConfig server configuration +func (c Config) GetHTTPConfig() HTTPServerConfig { + return c.HTTPServer +} + +// GetUpdateInterval user data update interval +func (c Config) GetUpdateInterval() int { + return c.UpdateInterval +} + +// IsDebug debug state +func (c Config) IsDebug() bool { + return c.Debug +} + +// GetName transport name +func (t Info) GetName() string { + return t.Name +} + +// GetCode transport code +func (t Info) GetCode() string { + return t.Code +} + +// GetLogoPath transport logo +func (t Info) GetLogoPath() string { + return t.LogoPath +} diff --git a/core/engine.go b/core/engine.go new file mode 100644 index 0000000..0eb3a08 --- /dev/null +++ b/core/engine.go @@ -0,0 +1,115 @@ +package core + +import ( + "github.com/gin-gonic/gin" + "github.com/op/go-logging" +) + +// Engine struct +type Engine struct { + Localizer + ORM + Sentry + Utils + ginEngine *gin.Engine + Logger *logging.Logger + Config ConfigInterface + LogFormatter logging.Formatter + prepared bool +} + +// New Engine instance (must be configured manually, gin can be accessed via engine.Router() directly or engine.ConfigureRouter(...) with callback) +func New() *Engine { + return &Engine{ + Config: nil, + Localizer: Localizer{}, + ORM: ORM{}, + Sentry: Sentry{}, + Utils: Utils{}, + ginEngine: nil, + Logger: nil, + prepared: false, + } +} + +func (e *Engine) initGin() { + if !e.Config.IsDebug() { + gin.SetMode(gin.ReleaseMode) + } + + r := gin.New() + r.Use(gin.Recovery()) + + if e.Config.IsDebug() { + r.Use(gin.Logger()) + } + + r.Use(e.LocalizationMiddleware(), e.ErrorMiddleware()) + e.ginEngine = r +} + +// Prepare engine for start +func (e *Engine) Prepare() *Engine { + if e.prepared { + panic("engine already initialized") + } + if e.Config == nil { + panic("engine.Config must be loaded before initializing") + } + + if e.DefaultError == "" { + e.DefaultError = "error" + } + if e.LogFormatter == nil { + e.LogFormatter = DefaultLogFormatter() + } + if e.LocaleBundle == nil { + e.LocaleBundle = DefaultLocalizerBundle() + } + if e.LocaleMatcher == nil { + e.LocaleMatcher = DefaultLocalizerMatcher() + } + + e.LoadTranslations() + e.createDB(e.Config.GetDBConfig()) + e.createRavenClient(e.Config.GetSentryDSN()) + e.resetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0) + e.Logger = NewLogger(e.Config.GetTransportInfo().GetCode(), e.Config.GetLogLevel(), e.LogFormatter) + e.Utils.Localizer = &e.Localizer + e.Sentry.Localizer = &e.Localizer + e.Utils.Logger = e.Logger + e.Sentry.Logger = e.Logger + e.prepared = true + + return e +} + +// CreateRenderer with translation function +func (e *Engine) CreateRenderer(callback func(*Renderer)) Renderer { + renderer := NewRenderer(e.LocalizationFuncMap()) + callback(&renderer) + return renderer +} + +// Router will return current gin.Engine or panic if it's not present +func (e *Engine) Router() *gin.Engine { + if !e.prepared { + panic("prepare engine first") + } + if e.ginEngine == nil { + e.initGin() + } + + return e.ginEngine +} + +// ConfigureRouter will call provided callback with current gin.Engine, or panic if engine is not present +func (e *Engine) ConfigureRouter(callback func(*gin.Engine)) *Engine { + callback(e.Router()) + return e +} + +// Run gin.Engine loop, or panic if engine is not present +func (e *Engine) Run() error { + return e.Router().Run(e.Config.GetHTTPConfig().Listen) +} diff --git a/core/error.go b/core/error.go new file mode 100644 index 0000000..0abf347 --- /dev/null +++ b/core/error.go @@ -0,0 +1,17 @@ +package core + +import "net/http" + +// ErrorResponse struct +type ErrorResponse struct { + Error string `json:"error"` +} + +// BadRequest returns ErrorResponse with code 400 +// Usage (with gin): +// context.JSON(BadRequest("invalid data")) +func BadRequest(error string) (int, interface{}) { + return http.StatusBadRequest, ErrorResponse{ + Error: error, + } +} diff --git a/core/localizer.go b/core/localizer.go new file mode 100644 index 0000000..bbb5f9e --- /dev/null +++ b/core/localizer.go @@ -0,0 +1,137 @@ +package core + +import ( + "html/template" + "io/ioutil" + "path" + + "github.com/gin-gonic/gin" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" + "gopkg.in/yaml.v2" +) + +// Localizer struct +type Localizer struct { + i18n *i18n.Localizer + LocaleBundle *i18n.Bundle + LocaleMatcher language.Matcher + LanguageTag language.Tag + TranslationsPath string +} + +// NewLocalizer returns localizer instance with specified parameters. +// Usage: +// NewLocalizer(language.English, DefaultLocalizerBundle(), DefaultLocalizerMatcher(), "translations") +func NewLocalizer(locale language.Tag, bundle *i18n.Bundle, matcher language.Matcher, translationsPath string) *Localizer { + localizer := &Localizer{ + i18n: nil, + LocaleBundle: bundle, + LocaleMatcher: matcher, + TranslationsPath: translationsPath, + } + localizer.SetLanguage(locale) + localizer.LoadTranslations() + + return localizer +} + +// DefaultLocalizerBundle returns new localizer bundle with English as default language +func DefaultLocalizerBundle() *i18n.Bundle { + return &i18n.Bundle{DefaultLanguage: language.English} +} + +// DefaultLocalizerMatcher returns matcher with English, Russian and Spanish tags +func DefaultLocalizerMatcher() language.Matcher { + return language.NewMatcher([]language.Tag{ + language.English, + language.Russian, + language.Spanish, + }) +} + +// LocalizationMiddleware returns gin.HandlerFunc which will set localizer language by Accept-Language header +// Usage: +// engine := gin.New() +// localizer := NewLocalizer("en", DefaultLocalizerBundle(), DefaultLocalizerMatcher(), "translations") +// engine.Use(localizer.LocalizationMiddleware()) +func (l *Localizer) LocalizationMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + l.SetLocale(c.GetHeader("Accept-Language")) + } +} + +// LocalizationFuncMap returns template.FuncMap (html template is used) with one method - trans +// Usage in code: +// engine := gin.New() +// engine.FuncMap = localizer.LocalizationFuncMap() +// or (with multitemplate) +// renderer := multitemplate.NewRenderer() +// funcMap := localizer.LocalizationFuncMap() +// renderer.AddFromFilesFuncs("index", funcMap, "template/index.html") +// funcMap must be passed for every .AddFromFilesFuncs call +// Usage in templates: +//

{{"need_login_msg" | trans}} +// You can borrow FuncMap from this method and add your functions to it. +func (l *Localizer) LocalizationFuncMap() template.FuncMap { + return template.FuncMap{ + "trans": l.GetLocalizedMessage, + } +} + +func (l *Localizer) getLocaleBundle() *i18n.Bundle { + if l.LocaleBundle == nil { + l.LocaleBundle = DefaultLocalizerBundle() + } + return l.LocaleBundle +} + +// LoadTranslations will load all translation files from translations directory +func (l *Localizer) LoadTranslations() { + l.getLocaleBundle().RegisterUnmarshalFunc("yml", yaml.Unmarshal) + files, err := ioutil.ReadDir(l.TranslationsPath) + if err != nil { + panic(err) + } + for _, f := range files { + if !f.IsDir() { + l.getLocaleBundle().MustLoadMessageFile(path.Join(l.TranslationsPath, f.Name())) + } + } +} + +// SetLocale will change language for current localizer +func (l *Localizer) SetLocale(al string) { + tag, _ := language.MatchStrings(l.LocaleMatcher, al) + l.SetLanguage(tag) +} + +// SetLanguage will change language using language tag +func (l *Localizer) SetLanguage(tag language.Tag) { + l.LanguageTag = tag + l.FetchLanguage() +} + +// FetchLanguage will load language from tag +func (l *Localizer) FetchLanguage() { + l.i18n = i18n.NewLocalizer(l.getLocaleBundle(), l.LanguageTag.String()) +} + +// GetLocalizedMessage will return localized message by it's ID +func (l *Localizer) GetLocalizedMessage(messageID string) string { + return l.i18n.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID}) +} + +// GetLocalizedTemplateMessage will return localized message with specified data +// It uses text/template syntax: https://golang.org/pkg/text/template/ +func (l *Localizer) GetLocalizedTemplateMessage(messageID string, templateData map[string]interface{}) string { + return l.i18n.MustLocalize(&i18n.LocalizeConfig{ + MessageID: messageID, + TemplateData: templateData, + }) +} + +// BadRequestLocalized is same as BadRequest(string), but passed string will be localized +func (l *Localizer) BadRequestLocalized(err string) (int, interface{}) { + return BadRequest(l.GetLocalizedMessage(err)) +} diff --git a/core/logger.go b/core/logger.go new file mode 100644 index 0000000..4a1398d --- /dev/null +++ b/core/logger.go @@ -0,0 +1,28 @@ +package core + +import ( + "os" + + "github.com/op/go-logging" +) + +// NewLogger will create new logger with specified formatter. +// Usage: +// logger := NewLogger(config, DefaultLogFormatter()) +func NewLogger(transportCode string, logLevel logging.Level, logFormat logging.Formatter) *logging.Logger { + logger := logging.MustGetLogger(transportCode) + logBackend := logging.NewLogBackend(os.Stdout, "", 0) + formatBackend := logging.NewBackendFormatter(logBackend, logFormat) + backend1Leveled := logging.AddModuleLevel(logBackend) + backend1Leveled.SetLevel(logLevel, "") + logging.SetBackend(formatBackend) + + return logger +} + +// DefaultLogFormatter will return default formatter for logs +func DefaultLogFormatter() logging.Formatter { + return logging.MustStringFormatter( + `%{time:2006-01-02 15:04:05.000} %{level:.4s} => %{message}`, + ) +} diff --git a/core/models.go b/core/models.go new file mode 100644 index 0000000..122612b --- /dev/null +++ b/core/models.go @@ -0,0 +1,53 @@ +package core + +import "time" + +// Connection model +type Connection struct { + ID int `gorm:"primary_key"` + ClientID string `gorm:"column:client_id;type:varchar(70);not null;unique" json:"clientId,omitempty"` + Key string `gorm:"column:api_key;type:varchar(100);not null" json:"api_key,omitempty" binding:"required,max=100"` + URL string `gorm:"column:api_url;type:varchar(255);not null" json:"api_url,omitempty" binding:"required,validatecrmurl,max=255"` + GateURL string `gorm:"column:mg_url;type:varchar(255);not null;" json:"mg_url,omitempty" binding:"max=255"` + GateToken string `gorm:"column:mg_token;type:varchar(100);not null;unique" json:"mg_token,omitempty" binding:"max=100"` + CreatedAt time.Time + UpdatedAt time.Time + Active bool `json:"active,omitempty"` + Accounts []Account `gorm:"foreignkey:ConnectionID"` +} + +// Account model +type Account struct { + ID int `gorm:"primary_key"` + ConnectionID int `gorm:"column:connection_id" json:"connectionId,omitempty"` + Channel uint64 `gorm:"column:channel;not null;unique" json:"channel,omitempty"` + ChannelSettingsHash string `gorm:"column:channel_settings_hash type:varchar(70)" binding:"max=70"` + Name string `gorm:"column:name type:varchar(40)" json:"name,omitempty" binding:"max=40"` + Lang string `gorm:"column:lang type:varchar(2)" json:"lang,omitempty" binding:"max=2"` + CreatedAt time.Time + UpdatedAt time.Time +} + +// User model +type User struct { + ID int `gorm:"primary_key"` + ExternalID int `gorm:"column:external_id;not null;unique"` + UserPhotoURL string `gorm:"column:user_photo_url type:varchar(255)" binding:"max=255"` + UserPhotoID string `gorm:"column:user_photo_id type:varchar(100)" binding:"max=100"` + CreatedAt time.Time + UpdatedAt time.Time +} + +// TableName will return table name for User +// It will not work if User is not embedded, but mapped as another type +// type MyUser User // will not work +// but +// type MyUser struct { // will work +// User +// } +func (User) TableName() string { + return "mg_user" +} + +// Accounts list +type Accounts []Account diff --git a/core/orm.go b/core/orm.go new file mode 100644 index 0000000..9171dc3 --- /dev/null +++ b/core/orm.go @@ -0,0 +1,42 @@ +package core + +import ( + "time" + + "github.com/jinzhu/gorm" + // PostgreSQL is an default + _ "github.com/jinzhu/gorm/dialects/postgres" +) + +// ORM struct +type ORM struct { + DB *gorm.DB +} + +// NewORM will init new database connection +func NewORM(config DatabaseConfig) *ORM { + orm := &ORM{} + orm.createDB(config) + return orm +} + +func (orm *ORM) createDB(config DatabaseConfig) { + db, err := gorm.Open("postgres", config.Connection) + if err != nil { + panic(err) + } + + db.DB().SetConnMaxLifetime(time.Duration(config.ConnectionLifetime) * time.Second) + db.DB().SetMaxOpenConns(config.MaxOpenConnections) + db.DB().SetMaxIdleConns(config.MaxIdleConnections) + + db.SingularTable(true) + db.LogMode(config.Logging) + + orm.DB = db +} + +// CloseDB close database connection +func (orm *ORM) CloseDB() { + _ = orm.DB.Close() +} diff --git a/core/sentry.go b/core/sentry.go new file mode 100644 index 0000000..d62f897 --- /dev/null +++ b/core/sentry.go @@ -0,0 +1,382 @@ +package core + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "runtime/debug" + "strconv" + + "github.com/Neur0toxine/mg-transport-lib/internal" + "github.com/getsentry/raven-go" + "github.com/gin-gonic/gin" + "github.com/op/go-logging" +) + +// ErrorHandlerFunc will handle errors +type ErrorHandlerFunc func(recovery interface{}, c *gin.Context) + +// SentryTaggedTypes list +type SentryTaggedTypes []SentryTagged + +// SentryTags list for SentryTaggedStruct. Format: name => property name +type SentryTags map[string]string + +// SentryTagged interface for both tagged scalar and struct +type SentryTagged interface { + BuildTags(interface{}) (map[string]string, error) + GetContextKey() string + GetTags() SentryTags + GetName() string +} + +// Sentry struct. Holds SentryTaggedStruct list +type Sentry struct { + TaggedTypes SentryTaggedTypes + Stacktrace bool + DefaultError string + Localizer *Localizer + Logger *logging.Logger + Client *raven.Client +} + +// SentryTaggedStruct holds information about type, it's key in gin.Context (for middleware), and it's properties +type SentryTaggedStruct struct { + Type reflect.Type + GinContextKey string + Tags SentryTags +} + +// SentryTaggedScalar variable from context +type SentryTaggedScalar struct { + SentryTaggedStruct + Name string +} + +// NewSentry constructor +func NewSentry(sentryDSN string, defaultError string, taggedTypes SentryTaggedTypes, logger *logging.Logger, localizer *Localizer) *Sentry { + sentry := &Sentry{ + DefaultError: defaultError, + TaggedTypes: taggedTypes, + Localizer: localizer, + Logger: logger, + Stacktrace: true, + } + sentry.createRavenClient(sentryDSN) + return sentry +} + +// NewTaggedStruct constructor +func NewTaggedStruct(sample interface{}, ginCtxKey string, tags map[string]string) *SentryTaggedStruct { + n := make(map[string]string) + for k, v := range tags { + n[v] = k + } + + return &SentryTaggedStruct{ + Type: reflect.TypeOf(sample), + GinContextKey: ginCtxKey, + Tags: n, + } +} + +// NewTaggedScalar constructor +func NewTaggedScalar(sample interface{}, ginCtxKey string, name string) *SentryTaggedScalar { + return &SentryTaggedScalar{ + SentryTaggedStruct: SentryTaggedStruct{ + Type: reflect.TypeOf(sample), + GinContextKey: ginCtxKey, + Tags: SentryTags{}, + }, + Name: name, + } +} + +// createRavenClient will init raven.Client +func (s *Sentry) createRavenClient(sentryDSN string) { + client, _ := raven.New(sentryDSN) + s.Client = client +} + +// combineGinErrorHandlers calls several error handlers simultaneously +func (s *Sentry) combineGinErrorHandlers(handlers ...ErrorHandlerFunc) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + rec := recover() + for _, handler := range handlers { + handler(rec, c) + } + + if rec != nil || len(c.Errors) > 0 { + c.Abort() + } + }() + + c.Next() + } +} + +// ErrorMiddleware returns error handlers, attachable to gin.Engine +func (s *Sentry) ErrorMiddleware() gin.HandlerFunc { + defaultHandlers := []ErrorHandlerFunc{ + s.ErrorResponseHandler(), + s.PanicLogger(), + s.ErrorLogger(), + } + + if s.Client != nil { + defaultHandlers = append(defaultHandlers, s.ErrorCaptureHandler()) + } + + return s.combineGinErrorHandlers(defaultHandlers...) +} + +// PanicLogger logs panic +func (s *Sentry) PanicLogger() ErrorHandlerFunc { + return func(recovery interface{}, c *gin.Context) { + if recovery != nil { + if s.Logger != nil { + s.Logger.Error(c.Request.RequestURI, recovery) + } else { + fmt.Print("ERROR =>", c.Request.RequestURI, recovery) + } + debug.PrintStack() + } + } +} + +// ErrorLogger logs basic errors +func (s *Sentry) ErrorLogger() ErrorHandlerFunc { + return func(recovery interface{}, c *gin.Context) { + for _, err := range c.Errors { + if s.Logger != nil { + s.Logger.Error(c.Request.RequestURI, err.Err) + } else { + fmt.Print("ERROR =>", c.Request.RequestURI, err.Err) + } + } + } +} + +// ErrorResponseHandler will be executed in case of any unexpected error +func (s *Sentry) ErrorResponseHandler() ErrorHandlerFunc { + return func(recovery interface{}, c *gin.Context) { + publicErrors := c.Errors.ByType(gin.ErrorTypePublic) + privateLen := len(c.Errors.ByType(gin.ErrorTypePrivate)) + publicLen := len(publicErrors) + + if privateLen == 0 && publicLen == 0 && recovery == nil { + return + } + + messagesLen := publicLen + if privateLen > 0 || recovery != nil { + messagesLen++ + } + + messages := make([]string, messagesLen) + index := 0 + for _, err := range publicErrors { + messages[index] = err.Error() + index++ + } + + if privateLen > 0 || recovery != nil { + if s.Localizer == nil { + messages[index] = s.DefaultError + } else { + messages[index] = s.Localizer.GetLocalizedMessage(s.DefaultError) + } + } + + c.JSON(http.StatusInternalServerError, gin.H{"error": messages}) + } +} + +// ErrorCaptureHandler will generate error data and send it to sentry +func (s *Sentry) ErrorCaptureHandler() ErrorHandlerFunc { + return func(recovery interface{}, c *gin.Context) { + tags := map[string]string{ + "endpoint": c.Request.RequestURI, + } + + if len(s.TaggedTypes) > 0 { + for _, tagged := range s.TaggedTypes { + if item, ok := c.Get(tagged.GetContextKey()); ok && item != nil { + if itemTags, err := tagged.BuildTags(item); err == nil { + for tagName, tagValue := range itemTags { + tags[tagName] = tagValue + } + } + } + } + } + + if recovery != nil { + stacktrace := raven.NewStacktrace(4, 3, nil) + recStr := fmt.Sprint(recovery) + err := errors.New(recStr) + go s.Client.CaptureMessageAndWait( + recStr, + tags, + raven.NewException(err, stacktrace), + raven.NewHttp(c.Request), + ) + } + + for _, err := range c.Errors { + if s.Stacktrace { + stacktrace := internal.NewRavenStackTrace(s.Client, err.Err, 0) + go s.Client.CaptureMessageAndWait( + err.Error(), + tags, + raven.NewException(err.Err, stacktrace), + raven.NewHttp(c.Request), + ) + } else { + go s.Client.CaptureErrorAndWait(err.Err, tags) + } + } + } +} + +// AddTag will add tag with property name which holds tag in object +func (t *SentryTaggedStruct) AddTag(name string, property string) *SentryTaggedStruct { + t.Tags[property] = name + return t +} + +// GetTags is Tags getter +func (t *SentryTaggedStruct) GetTags() SentryTags { + return t.Tags +} + +// GetContextKey is GinContextKey getter +func (t *SentryTaggedStruct) GetContextKey() string { + return t.GinContextKey +} + +// GetName is useless for SentryTaggedStruct +func (t *SentryTaggedStruct) GetName() string { + return "" +} + +// GetProperty will extract property string representation from specified object. It will be properly formatted. +func (t *SentryTaggedStruct) GetProperty(v interface{}, property string) (name string, value string, err error) { + val := reflect.Indirect(reflect.ValueOf(v)) + if !val.IsValid() { + err = errors.New("invalid value provided") + return + } + + if val.Kind() != reflect.Struct { + err = fmt.Errorf("passed value must be struct, %s provided", val.String()) + return + } + + if val.Type().Name() != t.Type.Name() { + err = fmt.Errorf("passed value should be of type `%s`, got `%s` instead", t.Type.String(), val.Type().String()) + return + } + + if i, ok := t.Tags[property]; ok { + name = i + } else { + err = fmt.Errorf("cannot find property `%s`", property) + } + + field := val.FieldByName(property) + if !field.IsValid() { + err = fmt.Errorf("invalid property, got %s", field.String()) + return + } + + value = t.valueToString(field) + return +} + +// BuildTags will extract tags for Sentry from specified object +func (t *SentryTaggedStruct) BuildTags(v interface{}) (tags map[string]string, err error) { + items := make(map[string]string) + for prop, name := range t.Tags { + if _, value, e := t.GetProperty(v, prop); e == nil { + items[name] = value + } else { + err = e + return + } + } + + tags = items + return +} + +// valueToString convert reflect.Value to string representation +func (t *SentryTaggedStruct) valueToString(field reflect.Value) string { + k := field.Kind() + switch { + case k == reflect.Bool: + return strconv.FormatBool(field.Bool()) + case k >= reflect.Int && k <= reflect.Int64: + return strconv.FormatInt(field.Int(), 10) + case k >= reflect.Uint && k <= reflect.Uintptr: + return strconv.FormatUint(field.Uint(), 10) + case k == reflect.Float32 || k == reflect.Float64: + bitSize := 32 + if k == reflect.Float64 { + bitSize = 64 + } + return strconv.FormatFloat(field.Float(), 'f', 12, bitSize) + default: + return field.String() + } +} + +// GetTags is useless for SentryTaggedScalar +func (t *SentryTaggedScalar) GetTags() SentryTags { + return SentryTags{} +} + +// GetContextKey is getter for GinContextKey +func (t *SentryTaggedScalar) GetContextKey() string { + return t.GinContextKey +} + +// GetName is getter for Name (tag name for scalar) +func (t *SentryTaggedScalar) GetName() string { + return t.Name +} + +// Get will extract property string representation from specified object. It will be properly formatted. +func (t *SentryTaggedScalar) Get(v interface{}) (value string, err error) { + val := reflect.Indirect(reflect.ValueOf(v)) + if !val.IsValid() { + err = errors.New("invalid value provided") + return + } + + if val.Kind() == reflect.Struct { + err = errors.New("passed value must not be struct") + return + } + + if val.Type().Name() != t.Type.Name() { + err = fmt.Errorf("passed value should be of type `%s`, got `%s` instead", t.Type.String(), val.Type().String()) + return + } + + value = t.valueToString(val) + return +} + +// BuildTags returns map with single item in this format: => +func (t *SentryTaggedScalar) BuildTags(v interface{}) (items map[string]string, err error) { + items = make(map[string]string) + if value, e := t.Get(v); e == nil { + items[t.Name] = value + } else { + err = e + } + return +} diff --git a/core/template.go b/core/template.go new file mode 100644 index 0000000..62bca8f --- /dev/null +++ b/core/template.go @@ -0,0 +1,26 @@ +package core + +import ( + "html/template" + + "github.com/gin-contrib/multitemplate" +) + +// Renderer wraps multitemplate.Renderer in order to make it easier to use +type Renderer struct { + multitemplate.Renderer + FuncMap template.FuncMap +} + +// NewRenderer is a Renderer constructor +func NewRenderer(funcMap template.FuncMap) Renderer { + return Renderer{ + Renderer: multitemplate.NewRenderer(), + FuncMap: funcMap, + } +} + +// Push is an AddFromFilesFuncs wrapper +func (r *Renderer) Push(name string, files ...string) *template.Template { + return r.AddFromFilesFuncs(name, r.FuncMap, files...) +} diff --git a/core/utils.go b/core/utils.go new file mode 100644 index 0000000..c66fd73 --- /dev/null +++ b/core/utils.go @@ -0,0 +1,208 @@ +package core + +import ( + "crypto/sha1" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "net/http" + "regexp" + "strings" + "sync/atomic" + "time" + + "github.com/Neur0toxine/mg-transport-lib/internal" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/op/go-logging" + v5 "github.com/retailcrm/api-client-go/v5" + v1 "github.com/retailcrm/mg-transport-api-client-go/v1" +) + +// Utils service object +type Utils struct { + IsDebug bool + ConfigAWS ConfigAWS + Localizer *Localizer + Logger *logging.Logger + TokenCounter uint32 + slashRegex *regexp.Regexp +} + +// NewUtils will create new Utils instance +func NewUtils(awsConfig ConfigAWS, localizer *Localizer, logger *logging.Logger, debug bool) *Utils { + return &Utils{ + IsDebug: debug, + ConfigAWS: awsConfig, + Localizer: localizer, + Logger: logger, + TokenCounter: 0, + slashRegex: internal.SlashRegex, + } +} + +// resetUtils +func (u *Utils) resetUtils(awsConfig ConfigAWS, debug bool, tokenCounter uint32) { + u.TokenCounter = tokenCounter + u.ConfigAWS = awsConfig + u.IsDebug = debug + u.slashRegex = internal.SlashRegex +} + +// GenerateToken will generate long pseudo-random string. +func (u *Utils) GenerateToken() string { + c := atomic.AddUint32(&u.TokenCounter, 1) + + return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d%d", time.Now().UnixNano(), c)))) +} + +// GetAPIClient will initialize retailCRM api client from url and key +func (u *Utils) GetAPIClient(url, key string) (*v5.Client, int, error) { + client := v5.New(url, key) + client.Debug = u.IsDebug + + cr, status, e := client.APICredentials() + if e.RuntimeErr != nil { + u.Logger.Error(url, status, e.RuntimeErr, cr) + return nil, http.StatusInternalServerError, e.RuntimeErr + + } + + if !cr.Success { + u.Logger.Error(url, status, e.ApiErr, cr) + return nil, http.StatusBadRequest, errors.New(u.Localizer.GetLocalizedMessage("incorrect_url_key")) + } + + if res := u.checkCredentials(cr.Credentials); len(res) != 0 { + u.Logger.Error(url, status, res) + return nil, + http.StatusBadRequest, + errors.New( + u.Localizer.GetLocalizedTemplateMessage( + "missing_credentials", + map[string]interface{}{ + "Credentials": strings.Join(res, ", "), + }, + ), + ) + } + + return client, 0, nil +} + +func (u *Utils) checkCredentials(credential []string) []string { + rc := make([]string, len(internal.CredentialsTransport)) + copy(rc, internal.CredentialsTransport) + + for _, vc := range credential { + for kn, vn := range rc { + if vn == vc { + if len(rc) == 1 { + rc = rc[:0] + break + } + rc = append(rc[:kn], rc[kn+1:]...) + } + } + } + + return rc +} + +// UploadUserAvatar will upload avatar for user +func (u *Utils) UploadUserAvatar(url string) (picURLs3 string, err error) { + s3Config := &aws.Config{ + Credentials: credentials.NewStaticCredentials( + u.ConfigAWS.AccessKeyID, + u.ConfigAWS.SecretAccessKey, + ""), + Region: aws.String(u.ConfigAWS.Region), + } + + s := session.Must(session.NewSession(s3Config)) + uploader := s3manager.NewUploader(s) + + resp, err := http.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + return "", fmt.Errorf("get: %v code: %v", url, resp.StatusCode) + } + + result, err := uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(u.ConfigAWS.Bucket), + Key: aws.String(fmt.Sprintf("%v/%v.jpg", u.ConfigAWS.FolderName, u.GenerateToken())), + Body: resp.Body, + ContentType: aws.String(u.ConfigAWS.ContentType), + ACL: aws.String("public-read"), + }) + if err != nil { + return + } + + picURLs3 = result.Location + + return +} + +// GetMGItemData will upload file to MG by URL and return information about attachable item +func GetMGItemData(client *v1.MgClient, url string, caption string) (v1.Item, int, error) { + item := v1.Item{} + + data, st, err := client.UploadFileByURL( + v1.UploadFileByUrlRequest{ + Url: url, + }, + ) + if err != nil { + return item, st, err + } + + item.ID = data.ID + item.Caption = caption + + return item, st, err +} + +// RemoveTrailingSlash will remove slash at the end of any string +func (u *Utils) RemoveTrailingSlash(crmURL string) string { + return u.slashRegex.ReplaceAllString(crmURL, ``) +} + +// GetEntitySHA1 will serialize any value to JSON and return SHA1 hash of this JSON +func GetEntitySHA1(v interface{}) (hash string, err error) { + res, _ := json.Marshal(v) + + h := sha1.New() + _, err = h.Write(res) + hash = fmt.Sprintf("%x", h.Sum(nil)) + + return +} + +// ReplaceMarkdownSymbols will remove markdown symbols from text +func ReplaceMarkdownSymbols(s string) string { + for _, v := range internal.MarkdownSymbols { + s = strings.Replace(s, v, "\\"+v, -1) + } + + return s +} + +// DefaultCurrencies will return default currencies list for all bots +func DefaultCurrencies() map[string]string { + return map[string]string{ + "rub": "₽", + "uah": "₴", + "byr": "Br", + "kzt": "₸", + "usd": "$", + "eur": "€", + } +} diff --git a/core/validator.go b/core/validator.go new file mode 100644 index 0000000..e22602b --- /dev/null +++ b/core/validator.go @@ -0,0 +1,24 @@ +package core + +import ( + "reflect" + + "github.com/gin-gonic/gin/binding" + "github.com/Neur0toxine/mg-transport-lib/internal" + "gopkg.in/go-playground/validator.v8" +) + +func init() { + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + if err := v.RegisterValidation("validatecrmurl", validateCrmURL); err != nil { + panic("cannot register crm url validator: " + err.Error()) + } + } +} + +func validateCrmURL( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + return internal.RegCommandName.Match([]byte(field.Interface().(string))) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..78811cd --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module github.com/retailcrm/mg-transport-core + +go 1.12 + +require ( + github.com/aws/aws-sdk-go v1.23.9 + github.com/certifi/gocertifi v0.0.0-20190506164543-d2eda7129713 // indirect + github.com/denisenkom/go-mssqldb v0.0.0-20190830225923-3302f0226fbd // indirect + github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect + github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03 + github.com/gin-contrib/multitemplate v0.0.0-20180827023943-5799bbbb6dce + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.3.0 + github.com/go-sql-driver/mysql v1.4.1 // indirect + github.com/golang/protobuf v1.3.2 // indirect + github.com/google/go-querystring v1.0.0 // indirect + github.com/jinzhu/gorm v1.9.1 + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.0.1 // indirect + github.com/lib/pq v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.9 // indirect + github.com/mattn/go-sqlite3 v1.11.0 // indirect + github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5 + github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 + github.com/pkg/errors v0.8.1 + github.com/retailcrm/api-client-go v1.1.1 + github.com/retailcrm/mg-transport-api-client-go v1.1.31 + github.com/stretchr/testify v1.4.0 // indirect + github.com/ugorji/go v1.1.7 // indirect + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect + golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 + golang.org/x/tools v0.0.0-20190830082254-f340ed3ae274 // indirect + gopkg.in/go-playground/validator.v8 v8.18.2 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a78a223 --- /dev/null +++ b/go.sum @@ -0,0 +1,200 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= +cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= +github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/aws/aws-sdk-go v1.23.9 h1:UYWPGrBMlrW5VCYeWMbog1T/kqZzkvvheUDQaaUAhqI= +github.com/aws/aws-sdk-go v1.23.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/certifi/gocertifi v0.0.0-20190506164543-d2eda7129713 h1:UNOqI3EKhvbqV8f1Vm3NIwkrhq388sGCeAH2Op7w0rc= +github.com/certifi/gocertifi v0.0.0-20190506164543-d2eda7129713/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20190830225923-3302f0226fbd h1:DoaaxHqzWPQCWKSTmsi8UDSiFqxbfue+Xt+qi/BFKb8= +github.com/denisenkom/go-mssqldb v0.0.0-20190830225923-3302f0226fbd/go.mod h1:uU0N10vx1abI4qeVe79CxepBP6PPREVTgMS5Gx6/mOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03 h1:G/9fPivTr5EiyqE9OlW65iMRUxFXMGRHgZFGo50uG8Q= +github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/gin-contrib/multitemplate v0.0.0-20180827023943-5799bbbb6dce h1:KqeVCdb+M2iwyF6GzdYxTazfE1cE+133RXuGaZ5Sc1E= +github.com/gin-contrib/multitemplate v0.0.0-20180827023943-5799bbbb6dce/go.mod h1:62qM8p4crGvNKE413gTzn4eMFin1VOJfMDWMRzHdvqM= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60= +github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA= +github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5 h1:/TjjTS4kg7vC+05gD0LE4+97f/+PRFICnK/7wJPk7kE= +github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/retailcrm/api-client-go v1.1.1 h1:yqsyYjBDdmDwExVlTdGucY9/IpEokXpkfTfA6z5AZ7M= +github.com/retailcrm/api-client-go v1.1.1/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c= +github.com/retailcrm/mg-transport-api-client-go v1.1.31 h1:21pE1JhT49rvbMLDYJa0iiqbb/roz+eSp27fPck4uUw= +github.com/retailcrm/mg-transport-api-client-go v1.1.31/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20171214130843-f21a4dfb5e38/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190830082254-f340ed3ae274 h1:3LEbAKuShoQDlrpbepJOeKph85ROShka+GypY1YNQYQ= +golang.org/x/tools v0.0.0-20190830082254-f340ed3ae274/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/stacktrace.go b/internal/stacktrace.go new file mode 100644 index 0000000..4a9ce49 --- /dev/null +++ b/internal/stacktrace.go @@ -0,0 +1,105 @@ +package internal + +import ( + "runtime" + + "github.com/getsentry/raven-go" + "github.com/pkg/errors" +) + +// NewRavenStackTrace generate stacktrace compatible with raven-go format +// It tries to extract better stacktrace from error from package "github.com/pkg/errors" +// In case of fail it will fallback to default stacktrace generation from raven-go. +// Default stacktrace highly likely will be useless, because it will not include call +// which returned error. This occurs because default stacktrace doesn't include any call +// before stacktrace generation, and raven-go will generate stacktrace here, which will end +// in trace to this file. But errors from "github.com/pkg/errors" will generate stacktrace +// immediately, it will include call which returned error, and we can fetch this trace. +// Also we can wrap default errors with error from this package, like this: +// errors.Wrap(err, err.Error) +func NewRavenStackTrace(client *raven.Client, myerr error, skip int) *raven.Stacktrace { + st := getErrorStackTraceConverted(myerr, 3, client.IncludePaths()) + if st == nil { + st = raven.NewStacktrace(skip, 3, client.IncludePaths()) + } + return st +} + +// getErrorStackTraceConverted will return converted stacktrace from custom error, or nil in case of default error +func getErrorStackTraceConverted(err error, context int, appPackagePrefixes []string) *raven.Stacktrace { + st := getErrorCauseStackTrace(err) + if st == nil { + return nil + } + return convertStackTrace(st, context, appPackagePrefixes) +} + +// getErrorCauseStackTrace tries to extract stacktrace from custom error, returns nil in case of failure +func getErrorCauseStackTrace(err error) errors.StackTrace { + // This code is inspired by github.com/pkg/errors.Cause(). + var st errors.StackTrace + for err != nil { + s := getErrorStackTrace(err) + if s != nil { + st = s + } + err = getErrorCause(err) + } + return st +} + +// convertStackTrace converts github.com/pkg/errors.StackTrace to github.com/getsentry/raven-go.Stacktrace +func convertStackTrace(st errors.StackTrace, context int, appPackagePrefixes []string) *raven.Stacktrace { + // This code is borrowed from github.com/getsentry/raven-go.NewStacktrace(). + var frames []*raven.StacktraceFrame + for _, f := range st { + frame := convertFrame(f, context, appPackagePrefixes) + if frame != nil { + frames = append(frames, frame) + } + } + if len(frames) == 0 { + return nil + } + for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { + frames[i], frames[j] = frames[j], frames[i] + } + return &raven.Stacktrace{Frames: frames} +} + +// convertFrame converts single frame from github.com/pkg/errors.Frame to github.com/pkg/errors.Frame +func convertFrame(f errors.Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame { + // This code is borrowed from github.com/pkg/errors.Frame. + pc := uintptr(f) - 1 + fn := runtime.FuncForPC(pc) + var file string + var line int + if fn != nil { + file, line = fn.FileLine(pc) + } else { + file = "unknown" + } + return raven.NewStacktraceFrame(pc, file, line, context, appPackagePrefixes) +} + +// getErrorStackTrace will try to extract stacktrace from error using StackTrace method (default errors doesn't have it) +func getErrorStackTrace(err error) errors.StackTrace { + ster, ok := err.(interface { + StackTrace() errors.StackTrace + }) + if !ok { + return nil + } + return ster.StackTrace() +} + +// getErrorCause will try to extract original error from wrapper - it is used only if stacktrace is not present +func getErrorCause(err error) error { + cer, ok := err.(interface { + Cause() error + }) + if !ok { + return nil + } + return cer.Cause() +} diff --git a/internal/variables.go b/internal/variables.go new file mode 100644 index 0000000..de9bf97 --- /dev/null +++ b/internal/variables.go @@ -0,0 +1,14 @@ +package internal + +import "regexp" + +// CredentialsTransport set of API methods for transport registration +var ( + CredentialsTransport = []string{ + "/api/integration-modules/{code}", + "/api/integration-modules/{code}/edit", + } + MarkdownSymbols = []string{"*", "_", "`", "["} + RegCommandName = regexp.MustCompile(`^https://?[\da-z.-]+\.(retailcrm\.(ru|pro|es)|ecomlogic\.com|simlachat\.(com|ru))/?$`) + SlashRegex = regexp.MustCompile(`/+$`) +)