diff --git a/can-i-use-bots-api.md b/can-i-use-bots-api.md index f6b8db3..a895860 100644 --- a/can-i-use-bots-api.md +++ b/can-i-use-bots-api.md @@ -81,6 +81,44 @@ In case of `Yes` or `limited` there is a link to documentation where applicable. ? ? + + Messages management + + +   Edit message + ✅ limited + ? + ? + ? + ? + + +   Delete message + ✅ limited + ? + ? + ? + ? + + + Group chats + + +   Can be added to group + ✅ Yes + ❌ no + ? + ? + ? + + +   Group administration + ✅ Yes + ❌ no + ? + ? + ? + Security diff --git a/core/app_context.go b/core/app_context.go index b09b6bc..f93828f 100644 --- a/core/app_context.go +++ b/core/app_context.go @@ -2,6 +2,7 @@ package bots import "github.com/strongo/app" +// BotAppContext is a context for bot app type BotAppContext interface { strongo.AppContext NewBotAppUserEntity() BotAppUser diff --git a/core/app_user.go b/core/app_user.go index ad92b6d..b5ec5ee 100644 --- a/core/app_user.go +++ b/core/app_user.go @@ -2,19 +2,21 @@ package bots import ( "github.com/strongo/app" - "golang.org/x/net/context" + "context" ) //type AppUserIntID int64 +// BotAppUser holds information about bot app user type BotAppUser interface { strongo.AppUser //GetAppUserIntID() int64 - SetBotUserID(platform, botID, botUserId string) + SetBotUserID(platform, botID, botUserID string) } +// BotAppUserStore interface for storing user information to persistent store type BotAppUserStore interface { - GetAppUserByID(c context.Context, appUserId int64, appUser BotAppUser) error - CreateAppUser(c context.Context, botID string, actor WebhookActor) (appUserId int64, appUserEntity BotAppUser, err error) + GetAppUserByID(c context.Context, appUserID int64, appUser BotAppUser) error + CreateAppUser(c context.Context, botID string, actor WebhookActor) (appUserID int64, appUserEntity BotAppUser, err error) //SaveAppUser(c context.Context, appUserId int64, appUserEntity BotAppUser) error } diff --git a/core/bot_chat.go b/core/bot_chat.go index 5e0a6f9..4a8e25c 100644 --- a/core/bot_chat.go +++ b/core/bot_chat.go @@ -1,11 +1,13 @@ package bots import ( - "github.com/satori/go.uuid" - "golang.org/x/net/context" "time" + + "github.com/satori/go.uuid" + "context" ) +// BotChat provides data about bot chat type BotChat interface { GetBotID() string SetBotID(botID string) @@ -40,6 +42,7 @@ type BotChat interface { GetGaClientID() uuid.UUID } +// BotChatStore is interface for DAL to store bot chat data type BotChatStore interface { GetBotChatEntityByID(c context.Context, botID, botChatID string) (BotChat, error) SaveBotChat(c context.Context, botID, botChatID string, chatEntity BotChat) error @@ -47,6 +50,7 @@ type BotChatStore interface { Close(c context.Context) error // TODO: Was io.Closer, should it? } +// NewChatID create a new bot chat ID, returns string func NewChatID(botID, botChatID string) string { return botID + ":" + botChatID } diff --git a/core/bot_user.go b/core/bot_user.go index f49a28d..9585de6 100644 --- a/core/bot_user.go +++ b/core/bot_user.go @@ -1,10 +1,12 @@ package bots import ( - "golang.org/x/net/context" "time" + + "context" ) +// BotUser interface provides information about bot user type BotUser interface { GetAppUserIntID() int64 IsAccessGranted() bool @@ -13,6 +15,7 @@ type BotUser interface { SetDtUpdated(time time.Time) } +// BotUserStore provider to store information about bot user type BotUserStore interface { GetBotUserById(c context.Context, botUserID interface{}) (BotUser, error) SaveBotUser(c context.Context, botUserID interface{}, botUserEntity BotUser) error diff --git a/core/commands.go b/core/commands.go index 2f4a7da..7b4091b 100644 --- a/core/commands.go +++ b/core/commands.go @@ -3,18 +3,27 @@ package bots import ( "fmt" "net/url" + "strings" ) +// CommandAction defines an action bot can perform in response to a command type CommandAction func(whc WebhookContext) (m MessageFromBot, err error) + +// CallbackAction defines a callback action bot can perform in response to a callback command type CallbackAction func(whc WebhookContext, callbackUrl *url.URL) (m MessageFromBot, err error) +// CommandMatcher returns true if action is matched to user input type CommandMatcher func(Command, WebhookContext) bool -const DEFAULT_TITLE = "" -const SHORT_TITLE = "short_title" +// DefaultTitle key +const DefaultTitle = "" // + +// ShortTitle key +const ShortTitle = "short_title" -//const LONG_TITLE = "long_title" +//const LongTitle = "long_title" +// Command defines command metadata and action type Command struct { InputTypes []WebhookInputType // Instant match if != WebhookInputUnknown && == whc.InputTypes() Icon string @@ -29,6 +38,7 @@ type Command struct { CallbackAction CallbackAction } +// NewCallbackCommand create a definition of a callback command func NewCallbackCommand(code string, action CallbackAction) Command { return Command{ Code: code, @@ -41,13 +51,15 @@ func (c Command) String() string { return fmt.Sprintf("Command{Code: '%v', InputTypes: %v, Icon: '%v', Title: '%v', ExactMatch: '%v', len(Command): %v, len(Replies): %v}", c.Code, c.InputTypes, c.Icon, c.Title, c.ExactMatch, len(c.Commands), len(c.Replies)) } +// CommandText returns a title for a command func (whcb *WebhookContextBase) CommandText(title, icon string) string { - if title != "" { + if title != "" && !strings.HasPrefix(title, "/") { title = whcb.Translate(title) } return CommandTextNoTrans(title, icon) } +// CommandTextNoTrans returns a title for a command (pre-translated) func CommandTextNoTrans(title, icon string) string { if title == "" && icon != "" { return icon @@ -60,13 +72,15 @@ func CommandTextNoTrans(title, icon string) string { } } +// DefaultTitle returns a default title for a command in current locale func (c Command) DefaultTitle(whc WebhookContext) string { - return c.TitleByKey(DEFAULT_TITLE, whc) + return c.TitleByKey(DefaultTitle, whc) } +// TitleByKey returns a short/long title for a command in current locale func (c Command) TitleByKey(key string, whc WebhookContext) string { var title string - if key == DEFAULT_TITLE && c.Title != "" { + if key == DefaultTitle && c.Title != "" { title = c.Title } else if val, ok := c.Titles[key]; ok { title = val diff --git a/core/commands_test.go b/core/commands_test.go index 6202879..32219b0 100644 --- a/core/commands_test.go +++ b/core/commands_test.go @@ -7,7 +7,7 @@ import ( var testCmd = Command{ Title: "Title1", Titles: map[string]string{ - SHORT_TITLE: "ttl1", + ShortTitle: "ttl1", }, } @@ -20,7 +20,7 @@ func TestCommand_DefaultTitle(t *testing.T) { } func TestCommand_TitleByKey(t *testing.T) { - if testCmd.TitleByKey(SHORT_TITLE, testWhc) != "ttl1" { + if testCmd.TitleByKey(ShortTitle, testWhc) != "ttl1" { t.Error("Wrong title") } } diff --git a/core/const.go b/core/const.go index c75ae21..5362169 100644 --- a/core/const.go +++ b/core/const.go @@ -2,7 +2,8 @@ package bots import "errors" -var NotImplementedError = errors.New("Not implemented") +// ErrNotImplemented if some feature is not implemented yet +var ErrNotImplemented = errors.New("Not implemented") const ( MESSAGE_TEXT_I_DID_NOT_UNDERSTAND_THE_COMMAND = "MESSAGE_TEXT_I_DID_NOT_UNDERSTAND_THE_COMMAND" diff --git a/core/context.go b/core/context.go index 6ada97a..0347d74 100644 --- a/core/context.go +++ b/core/context.go @@ -1,24 +1,33 @@ package bots import ( + "net/http" + "github.com/strongo/app" "github.com/strongo/db" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" - "net/http" + "github.com/strongo/gamp" + "context" ) +// WebhookInlineQueryContext provides context for inline query (TODO: check & document) type WebhookInlineQueryContext interface { } +type GaQueuer interface {// TODO: can be unexported? + Queue(message gamp.Message) error +} + +// GaContext provides context to Google Analytics type GaContext interface { - GaMeasurement() *measurement.BufferedSender - GaCommon() measurement.Common - GaEvent(category, action string) measurement.Event - GaEventWithLabel(category, action, label string) measurement.Event + GaMeasurement() GaQueuer + GaCommon() gamp.Common + GaEvent(category, action string) gamp.Event + GaEventWithLabel(category, action, label string) gamp.Event } +// WebhookContext provides context for current request from user to bot type WebhookContext interface { + // TODO: Make interface smaller? GaContext db.TransactionCoordinator Environment() strongo.Environment @@ -54,7 +63,6 @@ type WebhookContext interface { NewEditMessage(text string, format MessageFormat) (MessageFromBot, error) //NewEditMessageKeyboard(kbMarkup tgbotapi.InlineKeyboardMarkup) MessageFromBot - GetHttpClient() *http.Client UpdateLastProcessed(chatEntity BotChat) error AppUserIntID() int64 @@ -72,15 +80,18 @@ type WebhookContext interface { Responder() WebhookResponder } +// BotState provides state of the bot (TODO: document how is used) type BotState interface { IsNewerThen(chatEntity BotChat) bool } +// BotInputProvider provides an input from a specific bot interface (Telegram, FB Messenger, Viber, etc.) type BotInputProvider interface { Input() WebhookInput } -type BotApiUser interface { +// BotAPIUser provides info about current bot user +type BotAPIUser interface { //IdAsString() string //IdAsInt64() int64 FirstName() string diff --git a/core/context_auth.go b/core/context_auth.go index 44376ad..f46f6bf 100644 --- a/core/context_auth.go +++ b/core/context_auth.go @@ -3,9 +3,10 @@ package bots import ( "github.com/pkg/errors" "github.com/strongo/log" - "golang.org/x/net/context" + "context" ) +// SetAccessGranted marks current context as authenticated func SetAccessGranted(whc WebhookContext, value bool) (err error) { c := whc.Context() log.Debugf(c, "SetAccessGranted(value=%v)", value) @@ -38,25 +39,23 @@ func SetAccessGranted(whc WebhookContext, value bool) (err error) { log.Debugf(c, "SetAccessGranted(): whc.GetSender().GetID() = %v", botUserID) if botUser, err := whc.GetBotUserById(c, botUserID); err != nil { return errors.Wrapf(err, "Failed to get bot user by id=%v", botUserID) + } else if botUser.IsAccessGranted() == value { + log.Infof(c, "No need to change botUser.AccessGranted, as already is: %v", value) } else { - if botUser.IsAccessGranted() == value { - log.Infof(c, "No need to change botUser.AccessGranted, as already is: %v", value) - } else { - err = whc.RunInTransaction(c, func(c context.Context) error { - botUser.SetAccessGranted(value) - if botUser, err = whc.GetBotUserById(c, botUserID); err != nil { - return errors.Wrapf(err, "Failed to get transactionally bot user by id=%v", botUserID) - } - if changed := botUser.SetAccessGranted(value); changed { - if err = whc.SaveBotUser(c, botUserID, botUser); err != nil { - err = errors.Wrapf(err, "Failed to call whc.SaveBotUser(botUserID=%v)", botUserID) - } + err = whc.RunInTransaction(c, func(c context.Context) error { + botUser.SetAccessGranted(value) + if botUser, err = whc.GetBotUserById(c, botUserID); err != nil { + return errors.Wrapf(err, "Failed to get transactionally bot user by id=%v", botUserID) + } + if changed := botUser.SetAccessGranted(value); changed { + if err = whc.SaveBotUser(c, botUserID, botUser); err != nil { + err = errors.Wrapf(err, "Failed to call whc.SaveBotUser(botUserID=%v)", botUserID) } - return err - }, nil) - } - return err + } + return err + }, nil) } + return err //return SetAccessGrantedForAllUserChats(whc, whc.BotUserKey, value) // TODO: Call in deferrer } diff --git a/core/context_base.go b/core/context_base.go index 2e52de6..7381395 100644 --- a/core/context_base.go +++ b/core/context_base.go @@ -2,21 +2,23 @@ package bots import ( "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + "github.com/pkg/errors" "github.com/strongo/app" "github.com/strongo/db" "github.com/strongo/log" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" + "github.com/strongo/gamp" + "context" "google.golang.org/appengine" "google.golang.org/appengine/datastore" - "net/http" - "net/url" - "strconv" - "strings" - "time" ) +// WebhookContextBase provides base implementation of WebhookContext interface type WebhookContextBase struct { //w http.ResponseWriter r *http.Request @@ -43,33 +45,39 @@ type WebhookContextBase struct { BotCoreStores - gaMeasurement *measurement.BufferedSender + gaMeasurement GaQueuer } func (whcb *WebhookContextBase) SetChatID(v string) { whcb.chatID = v } +// LogRequest logs request data to logging system func (whcb *WebhookContextBase) LogRequest() { whcb.input.LogRequest() } +// RunInTransaction starts a transaction. This needed to coordinate application & framework changes. func (whcb *WebhookContextBase) RunInTransaction(c context.Context, f func(c context.Context) error, options db.RunOptions) error { return whcb.BotContext.BotHost.DB().RunInTransaction(c, f, options) } +// IsInTransaction detects if request is withing a transaction func (whcb *WebhookContextBase) IsInTransaction(c context.Context) bool { return whcb.BotContext.BotHost.DB().IsInTransaction(c) } +// NonTransactionalContext creates a non transaction context for operations that needs to be executed outside of transaction. func (whcb *WebhookContextBase) NonTransactionalContext(tc context.Context) context.Context { return whcb.BotContext.BotHost.DB().NonTransactionalContext(tc) } +// Request returns reference to current HTTP request func (whcb *WebhookContextBase) Request() *http.Request { return whcb.r } +// Environment defines current environment (PROD, DEV, LOCAL, etc) func (whcb *WebhookContextBase) Environment() strongo.Environment { return whcb.BotContext.BotSettings.Env } @@ -111,11 +119,11 @@ func (whcb *WebhookContextBase) BotChatID() (botChatID string, err error) { callbackQuery := input.(WebhookCallbackQuery) data := callbackQuery.GetData() if strings.Contains(data, "chat=") { - if values, err := url.ParseQuery(data); err != nil { + values, err := url.ParseQuery(data) + if err != nil { return "", errors.WithMessage(err, "Failed to GetData() from webhookInput.InputCallbackQuery()") - } else { - whcb.chatID = values.Get("chat") } + whcb.chatID = values.Get("chat") } case WebhookInlineQuery: // pass @@ -129,10 +137,12 @@ func (whcb *WebhookContextBase) BotChatID() (botChatID string, err error) { return whcb.chatID, nil } +// AppUserStrID return current app user ID as a string func (whcb *WebhookContextBase) AppUserStrID() string { return strconv.FormatInt(whcb.AppUserIntID(), 10) } +// AppUserIntID return current app user ID as integer func (whcb *WebhookContextBase) AppUserIntID() (appUserIntID int64) { if !whcb.isInGroup() { if chatEntity := whcb.ChatEntity(); chatEntity != nil { @@ -150,6 +160,7 @@ func (whcb *WebhookContextBase) AppUserIntID() (appUserIntID int64) { return } +// GetAppUser loads information about current app user from persistent storage func (whcb *WebhookContextBase) GetAppUser() (BotAppUser, error) { // TODO: Can/should this be cached? appUserID := whcb.AppUserIntID() appUser := whcb.BotAppContext().NewBotAppUserEntity() @@ -157,14 +168,17 @@ func (whcb *WebhookContextBase) GetAppUser() (BotAppUser, error) { // TODO: Can/ return appUser, err } +// ExecutionContext returns an execution context for strongo app func (whcb *WebhookContextBase) ExecutionContext() strongo.ExecutionContext { return whcb } +// BotAppContext returns bot app context func (whcb *WebhookContextBase) BotAppContext() BotAppContext { return whcb.botAppContext } +// IsInGroup signals if the bot request is send within group chat func (whcb *WebhookContextBase) IsInGroup() bool { return whcb.isInGroup() } @@ -176,7 +190,7 @@ func NewWebhookContextBase( botContext BotContext, webhookInput WebhookInput, botCoreStores BotCoreStores, - gaMeasurement *measurement.BufferedSender, + gaMeasurement GaQueuer, isInGroup func() bool, getLocaleAndChatID func(c context.Context) (locale, chatID string, err error), ) *WebhookContextBase { @@ -235,14 +249,14 @@ func (whcb *WebhookContextBase) InputType() WebhookInputType { return whcb.input.InputType() } -func (whcb *WebhookContextBase) GaMeasurement() *measurement.BufferedSender { +func (whcb *WebhookContextBase) GaMeasurement() GaQueuer { return whcb.gaMeasurement } -func (whcb *WebhookContextBase) GaCommon() measurement.Common { +func (whcb *WebhookContextBase) GaCommon() gamp.Common { if whcb.chatEntity != nil { c := whcb.Context() - return measurement.Common{ + return gamp.Common{ UserID: strconv.FormatInt(whcb.chatEntity.GetAppUserIntID(), 10), UserLanguage: strings.ToLower(whcb.chatEntity.GetPreferredLanguage()), ClientID: whcb.chatEntity.GetGaClientID().String(), @@ -251,18 +265,18 @@ func (whcb *WebhookContextBase) GaCommon() measurement.Common { DataSource: "bot", } } - return measurement.Common{ + return gamp.Common{ DataSource: "bot", ClientID: "c7ea15eb-3333-4d47-a002-9d1a14996371", } } -func (whcb *WebhookContextBase) GaEvent(category, action string) measurement.Event { - return measurement.NewEvent(category, action, whcb.GaCommon()) +func (whcb *WebhookContextBase) GaEvent(category, action string) gamp.Event { + return gamp.NewEvent(category, action, whcb.GaCommon()) } -func (whcb *WebhookContextBase) GaEventWithLabel(category, action, label string) measurement.Event { - return measurement.NewEventWithLabel(category, action, label, whcb.GaCommon()) +func (whcb *WebhookContextBase) GaEventWithLabel(category, action, label string) gamp.Event { + return gamp.NewEventWithLabel(category, action, label, whcb.GaCommon()) } func (whcb *WebhookContextBase) BotPlatform() BotPlatform { @@ -289,9 +303,9 @@ func (whcb *WebhookContextBase) TranslateNoWarning(key string, args ...interface return whcb.Translator.TranslateNoWarning(key, whcb.locale.Code5, args...) } -func (whcb *WebhookContextBase) GetHttpClient() *http.Client { - return whcb.BotContext.BotHost.GetHttpClient(whcb.c) -} +//func (whcb *WebhookContextBase) GetHttpClient() *http.Client { +// return whcb.BotContext.BotHost.GetHttpClient(whcb.c) +//} func (whcb *WebhookContextBase) HasChatEntity() bool { return whcb.chatEntity != nil @@ -348,9 +362,9 @@ func (whcb *WebhookContextBase) GetOrCreateBotUserEntityBase() (BotUser, error) whcb.gaMeasurement.Queue(whcb.GaEventWithLabel("users", "messenger-linked", whcb.botPlatform.Id())) // TODO: Should be outside if whcb.GetBotSettings().Env == strongo.EnvProduction { - gaEvent := measurement.NewEvent("bot-users", "bot-user-created", whcb.GaCommon()) + gaEvent := gamp.NewEvent("bot-users", "bot-user-created", whcb.GaCommon()) gaEvent.Label = whcb.botPlatform.Id() - whcb.GaMeasurement().Queue(gaEvent) + whcb.gaMeasurement.Queue(gaEvent) } } else { log.Infof(c, "Found existing bot user entity") @@ -390,7 +404,7 @@ func (whcb *WebhookContextBase) loadChatEntityBase() error { botChatEntity = whcb.BotChatStore.NewBotChatEntity(c, whcb.GetBotCode(), whcb.input.Chat(), botUser.GetAppUserIntID(), botChatID, botUser.IsAccessGranted()) if whcb.GetBotSettings().Env == strongo.EnvProduction { - gaEvent := measurement.NewEvent("bot-chats", "bot-chat-created", whcb.GaCommon()) + gaEvent := gamp.NewEvent("bot-chats", "bot-chat-created", whcb.GaCommon()) gaEvent.Label = whcb.botPlatform.Id() whcb.GaMeasurement().Queue(gaEvent) } @@ -413,10 +427,12 @@ func (whcb *WebhookContextBase) loadChatEntityBase() error { return err } +// AppUserEntity current app user entity from data storage func (whcb *WebhookContextBase) AppUserEntity() BotAppUser { return whcb.appUser } +// Context for current request func (whcb *WebhookContextBase) Context() context.Context { return whcb.c } diff --git a/core/context_new.go b/core/context_new.go index 7ad9a8c..4788be8 100644 --- a/core/context_new.go +++ b/core/context_new.go @@ -1,5 +1,6 @@ package bots +// WebhookNewContext TODO: needs to be checked & described type WebhookNewContext struct { BotContext WebhookInput diff --git a/core/context_test.go b/core/context_test.go index 55df82b..0dd43bc 100644 --- a/core/context_test.go +++ b/core/context_test.go @@ -2,11 +2,12 @@ package bots import ( "fmt" - "github.com/strongo/app" - "golang.org/x/net/context" - "google.golang.org/appengine/datastore" "net/http" "time" + + "github.com/strongo/app" + "context" + "google.golang.org/appengine/datastore" ) type TestWebhookContext struct { @@ -23,7 +24,7 @@ func (whc TestWebhookContext) IsInGroup() bool { return false } -func (tc TestWebhookContext) Close(c context.Context) error { +func (whc TestWebhookContext) Close(c context.Context) error { return nil } @@ -43,7 +44,7 @@ func (whc TestWebhookContext) GetBotToken() string { panic("Not implemented") } -func (whc TestWebhookContext) GetBotUserById(c context.Context, botUserId interface{}) (BotUser, error) { +func (whc TestWebhookContext) GetBotUserByID(c context.Context, botUserID interface{}) (BotUser, error) { panic("Not implemented") } @@ -132,7 +133,6 @@ func (whc TestWebhookContext) Responder() WebhookResponder { panic("Not implemented") } -func (whc TestWebhookContext) GetHttpClient() *http.Client { panic("Not implemented") } func (whc TestWebhookContext) IsNewerThen(chatEntity BotChat) bool { panic("Not implemented") } func (whc TestWebhookContext) UpdateLastProcessed(chatEntity BotChat) error { panic("Not implemented") } diff --git a/core/driver.go b/core/driver.go index 518fbb4..f15396d 100644 --- a/core/driver.go +++ b/core/driver.go @@ -4,25 +4,27 @@ import ( "bytes" "encoding/json" "fmt" + "net/http" + "runtime/debug" + "strings" + "time" + "github.com/DebtsTracker/translations/emoji" "github.com/julienschmidt/httprouter" "github.com/pkg/errors" "github.com/strongo/app" "github.com/strongo/log" - "github.com/strongo/measurement-protocol" - "net/http" - "runtime/debug" - "strings" - "time" + "github.com/strongo/gamp" ) -// The driver is doing initial request & final response processing -// That includes logging, creating input messages in a general format, sending response +// WebhookDriver is doing initial request & final response processing. +// That includes logging, creating input messages in a general format, sending response. type WebhookDriver interface { RegisterWebhookHandlers(httpRouter *httprouter.Router, pathPrefix string, webhookHandlers ...WebhookHandler) HandleWebhook(w http.ResponseWriter, r *http.Request, webhookHandler WebhookHandler) } +// BotDriver keeps information about bots and map requests to appropriate handlers type BotDriver struct { Analytics AnalyticsSettings botHost BotHost @@ -33,11 +35,13 @@ type BotDriver struct { var _ WebhookDriver = (*BotDriver)(nil) // Ensure BotDriver is implementing interface WebhookDriver +// AnalyticsSettings keeps data for Google Analytics type AnalyticsSettings struct { GaTrackingID string // TODO: Refactor to list of analytics providers Enabled func(r *http.Request) bool } +// NewBotDriver registers new bot driver (TODO: describe why we need it) func NewBotDriver(gaSettings AnalyticsSettings, appContext BotAppContext, host BotHost, panicTextFooter string) WebhookDriver { if appContext.AppUserEntityKind() == "" { panic("appContext.AppUserEntityKind() is empty") @@ -54,14 +58,14 @@ func NewBotDriver(gaSettings AnalyticsSettings, appContext BotAppContext, host B } } -var ErrNotImplemented = errors.New("Not implemented") - +// RegisterWebhookHandlers adds handlers to a bot driver func (d BotDriver) RegisterWebhookHandlers(httpRouter *httprouter.Router, pathPrefix string, webhookHandlers ...WebhookHandler) { for _, webhookHandler := range webhookHandlers { webhookHandler.RegisterWebhookHandler(d, d.botHost, httpRouter, pathPrefix) } } +// HandleWebhook takes and HTTP request and process it func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhookHandler WebhookHandler) { started := time.Now() c := d.botHost.Context(r) @@ -79,7 +83,7 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook botContext, entriesWithInputs, err := webhookHandler.GetBotContextAndInputs(c, r) if err != nil { - if _, ok := err.(AuthFailedError); ok { + if _, ok := err.(ErrAuthFailed); ok { log.Warningf(c, "Auth failed: %v", err) http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) } else if errors.Cause(err) == ErrNotImplemented { @@ -114,8 +118,7 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook log.Debugf(c, "BotDriver.HandleWebhook() => botCode=%v, len(entriesWithInputs): %d", botContext.BotSettings.Code, len(entriesWithInputs)) - env := botContext.BotSettings.Env - switch env { + switch botContext.BotSettings.Env { case strongo.EnvLocal: if r.Host != "localhost" && !strings.HasSuffix(r.Host, ".ngrok.io") { log.Warningf(c, "whc.GetBotSettings().Mode == Local, host: %v", r.Host) @@ -129,13 +132,14 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook return } } - + var ( - whc WebhookContext // TODO: How do deal with Facebook multiple entries per request? - gaMeasurement *measurement.BufferedSender + whc WebhookContext // TODO: How do deal with Facebook multiple entries per request? + measurementSender *gamp.BufferedClient ) + + var sendStats bool { // Initiate Google Analytics Measurement API client - var sendStats bool if d.Analytics.Enabled == nil { sendStats = botContext.BotSettings.Env == strongo.EnvProduction //log.Debugf(c, "d.AnalyticsSettings.Enabled == nil, botContext.BotSettings.Env: %v, sendStats: %v", strongo.EnvironmentNames[botContext.BotSettings.Env], sendStats) @@ -144,39 +148,38 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook //log.Debugf(c, "d.AnalyticsSettings.Enabled != nil, sendStats: %v", sendStats) } if sendStats { - trackingID := d.Analytics.GaTrackingID botHost := botContext.BotHost - gaMeasurement = measurement.NewBufferedSender([]string{trackingID}, true, botHost.GetHttpClient(c)) - } else { - gaMeasurement = measurement.NewDiscardingBufferedSender() + measurementSender = gamp.NewBufferedClient("", botHost.GetHttpClient(c), nil) } } defer func() { log.Debugf(c, "driver.deferred(recover) - checking for panic & flush GA") - gaMeasurement.Queue(measurement.NewTiming(time.Now().Sub(started))) - if recovered := recover(); recovered != nil { + if sendStats { + measurementSender.Queue(gamp.NewTiming(time.Now().Sub(started))) + } + + reportError := func(recovered interface{}) { messageText := fmt.Sprintf("Server error (panic): %v\n\n%v", recovered, d.panicTextFooter) log.Criticalf(c, "Panic recovered: %s\n%s", messageText, debug.Stack()) - if gaMeasurement.QueueDepth() > 0 { // Zero if GA is disabled - gaMessage := measurement.NewException(messageText, true) + if sendStats { // Zero if GA is disabled + gaMessage := gamp.NewException(messageText, true) if whc != nil { // TODO: How do deal with Facebook multiple entries per request? gaMessage.Common = whc.GaCommon() } else { - gaMessage.Common.ClientID = "c7ea15eb-3333-4d47-a002-9d1a14996371" - gaMessage.Common.DataSource = "bot" + gaMessage.Common.ClientID = "c7ea15eb-3333-4d47-a002-9d1a14996371" // TODO: move hardcoded value + gaMessage.Common.DataSource = "bot-" + whc.BotPlatform().Id() } - if err := gaMeasurement.Queue(gaMessage); err != nil { + if err := measurementSender.Queue(gaMessage); err != nil { log.Errorf(c, "Failed to queue exception details for GA: %v", err) } else { log.Debugf(c, "Exception details queued for GA.") } - log.Debugf(c, "Flushing gaMeasurement (with exeception, len(queue): %v)...", gaMeasurement.QueueDepth()) - if err = gaMeasurement.Flush(); err != nil { + if err = measurementSender.Flush(); err != nil { log.Errorf(c, "Failed to send exception details to GA: %v", err) } else { log.Debugf(c, "Exception details sent to GA.") @@ -192,11 +195,15 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook } } } - } else if gaQueueDepth := gaMeasurement.QueueDepth(); gaQueueDepth > 0 { // Zero if GA is disabled - if err = gaMeasurement.Flush(); err != nil { - log.Warningf(c, "Failed to send to GA %v items: %v", gaQueueDepth, err) + } + + if recovered := recover(); recovered != nil { + reportError(recovered) + } else if sendStats { + if err = measurementSender.Flush(); err != nil { + log.Warningf(c, "Failed to flush to GA: %v", err) } else { - log.Debugf(c, "Sent to GA: %v items", gaQueueDepth) + log.Debugf(c, "Sent to GA: %v items", measurementSender.QueueDepth()) } } }() @@ -270,7 +277,7 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook panic(fmt.Sprintf("entryWithInputs.Inputs[%d] == nil", i)) } logInput(i, input) - whc = webhookHandler.CreateWebhookContext(d.appContext, r, *botContext, input, botCoreStores, gaMeasurement) + whc = webhookHandler.CreateWebhookContext(d.appContext, r, *botContext, input, botCoreStores, measurementSender) responder := webhookHandler.GetResponder(w, whc) // TODO: Move inside webhookHandler.CreateWebhookContext()? dispatch(responder, whc) } diff --git a/core/errors.go b/core/errors.go index c98e0db..0ecb9f8 100644 --- a/core/errors.go +++ b/core/errors.go @@ -2,12 +2,14 @@ package bots import "errors" -type AuthFailedError string +// ErrAuthFailed raised if authentication failed +type ErrAuthFailed string -func (e AuthFailedError) Error() string { +func (e ErrAuthFailed) Error() string { return string(e) } var ( + // ErrEntityNotFound is returned if entity not found in storage ErrEntityNotFound = errors.New("bots-framework: no such entity") ) diff --git a/core/handler.go b/core/handler.go index 60a2f3c..eb9203b 100644 --- a/core/handler.go +++ b/core/handler.go @@ -1,18 +1,19 @@ package bots import ( - "github.com/julienschmidt/httprouter" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" "net/http" + + "github.com/julienschmidt/httprouter" + "context" ) +// WebhookHandler handles requests from a specific bot API type WebhookHandler interface { RegisterWebhookHandler(driver WebhookDriver, botHost BotHost, router *httprouter.Router, pathPrefix string) HandleWebhookRequest(w http.ResponseWriter, r *http.Request, params httprouter.Params) GetBotContextAndInputs(c context.Context, r *http.Request) (botContext *BotContext, entriesWithInputs []EntryInputs, err error) CreateBotCoreStores(appContext BotAppContext, r *http.Request) BotCoreStores - CreateWebhookContext(appContext BotAppContext, r *http.Request, botContext BotContext, webhookInput WebhookInput, botCoreStores BotCoreStores, gaMeasurement *measurement.BufferedSender) WebhookContext //TODO: Can we get rid of http.Request? Needed for botHost.GetHttpClient() + CreateWebhookContext(appContext BotAppContext, r *http.Request, botContext BotContext, webhookInput WebhookInput, botCoreStores BotCoreStores, gaMeasurement GaQueuer) WebhookContext //TODO: Can we get rid of http.Request? Needed for botHost.GetHttpClient() GetResponder(w http.ResponseWriter, whc WebhookContext) WebhookResponder //ProcessInput(input WebhookInput, entry *WebhookEntry) } diff --git a/core/interfaces.go b/core/interfaces.go index 1b23bde..b287eb8 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -1,17 +1,20 @@ package bots import ( - "github.com/strongo/db" - "golang.org/x/net/context" "net/http" "time" + + "github.com/strongo/db" + "context" ) +// BotPlatform describes current bot platform type BotPlatform interface { Id() string Version() string } +// BotHost describes current bot app host environment type BotHost interface { Context(r *http.Request) context.Context GetHttpClient(c context.Context) *http.Client @@ -19,12 +22,14 @@ type BotHost interface { DB() db.Database } +// BotContext describes current bot app host & settings type BotContext struct { // TODO: Rename to BotWebhookContext or just WebhookContext (replace old one) BotHost BotHost BotSettings BotSettings } +// NewBotContext creates current bot host & settings func NewBotContext(host BotHost, settings BotSettings) *BotContext { if settings.Code == "" { panic("ReferredTo settings.Code is empty string") diff --git a/core/misc.go b/core/misc.go index a193974..449da3b 100644 --- a/core/misc.go +++ b/core/misc.go @@ -1,15 +1,18 @@ package bots import ( - "github.com/julienschmidt/httprouter" "net/http" + + "github.com/julienschmidt/httprouter" ) +// PingHandler returns 'Pong' back to user func PingHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Write([]byte("Pong")) } +// NotFoundHandler returns HTTP status code 404 func NotFoundHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } diff --git a/core/test_mocks.go b/core/mocks_test.go similarity index 93% rename from core/test_mocks.go rename to core/mocks_test.go index 3de8500..2c2e8e5 100644 --- a/core/test_mocks.go +++ b/core/mocks_test.go @@ -2,9 +2,10 @@ package bots import ( "fmt" - "github.com/strongo/log" - "golang.org/x/net/context" "testing" + + "github.com/strongo/log" + "context" ) type MockLogger struct { @@ -13,7 +14,7 @@ type MockLogger struct { Infos []string } -func (_ *MockLogger) Name() string { +func (*MockLogger) Name() string { return "MockLogger" } diff --git a/core/plugs.go b/core/plugs.go index 9fe0d25..547129c 100644 --- a/core/plugs.go +++ b/core/plugs.go @@ -4,6 +4,7 @@ import ( "net/url" ) +// IgnoreCommand is a command that does nothing var IgnoreCommand = Command{ Code: "bots.IgnoreCommand", Action: func(_ WebhookContext) (m MessageFromBot, err error) { diff --git a/core/router.go b/core/router.go index e309a0e..ca40583 100644 --- a/core/router.go +++ b/core/router.go @@ -3,15 +3,17 @@ package bots import ( "fmt" //"net/http" + "net/url" + "strings" + "github.com/DebtsTracker/translations/emoji" "github.com/pkg/errors" "github.com/strongo/app" "github.com/strongo/log" - "github.com/strongo/measurement-protocol" - "net/url" - "strings" + "github.com/strongo/gamp" ) +// TypeCommands container for commands type TypeCommands struct { all []Command byCode map[string]Command @@ -36,11 +38,13 @@ func (v *TypeCommands) addCommand(command Command, commandType WebhookInputType) } } +// WebhooksRouter maps routes to commands type WebhooksRouter struct { commandsByType map[WebhookInputType]*TypeCommands errorFooterText func() string } +// NewWebhookRouter creates new router func NewWebhookRouter(commandsByType map[WebhookInputType][]Command, errorFooterText func() string) WebhooksRouter { r := WebhooksRouter{ commandsByType: make(map[WebhookInputType]*TypeCommands, len(commandsByType)), @@ -56,6 +60,7 @@ func NewWebhookRouter(commandsByType map[WebhookInputType][]Command, errorFooter return r } +// AddCommands add commands to a router func (router *WebhooksRouter) AddCommands(commandsType WebhookInputType, commands []Command) { typeCommands, ok := router.commandsByType[commandsType] if !ok { @@ -75,6 +80,7 @@ func (router *WebhooksRouter) AddCommands(commandsType WebhookInputType, command } } +// RegisterCommands is registering commands with router func (router *WebhooksRouter) RegisterCommands(commands []Command) { addCommand := func(t WebhookInputType, command Command) { typeCommands, ok := router.commandsByType[t] @@ -100,16 +106,16 @@ func (router *WebhooksRouter) RegisterCommands(commands []Command) { } } -func matchCallbackCommands(whc WebhookContext, input WebhookCallbackQuery, typeCommands *TypeCommands) (matchedCommand *Command, callbackUrl *url.URL, err error) { +func matchCallbackCommands(whc WebhookContext, input WebhookCallbackQuery, typeCommands *TypeCommands) (matchedCommand *Command, callbackURL *url.URL, err error) { if len(typeCommands.all) > 0 { callbackData := input.GetData() - callbackUrl, err = url.Parse(callbackData) + callbackURL, err = url.Parse(callbackData) if err != nil { log.Errorf(whc.Context(), "Failed to parse callback data to URL: %v", err.Error()) } else { - callbackPath := callbackUrl.Path + callbackPath := callbackURL.Path if command, ok := typeCommands.byCode[callbackPath]; ok { - return &command, callbackUrl, nil + return &command, callbackURL, nil } } if err == nil && matchedCommand == nil { @@ -119,7 +125,7 @@ func matchCallbackCommands(whc WebhookContext, input WebhookCallbackQuery, typeC } else { panic("len(typeCommands.all) == 0") } - return nil, callbackUrl, err + return nil, callbackURL, err } func (router *WebhooksRouter) matchMessageCommands(whc WebhookContext, input WebhookMessage, parentPath string, commands []Command) (matchedCommand *Command) { @@ -188,7 +194,7 @@ func (router *WebhooksRouter) matchMessageCommands(whc WebhookContext, input Web log.Debugf(c, "%v matched by command.FullName()", command.Code) matchedCommand = &command return - } else { + // } else { //log.Debugf(c, "command(code=%v).Title(whc): %v", command.ByCode, command.DefaultTitle(whc)) } if command.Matcher != nil && command.Matcher(command, whc) { @@ -223,10 +229,12 @@ func (router *WebhooksRouter) matchMessageCommands(whc WebhookContext, input Web return } +// DispatchInlineQuery dispatches inlines query func (router *WebhooksRouter) DispatchInlineQuery(responder WebhookResponder) { } +// Dispatch query to commands func (router *WebhooksRouter) Dispatch(responder WebhookResponder, whc WebhookContext) { c := whc.Context() //defer func() { @@ -254,8 +262,8 @@ func (router *WebhooksRouter) Dispatch(responder WebhookResponder, whc WebhookCo input := whc.Input() switch input.(type) { case WebhookCallbackQuery: - var callbackUrl *url.URL - matchedCommand, callbackUrl, err = matchCallbackCommands(whc, input.(WebhookCallbackQuery), typeCommands) + var callbackURL *url.URL + matchedCommand, callbackURL, err = matchCallbackCommands(whc, input.(WebhookCallbackQuery), typeCommands) if err == nil && matchedCommand != nil { if matchedCommand.Code == "" { err = fmt.Errorf("matchedCommand(%T: %v).ByCode is empty string", matchedCommand, matchedCommand) @@ -264,7 +272,7 @@ func (router *WebhooksRouter) Dispatch(responder WebhookResponder, whc WebhookCo } else { log.Debugf(c, "matchCallbackCommands() => matchedCommand: %T(code=%v)", matchedCommand, matchedCommand.Code) commandAction = func(whc WebhookContext) (MessageFromBot, error) { - return matchedCommand.CallbackAction(whc, callbackUrl) + return matchedCommand.CallbackAction(whc, callbackURL) } } } @@ -378,16 +386,16 @@ func (router *WebhooksRouter) processCommandResponse(matchedCommand *Command, re inputType := whc.InputType() if err == nil { if _, err = responder.SendMessage(c, m, BotApiSendMessageOverHTTPS); err != nil { - const FAILED_TO_SEND_MESSAGE_TO_MESSENGER = "failed to send a message to messenger" + const failedToSendMessageToMessenger = "failed to send a message to messenger" if strings.Contains(err.Error(), "message is not modified") { // TODO: This check is specific to Telegram and should be abstracted - logText := FAILED_TO_SEND_MESSAGE_TO_MESSENGER + logText := failedToSendMessageToMessenger if inputType == WebhookInputCallbackQuery { logText += "(can be duplicate callback)" } log.Warningf(c, errors.WithMessage(err, logText).Error()) // TODO: Think how to get rid of warning on duplicate callbacks when users clicks multiple times err = nil } else { - log.Errorf(c, errors.WithMessage(err, FAILED_TO_SEND_MESSAGE_TO_MESSENGER).Error()) //TODO: Decide how do we handle this + log.Errorf(c, errors.WithMessage(err, failedToSendMessageToMessenger).Error()) //TODO: Decide how do we handle this } } if matchedCommand != nil { @@ -395,7 +403,7 @@ func (router *WebhooksRouter) processCommandResponse(matchedCommand *Command, re gaHostName := fmt.Sprintf("%v.debtstracker.io", strings.ToLower(whc.BotPlatform().Id())) pathPrefix := "bot/" - var pageview measurement.Pageview + var pageview gamp.Pageview var chatEntity BotChat if inputType != WebhookInputCallbackQuery { chatEntity = whc.ChatEntity() @@ -404,17 +412,16 @@ func (router *WebhooksRouter) processCommandResponse(matchedCommand *Command, re path := chatEntity.GetAwaitingReplyTo() if path == "" { path = matchedCommand.Code - } else if pathUrl, err := url.Parse(path); err == nil { - path = pathUrl.Path + } else if pathURL, err := url.Parse(path); err == nil { + path = pathURL.Path } - pageview = measurement.NewPageviewWithDocumentHost(gaHostName, pathPrefix+path, matchedCommand.Title) + pageview = gamp.NewPageviewWithDocumentHost(gaHostName, pathPrefix+path, matchedCommand.Title) } else { - pageview = measurement.NewPageviewWithDocumentHost(gaHostName, pathPrefix+WebhookInputTypeNames[inputType], matchedCommand.Title) + pageview = gamp.NewPageviewWithDocumentHost(gaHostName, pathPrefix+WebhookInputTypeNames[inputType], matchedCommand.Title) } pageview.Common = whc.GaCommon() - err := gaMeasurement.Queue(pageview) - if err != nil { + if err := gaMeasurement.Queue(pageview); err != nil { log.Warningf(c, "Failed to send page view to GA: %v", err) } } @@ -422,7 +429,7 @@ func (router *WebhooksRouter) processCommandResponse(matchedCommand *Command, re } else { log.Errorf(c, err.Error()) if env == strongo.EnvProduction && gaMeasurement != nil { - exceptionMessage := measurement.NewException(err.Error(), false) + exceptionMessage := gamp.NewException(err.Error(), false) exceptionMessage.Common = whc.GaCommon() err = gaMeasurement.Queue(exceptionMessage) if err != nil { diff --git a/core/settings.go b/core/settings.go index 40e79ff..a79d526 100644 --- a/core/settings.go +++ b/core/settings.go @@ -2,10 +2,12 @@ package bots import ( "fmt" + "github.com/strongo/app" - "golang.org/x/net/context" + "context" ) +// BotSettings keeps parameters of a bot type BotSettings struct { Env strongo.Environment ID string @@ -19,6 +21,7 @@ type BotSettings struct { Router WebhooksRouter } +// NewBotSettings configures bot application func NewBotSettings(mode strongo.Environment, profile, code, id, token string, locale strongo.Locale) BotSettings { if profile == "" { panic("Missing required parameter: profile") @@ -42,23 +45,26 @@ func NewBotSettings(mode strongo.Environment, profile, code, id, token string, l } } +// SettingsProvider returns settings per different keys (ID, code, API token, locale) type SettingsProvider func(c context.Context) SettingsBy +// SettingsBy keeps settings per different keys (ID, code, API token, locale) type SettingsBy struct { // TODO: Decide if it should have map[string]*BotSettings instead of map[string]BotSettings ByCode map[string]BotSettings - ByApiToken map[string]BotSettings + ByAPIToken map[string]BotSettings ByLocale map[string][]BotSettings ByID map[string]BotSettings HasRouter bool } +// NewBotSettingsBy create settings per different keys (ID, code, API token, locale) func NewBotSettingsBy(router func(profile string) WebhooksRouter, bots ...BotSettings) SettingsBy { count := len(bots) settingsBy := SettingsBy{ HasRouter: router != nil, ByCode: make(map[string]BotSettings, count), - ByApiToken: make(map[string]BotSettings, count), + ByAPIToken: make(map[string]BotSettings, count), ByLocale: make(map[string][]BotSettings, count), ByID: make(map[string]BotSettings, count), } @@ -71,10 +77,10 @@ func NewBotSettingsBy(router func(profile string) WebhooksRouter, bots ...BotSet } else { settingsBy.ByCode[bot.Code] = bot } - if _, ok := settingsBy.ByApiToken[bot.Token]; ok { + if _, ok := settingsBy.ByAPIToken[bot.Token]; ok { panic(fmt.Sprintf("Bot with duplicate token: %v", bot.Token)) } else { - settingsBy.ByApiToken[bot.Token] = bot + settingsBy.ByAPIToken[bot.Token] = bot } if bot.ID != "" { if _, ok := settingsBy.ByID[bot.ID]; ok { diff --git a/core/structs.go b/core/structs.go index f96b490..3b631ae 100644 --- a/core/structs.go +++ b/core/structs.go @@ -3,24 +3,29 @@ package bots //go:generate ffjson $GOFILE import ( + "strconv" + "github.com/strongo/app" "github.com/strongo/bots-api-fbm" - "golang.org/x/net/context" - "strconv" + "context" ) +// EntryInputs provides information on parsed inputs from bot API request type EntryInputs struct { Entry WebhookEntry Inputs []WebhookInput } +// EntryInput provides information on parsed input from bot API request type EntryInput struct { Entry WebhookEntry Input WebhookInput } +// TranslatorProvider translates texts type TranslatorProvider func(c context.Context) strongo.Translator +// BaseHandler provides base implemnetation of a bot handler type BaseHandler struct { WebhookDriver BotHost @@ -28,6 +33,7 @@ type BaseHandler struct { TranslatorProvider TranslatorProvider } +// Register driver func (bh *BaseHandler) Register(d WebhookDriver, h BotHost) { if d == nil { panic("WebhookDriver == nil") @@ -39,74 +45,116 @@ func (bh *BaseHandler) Register(d WebhookDriver, h BotHost) { bh.BotHost = h } +// MessageFormat specify formatting of a text message to BOT (e.g. Text, HTML, MarkDown) type MessageFormat int const ( + // MessageFormatText is for text messages MessageFormatText MessageFormat = iota + // MessageFormatHTML is for HTML messages MessageFormatHTML + // MessageFormatMarkdown is for markdown messages MessageFormatMarkdown ) +// NoMessageToSend returned explicitly if we don't want to reply to user intput const NoMessageToSend = "" +// ChatUID returns chat ID as unique string type ChatUID interface { ChatUID() string } +// ChatIntID returns chat ID as unique integer type ChatIntID int64 +// ChatUID returns chat ID as unique string func (chatUID ChatIntID) ChatUID() string { return strconv.FormatInt(int64(chatUID), 10) } +// MessageUID is unique message ID as string type MessageUID interface { UID() string } +// KeyboardType defines keyboard type type KeyboardType int const ( + // KeyboardTypeNone for no keyboard KeyboardTypeNone KeyboardType = iota + + // KeyboardTypeHide commands to hide keyboard KeyboardTypeHide + + // KeyboardTypeInline for inline keyboard KeyboardTypeInline + + // KeyboardTypeBottom for bottom keyboard KeyboardTypeBottom + + // KeyboardTypeForceReply to force reply from a user KeyboardTypeForceReply ) +//Keyboard defines keyboard type Keyboard interface { + // KeyboardType defines keyboard type KeyboardType() KeyboardType } +// AttachmentType to a bot message type AttachmentType int const ( + // AttachmentTypeNone says there is no attachment AttachmentTypeNone AttachmentType = iota + + // AttachmentTypeAudio is for audio attachments AttachmentTypeAudio + + // AttachmentTypeFile is for file attachments AttachmentTypeFile + + // AttachmentTypeImage is for image attachments AttachmentTypeImage + + // AttachmentTypeVideo is for video attachments AttachmentTypeVideo ) +// Attachment to a bot message type Attachment interface { AttachmentType() AttachmentType } +// BotMessageType defines type of an output message from bot to user type BotMessageType int const ( + // BotMessageTypeUndefined unknown type BotMessageTypeUndefined BotMessageType = iota + // BotMessageTypeCallbackAnswer sends callback answer BotMessageTypeCallbackAnswer + // BotMessageTypeInlineResults sends inline results BotMessageTypeInlineResults + // BotMessageTypeText sends text reply BotMessageTypeText + // BotMessageTypeEditMessage edit previously sent message BotMessageTypeEditMessage + // BotMessageTypeLeaveChat commands messenger to kick off bot from a chat BotMessageTypeLeaveChat + // BotMessageTypeExportChatInviteLink sends invite link BotMessageTypeExportChatInviteLink ) +// BotMessage is an output message from bot to user type BotMessage interface { BotMessageType() BotMessageType } +// TextMessageFromBot is a text output message from bot to user type TextMessageFromBot struct { Text string `json:",omitempty"` Format MessageFormat `json:",omitempty"` @@ -117,6 +165,7 @@ type TextMessageFromBot struct { EditMessageUID MessageUID `json:",omitempty"` } +// BotMessageType returns if we want to send a new message or edit existing one func (m TextMessageFromBot) BotMessageType() BotMessageType { if m.IsEdit { return BotMessageTypeEditMessage @@ -126,6 +175,7 @@ func (m TextMessageFromBot) BotMessageType() BotMessageType { var _ BotMessage = (*TextMessageFromBot)(nil) +// MessageFromBot keeps all the details of answer from bot to user type MessageFromBot struct { ToChat ChatUID `json:",omitempty"` TextMessageFromBot // This is a shortcut to MessageFromBot{}.BotMessage = TextMessageFromBot{text: "abc"} diff --git a/hosts/appengine/fbm_store_chat.go b/hosts/appengine/fbm_store_chat.go index 1038bba..bc0fe7c 100644 --- a/hosts/appengine/fbm_store_chat.go +++ b/hosts/appengine/fbm_store_chat.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/strongo/bots-framework/core" "github.com/strongo/bots-framework/platforms/fbm" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" ) diff --git a/hosts/appengine/fbm_store_user.go b/hosts/appengine/fbm_store_user.go index c6d1d6c..8eab5ab 100644 --- a/hosts/appengine/fbm_store_user.go +++ b/hosts/appengine/fbm_store_user.go @@ -5,7 +5,7 @@ import ( "github.com/strongo/app/user" "github.com/strongo/bots-framework/core" "github.com/strongo/bots-framework/platforms/fbm" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" "time" ) diff --git a/hosts/appengine/host.go b/hosts/appengine/host.go index 106f5de..362fafd 100644 --- a/hosts/appengine/host.go +++ b/hosts/appengine/host.go @@ -5,7 +5,7 @@ import ( "github.com/strongo/bots-framework/platforms/telegram" "github.com/strongo/db" "github.com/strongo/db/gaedb" - "golang.org/x/net/context" + "context" "google.golang.org/appengine" "google.golang.org/appengine/urlfetch" "net/http" diff --git a/hosts/appengine/logger.go b/hosts/appengine/logger.go index d77fa10..f26801c 100644 --- a/hosts/appengine/logger.go +++ b/hosts/appengine/logger.go @@ -2,7 +2,7 @@ package gae_host import ( "github.com/strongo/log" - "golang.org/x/net/context" + "context" logGae "google.golang.org/appengine/log" ) diff --git a/hosts/appengine/store_app_user.go b/hosts/appengine/store_app_user.go index c8520ed..8894fe0 100644 --- a/hosts/appengine/store_app_user.go +++ b/hosts/appengine/store_app_user.go @@ -5,7 +5,7 @@ import ( "github.com/strongo/bots-framework/core" "github.com/strongo/log" "github.com/strongo/nds" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" "reflect" ) diff --git a/hosts/appengine/store_bot_chat.go b/hosts/appengine/store_bot_chat.go index 86dc497..92c69af 100644 --- a/hosts/appengine/store_bot_chat.go +++ b/hosts/appengine/store_bot_chat.go @@ -5,7 +5,7 @@ import ( "github.com/strongo/bots-framework/core" "github.com/strongo/log" "github.com/strongo/nds" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" "strconv" "time" diff --git a/hosts/appengine/store_bot_user.go b/hosts/appengine/store_bot_user.go index b734579..89b1a73 100644 --- a/hosts/appengine/store_bot_user.go +++ b/hosts/appengine/store_bot_user.go @@ -6,7 +6,7 @@ import ( "github.com/strongo/bots-framework/core" "github.com/strongo/log" "github.com/strongo/nds" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" "time" ) diff --git a/hosts/appengine/telegram_store_chat.go b/hosts/appengine/telegram_store_chat.go index 949e1fe..01119eb 100644 --- a/hosts/appengine/telegram_store_chat.go +++ b/hosts/appengine/telegram_store_chat.go @@ -5,7 +5,7 @@ import ( "github.com/strongo/bots-framework/core" "github.com/strongo/bots-framework/platforms/telegram" "github.com/strongo/nds" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" "strconv" "time" diff --git a/hosts/appengine/telegram_store_user.go b/hosts/appengine/telegram_store_user.go index c30d1a0..5c10302 100644 --- a/hosts/appengine/telegram_store_user.go +++ b/hosts/appengine/telegram_store_user.go @@ -5,7 +5,7 @@ import ( "github.com/strongo/app/user" "github.com/strongo/bots-framework/core" "github.com/strongo/bots-framework/platforms/telegram" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" "time" ) diff --git a/hosts/appengine/tg_chat_instance_dal.go b/hosts/appengine/tg_chat_instance_dal.go index 5bc3337..b5e8298 100644 --- a/hosts/appengine/tg_chat_instance_dal.go +++ b/hosts/appengine/tg_chat_instance_dal.go @@ -6,7 +6,7 @@ import ( "github.com/strongo/bots-framework/platforms/telegram" "github.com/strongo/db" "github.com/strongo/db/gaedb" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" ) diff --git a/hosts/appengine/viber_store_user_chat.go b/hosts/appengine/viber_store_user_chat.go index a1c0d3f..16cb678 100644 --- a/hosts/appengine/viber_store_user_chat.go +++ b/hosts/appengine/viber_store_user_chat.go @@ -5,7 +5,7 @@ import ( "github.com/strongo/bots-framework/core" "github.com/strongo/bots-framework/platforms/viber" "github.com/strongo/nds" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/datastore" "time" ) diff --git a/platforms/fbm/fbm_webhooks.go b/platforms/fbm/fbm_webhooks.go index 2d579fb..01fe8f2 100644 --- a/platforms/fbm/fbm_webhooks.go +++ b/platforms/fbm/fbm_webhooks.go @@ -9,8 +9,7 @@ import ( "github.com/strongo/bots-api-fbm" "github.com/strongo/bots-framework/core" "github.com/strongo/log" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" + "context" "google.golang.org/appengine" "io/ioutil" "net/http" @@ -173,7 +172,7 @@ func (handler FbmWebhookHandler) GetBotContextAndInputs(c context.Context, r *ht return } -func (_ FbmWebhookHandler) CreateWebhookContext(appContext bots.BotAppContext, r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, gaMeasurement *measurement.BufferedSender) bots.WebhookContext { +func (_ FbmWebhookHandler) CreateWebhookContext(appContext bots.BotAppContext, r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, gaMeasurement bots.GaQueuer) bots.WebhookContext { return NewFbmWebhookContext(appContext, r, botContext, webhookInput, botCoreStores, gaMeasurement) } diff --git a/platforms/fbm/webhook_context.go b/platforms/fbm/webhook_context.go index cb44b88..470a3ab 100644 --- a/platforms/fbm/webhook_context.go +++ b/platforms/fbm/webhook_context.go @@ -3,8 +3,7 @@ package fbm_bot import ( "github.com/strongo/bots-api-fbm" "github.com/strongo/bots-framework/core" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" + "context" "net/http" ) @@ -17,7 +16,7 @@ type FbmWebhookContext struct { var _ bots.WebhookContext = (*FbmWebhookContext)(nil) -func NewFbmWebhookContext(appContext bots.BotAppContext, r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, gaMeasurement *measurement.BufferedSender) *FbmWebhookContext { +func NewFbmWebhookContext(appContext bots.BotAppContext, r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, gaMeasurement bots.GaQueuer) *FbmWebhookContext { whcb := bots.NewWebhookContextBase( r, appContext, diff --git a/platforms/fbm/webhook_responder.go b/platforms/fbm/webhook_responder.go index 280b74c..53b53c9 100644 --- a/platforms/fbm/webhook_responder.go +++ b/platforms/fbm/webhook_responder.go @@ -5,7 +5,7 @@ import ( "github.com/strongo/bots-api-fbm" "github.com/strongo/bots-framework/core" "github.com/strongo/log" - "golang.org/x/net/context" + "context" "google.golang.org/appengine/urlfetch" ) diff --git a/platforms/kik/secrets.go b/platforms/kik/secrets.go index 6c6b082..cd60a22 100644 --- a/platforms/kik/secrets.go +++ b/platforms/kik/secrets.go @@ -1,4 +1,4 @@ package kik -const BOT_USERNAME = "debtstracker" -const API_KEY = "1e296a7a-762a-4a00-9152-e9f410cacde1" +const BOT_USERNAME = "some-bot" +const API_KEY = "some-secret" diff --git a/platforms/telegram/dal.go b/platforms/telegram/dal.go index bf74b82..3ab9f24 100644 --- a/platforms/telegram/dal.go +++ b/platforms/telegram/dal.go @@ -2,7 +2,7 @@ package telegram_bot import ( "github.com/strongo/db" - "golang.org/x/net/context" + "context" ) type TgChatInstanceDal interface { diff --git a/platforms/telegram/tg_webhooks.go b/platforms/telegram/tg_webhooks.go index 9dfccc7..92dd7a2 100644 --- a/platforms/telegram/tg_webhooks.go +++ b/platforms/telegram/tg_webhooks.go @@ -7,8 +7,7 @@ import ( "github.com/strongo/bots-api-telegram" "github.com/strongo/bots-framework/core" "github.com/strongo/log" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" + "context" "io/ioutil" "net/http" //"github.com/kylelemons/go-gypsy/yaml" @@ -110,10 +109,10 @@ func (h TelegramWebhookHandler) SetWebhook(c context.Context, w http.ResponseWri func (h TelegramWebhookHandler) GetBotContextAndInputs(c context.Context, r *http.Request) (botContext *bots.BotContext, entriesWithInputs []bots.EntryInputs, err error) { //log.Debugf(c, "TelegramWebhookHandler.GetBotContextAndInputs()") token := r.URL.Query().Get("token") - botSettings, ok := h.botsBy(c).ByApiToken[token] + botSettings, ok := h.botsBy(c).ByAPIToken[token] if !ok { errMess := fmt.Sprintf("Unknown token: [%v]", token) - err = bots.AuthFailedError(errMess) + err = bots.ErrAuthFailed(errMess) return } botContext = bots.NewBotContext(h.BotHost, botSettings) @@ -186,7 +185,7 @@ func (h TelegramWebhookHandler) CreateWebhookContext( r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, - gaMeasurement *measurement.BufferedSender, + gaMeasurement bots.GaQueuer, ) bots.WebhookContext { return newTelegramWebhookContext( appContext, r, botContext, webhookInput.(TelegramWebhookInput), botCoreStores, gaMeasurement) diff --git a/platforms/telegram/webhook_context.go b/platforms/telegram/webhook_context.go index d757e57..63e2d28 100644 --- a/platforms/telegram/webhook_context.go +++ b/platforms/telegram/webhook_context.go @@ -9,8 +9,7 @@ import ( "github.com/pkg/errors" "github.com/strongo/db" "github.com/strongo/log" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" + "context" "net/http" "strconv" ) @@ -136,7 +135,7 @@ func newTelegramWebhookContext( r *http.Request, botContext bots.BotContext, input TelegramWebhookInput, botCoreStores bots.BotCoreStores, - gaMeasurement *measurement.BufferedSender, + gaMeasurement bots.GaQueuer, ) *TelegramWebhookContext { twhc := &TelegramWebhookContext{ tgInput: input.(TelegramWebhookInput), @@ -208,7 +207,7 @@ func (twhc *TelegramWebhookContext) Init(w http.ResponseWriter, r *http.Request) } func (twhc *TelegramWebhookContext) BotApi() *tgbotapi.BotAPI { - return tgbotapi.NewBotAPIWithClient(twhc.BotContext.BotSettings.Token, twhc.GetHttpClient()) + return tgbotapi.NewBotAPIWithClient(twhc.BotContext.BotSettings.Token, twhc.BotContext.BotHost.GetHttpClient(twhc.Context())) } func (twhc *TelegramWebhookContext) GetAppUser() (bots.BotAppUser, error) { diff --git a/platforms/telegram/webhook_responder.go b/platforms/telegram/webhook_responder.go index b662982..1e97e20 100644 --- a/platforms/telegram/webhook_responder.go +++ b/platforms/telegram/webhook_responder.go @@ -9,7 +9,7 @@ import ( "github.com/strongo/bots-api-telegram" "github.com/strongo/bots-framework/core" "github.com/strongo/log" - "golang.org/x/net/context" + "context" "net/http" "strconv" ) @@ -209,7 +209,7 @@ func (r TelegramWebhookResponder) SendMessage(c context.Context, m bots.MessageF case bots.BotApiSendMessageOverHTTPS: botApi := tgbotapi.NewBotAPIWithClient( r.whc.BotContext.BotSettings.Token, - r.whc.GetHttpClient(), + r.whc.BotContext.BotHost.GetHttpClient(c), ) botApi.EnableDebug(c) if message, err := botApi.Send(chattable); err != nil { diff --git a/platforms/viber/viber_webhooks.go b/platforms/viber/viber_webhooks.go index f9bbb4a..2d5e24e 100644 --- a/platforms/viber/viber_webhooks.go +++ b/platforms/viber/viber_webhooks.go @@ -10,8 +10,7 @@ import ( "github.com/strongo/bots-api-viber/viberinterface" "github.com/strongo/bots-framework/core" "github.com/strongo/log" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" + "context" "io/ioutil" "net/http" "regexp" @@ -63,7 +62,7 @@ func (h ViberWebhookHandler) GetBotContextAndInputs(c context.Context, r *http.R botSettings, ok := h.botsBy(c).ByCode[code] if !ok { errMess := fmt.Sprintf("Unknown public account: [%v]", code) - err = bots.AuthFailedError(errMess) + err = bots.ErrAuthFailed(errMess) return } @@ -173,7 +172,7 @@ func (h ViberWebhookHandler) GetBotContextAndInputs(c context.Context, r *http.R return } -func (_ ViberWebhookHandler) CreateWebhookContext(appContext bots.BotAppContext, r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, gaMeasurement *measurement.BufferedSender) bots.WebhookContext { +func (_ ViberWebhookHandler) CreateWebhookContext(appContext bots.BotAppContext, r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, gaMeasurement bots.GaQueuer) bots.WebhookContext { return NewViberWebhookContext(appContext, r, botContext, webhookInput, botCoreStores, gaMeasurement) } diff --git a/platforms/viber/webhook_context.go b/platforms/viber/webhook_context.go index 0bdbe07..c79a7ff 100644 --- a/platforms/viber/webhook_context.go +++ b/platforms/viber/webhook_context.go @@ -5,8 +5,7 @@ import ( "github.com/strongo/bots-api-viber" "github.com/strongo/bots-framework/core" "github.com/strongo/log" - "github.com/strongo/measurement-protocol" - "golang.org/x/net/context" + "context" "net/http" ) @@ -23,7 +22,7 @@ func (whc *ViberWebhookContext) NewEditMessage(text string, format bots.MessageF panic("Not supported by Viber") } -func NewViberWebhookContext(appContext bots.BotAppContext, r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, gaMeasurement *measurement.BufferedSender) *ViberWebhookContext { +func NewViberWebhookContext(appContext bots.BotAppContext, r *http.Request, botContext bots.BotContext, webhookInput bots.WebhookInput, botCoreStores bots.BotCoreStores, gaMeasurement bots.GaQueuer) *ViberWebhookContext { whcb := bots.NewWebhookContextBase( r, appContext, @@ -76,7 +75,7 @@ func (whc *ViberWebhookContext) Init(w http.ResponseWriter, r *http.Request) err } func (whc *ViberWebhookContext) BotApi() *viberbotapi.ViberBotApi { - return viberbotapi.NewViberBotApiWithHttpClient(whc.BotContext.BotSettings.Token, whc.GetHttpClient()) + return viberbotapi.NewViberBotApiWithHttpClient(whc.BotContext.BotSettings.Token, whc.BotContext.BotHost.GetHttpClient(whc.Context())) } func (whc *ViberWebhookContext) IsNewerThen(chatEntity bots.BotChat) bool { diff --git a/platforms/viber/webhook_responder.go b/platforms/viber/webhook_responder.go index 60a6108..4908c87 100644 --- a/platforms/viber/webhook_responder.go +++ b/platforms/viber/webhook_responder.go @@ -6,7 +6,7 @@ import ( "github.com/strongo/bots-api-viber/viberinterface" "github.com/strongo/bots-framework/core" "github.com/strongo/log" - "golang.org/x/net/context" + "context" ) type ViberWebhookResponder struct { @@ -25,7 +25,7 @@ func NewViberWebhookResponder(whc *ViberWebhookContext) ViberWebhookResponder { func (r ViberWebhookResponder) SendMessage(c context.Context, m bots.MessageFromBot, channel bots.BotApiSendMessageChannel) (resp bots.OnMessageSentResponse, err error) { log.Debugf(c, "ViberWebhookResponder.SendMessage()...") botSettings := r.whc.GetBotSettings() - viberBotApi := viberbotapi.NewViberBotApiWithHttpClient(botSettings.Token, r.whc.GetHttpClient()) + viberBotApi := viberbotapi.NewViberBotApiWithHttpClient(botSettings.Token, r.whc.BotContext.BotHost.GetHttpClient(c)) log.Debugf(c, "Keyboard: %v", m.Keyboard) var viberKeyboard *viberinterface.Keyboard