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