diff --git a/botinput/bot_user.go b/botinput/bot_user.go new file mode 100644 index 0000000..341af10 --- /dev/null +++ b/botinput/bot_user.go @@ -0,0 +1,53 @@ +package botinput + +//// BotUser provides info about current bot user +//type BotUser interface { +// // GetBotUserID returns bot user ID +// GetBotUserID() string +// +// // GetFirstName returns user's first name +// GetFirstName() string +// +// // GetLastName returns user's last name +// GetLastName() string +//} +// +//func New(botUserID string, fields ...func(user *botUser)) BotUser { +// svr := &botUser{botUserID: botUserID} +// for _, f := range fields { +// f(svr) +// } +// return svr +//} +// +//var _ BotUser = (*botUser)(nil) +// +//type botUser struct { +// botUserID string +// firstName string +// lastName string +//} +// +//func (v *botUser) GetBotUserID() string { +// return v.botUserID +//} +// +//func (v *botUser) GetFirstName() string { +// return v.firstName +//} +// +//func (v *botUser) GetLastName() string { +// return v.lastName +//} +// +//func WithFirstName(s string) func(user *botUser) { +// return func(v *botUser) { +// v.firstName = s +// } +//} +// +//func WithLastName(s string) func(user *botUser) { +// return func(v *botUser) { +// v.lastName = s +// } +//} diff --git a/botinput/webhook_interfaces.go b/botinput/webhook_interfaces.go new file mode 100644 index 0000000..3079e48 --- /dev/null +++ b/botinput/webhook_interfaces.go @@ -0,0 +1,267 @@ +package botinput + +import ( + "fmt" + "strconv" + "time" +) + +// WebhookEntry represents a single message from a messenger user +type WebhookEntry interface { + GetID() interface{} + GetTime() time.Time +} + +// WebhookInputType is enum of input type +type WebhookInputType int + +const ( + // WebhookInputUnknown is an unknown input type + WebhookInputUnknown WebhookInputType = iota + // WebhookInputNotImplemented is not implemented input type + WebhookInputNotImplemented + // WebhookInputText is a text input type + WebhookInputText // Facebook, Telegram, Viber + // WebhookInputVoice is voice input type + WebhookInputVoice + // WebhookInputPhoto is a photo input type + WebhookInputPhoto + // WebhookInputAudio is an audio input type + WebhookInputAudio + // WebhookInputContact is a contact input type + WebhookInputContact // Facebook, Telegram, Viber + // WebhookInputPostback is unknown input type + WebhookInputPostback + // WebhookInputDelivery is a postback input type + WebhookInputDelivery + // WebhookInputAttachment is a delivery report input type + WebhookInputAttachment + // WebhookInputInlineQuery is an attachment input type + WebhookInputInlineQuery // Telegram + // WebhookInputCallbackQuery is inline input type + WebhookInputCallbackQuery + // WebhookInputReferral is a callback input type + WebhookInputReferral // FBM + // WebhookInputChosenInlineResult is chosen inline result input type + WebhookInputChosenInlineResult // Telegram + // WebhookInputSubscribed is subscribed input type + WebhookInputSubscribed // Viber + // WebhookInputUnsubscribed is unsubscribed input type + WebhookInputUnsubscribed // Viber + // WebhookInputConversationStarted is conversation started input type + WebhookInputConversationStarted // Viber + // WebhookInputNewChatMembers is new botChat members input type + WebhookInputNewChatMembers // Telegram groups + // WebhookInputLeftChatMembers is left botChat members input type + WebhookInputLeftChatMembers + // WebhookInputSticker is a sticker input type + WebhookInputSticker // Telegram +) + +var webhookInputTypeNames = map[WebhookInputType]string{ + WebhookInputUnknown: "unknown", + WebhookInputNotImplemented: "NotImplemented", + WebhookInputText: "Text", + WebhookInputVoice: "Voice", + WebhookInputPhoto: "Photo", + WebhookInputAudio: "Audio", + WebhookInputReferral: "Referral", + WebhookInputContact: "Contact", + WebhookInputPostback: "Postback", + WebhookInputDelivery: "Delivery", + WebhookInputAttachment: "Attachment", + WebhookInputInlineQuery: "InlineQuery", + WebhookInputCallbackQuery: "CallbackQuery", + WebhookInputChosenInlineResult: "ChosenInlineResult", + WebhookInputSubscribed: "Subscribed", // Viber + WebhookInputUnsubscribed: "Unsubscribed", // Viber + WebhookInputConversationStarted: "ConversationStarted", // Telegram + WebhookInputNewChatMembers: "NewChatMembers", // Telegram + WebhookInputSticker: "Sticker", // Telegram + WebhookInputLeftChatMembers: "LeftChatMembers", // Telegram +} + +func GetWebhookInputTypeIdNameString(whInputType WebhookInputType) string { + name, ok := webhookInputTypeNames[whInputType] + if ok { + return fmt.Sprintf("%d:%s", whInputType, name) + } + return strconv.Itoa(int(whInputType)) +} + +// WebhookInput represent a single message +// '/entry/messaging' for Facebook Messenger +type WebhookInput interface { + GetSender() WebhookUser + GetRecipient() WebhookRecipient + GetTime() time.Time + InputType() WebhookInputType + BotChatID() (string, error) + Chat() WebhookChat + LogRequest() // TODO: should not be part of Input? If should - specify why +} + +// WebhookActor represents sender +type WebhookActor interface { + Platform() string // TODO: Consider removing this? + GetID() any + IsBotUser() bool + GetFirstName() string + GetLastName() string + GetUserName() string + GetLanguage() string +} + +// WebhookSender represents sender with avatar +type WebhookSender interface { + GetAvatar() string // Extension to support avatar (Viber) + WebhookActor +} + +// WebhookUser represents sender with country +type WebhookUser interface { + WebhookSender + + // GetCountry is an extension to support language & country (Viber) + GetCountry() string +} + +// WebhookRecipient represents receiver +type WebhookRecipient interface { + WebhookActor +} + +// WebhookMessage represents single message +type WebhookMessage interface { + IntID() int64 + StringID() string + Chat() WebhookChat + //Sequence() int // 'seq' for Facebook, '???' for Telegram +} + +// WebhookTextMessage represents single text message +type WebhookTextMessage interface { + WebhookMessage + Text() string + IsEdited() bool +} + +// WebhookStickerMessage represents single sticker message +type WebhookStickerMessage interface { + WebhookMessage + // TODO: Define sticker message interface +} + +// WebhookVoiceMessage represents single voice message +type WebhookVoiceMessage interface { + WebhookMessage + // TODO: Define voice message interface +} + +// WebhookPhotoMessage represents single photo message +type WebhookPhotoMessage interface { + WebhookMessage + // TODO: Define photo message interface +} + +// WebhookAudioMessage represents single audio message +type WebhookAudioMessage interface { + WebhookMessage + // TODO: Define audio message interface +} + +// WebhookReferralMessage represents single referral message +// https://developers.facebook.com/docs/messenger-platform/webhook-reference/referral +type WebhookReferralMessage interface { + Type() string + Source() string + RefData() string +} + +// WebhookContactMessage represents single contact message +type WebhookContactMessage interface { + GetPhoneNumber() string + GetFirstName() string + GetLastName() string + GetBotUserID() string +} + +// WebhookNewChatMembersMessage represents single message about a new member of a botChat +type WebhookNewChatMembersMessage interface { + BotChatID() (string, error) + NewChatMembers() []WebhookActor +} + +// WebhookLeftChatMembersMessage represents single message about a member leaving a botChat +type WebhookLeftChatMembersMessage interface { + BotChatID() (string, error) + LeftChatMembers() []WebhookActor +} + +// WebhookChat represents botChat of a messenger +type WebhookChat interface { + GetID() string + GetType() string + IsGroupChat() bool +} + +// WebhookPostback represents single postback message +type WebhookPostback interface { + PostbackMessage() interface{} + Payload() string +} + +// WebhookSubscribed represents a subscription message +type WebhookSubscribed interface { + SubscribedMessage() interface{} +} + +// WebhookUnsubscribed represents a message when user unsubscribe +type WebhookUnsubscribed interface { + UnsubscribedMessage() interface{} +} + +// WebhookConversationStarted represents a single message about new conversation +type WebhookConversationStarted interface { + ConversationStartedMessage() interface{} +} + +// WebhookInlineQuery represents a single inline message +type WebhookInlineQuery interface { + GetID() interface{} + GetInlineQueryID() string + GetFrom() WebhookSender + GetQuery() string + GetOffset() string + //GetLocation() - TODO: Not implemented yet +} + +// WebhookDelivery represents a single delivery report message +type WebhookDelivery interface { + Payload() string +} + +// WebhookChosenInlineResult represents a single report message on chosen inline result +type WebhookChosenInlineResult interface { + GetResultID() string + GetInlineMessageID() string // Telegram only? + GetFrom() WebhookSender + GetQuery() string + //GetLocation() - TODO: Not implemented yet +} + +// WebhookCallbackQuery represents a single callback query message +type WebhookCallbackQuery interface { + GetID() string + GetInlineMessageID() string // Telegram only? + GetFrom() WebhookSender + GetMessage() WebhookMessage + GetData() string + Chat() WebhookChat +} + +// WebhookAttachment represents attachment to a message +type WebhookAttachment interface { + Type() string // Enum(image, video, audio) for Facebook + PayloadUrl() string // 'payload.url' for Facebook +} diff --git a/botsdal/facade_user.go b/botsdal/facade_user.go index 36f0925..3c536a6 100644 --- a/botsdal/facade_user.go +++ b/botsdal/facade_user.go @@ -3,45 +3,55 @@ package botsdal import ( "context" "github.com/bots-go-framework/bots-fw-store/botsfwmodels" - "github.com/bots-go-framework/bots-fw/botsfsobject" + "github.com/bots-go-framework/bots-fw/botinput" + "github.com/bots-go-framework/bots-fw/botsfwconst" "github.com/dal-go/dalgo/dal" "github.com/dal-go/dalgo/record" ) +type Bot struct { + Platform botsfwconst.Platform + ID string + User botinput.WebhookUser +} + type AppUserDal interface { - CreateAppUserFromBotUser(ctx context.Context, tx dal.ReadwriteTransaction, platform, botID string, botUser botsfsobject.BotUser) ( - appUser record.DataWithID[string, botsfwmodels.AppUserData], err error, - ) - GetAppUserByBotUserID(ctx context.Context, tx dal.ReadwriteTransaction, platform, botID, botUserID string) ( + + // CreateAppUserFromBotUser creates app user record using bot user data + CreateAppUserFromBotUser(ctx context.Context, tx dal.ReadwriteTransaction, bot Bot) ( appUser record.DataWithID[string, botsfwmodels.AppUserData], err error, ) + + //GetAppUserByBotUserID(ctx context.Context, tx dal.ReadwriteTransaction, platform, botID, botUserID string) ( + // appUser record.DataWithID[string, botsfwmodels.AppUserData], err error, + //) //UpdateAppUser(ctx context.Context, tx dal.ReadwriteTransaction, appUser record.DataWithID[string, botsfwmodels.AppUserData]) error //LinkAppUserToBotUser(ctx context.Context, platform, botID, botUserID, appUserID string) (err error) } -type appUserDal struct { -} - -func DefaultAppUserDal() AppUserDal { - return appUserDal{} -} - -func (a appUserDal) CreateAppUserFromBotUser(ctx context.Context, tx dal.ReadwriteTransaction, platform, botID string, botUser botsfsobject.BotUser) (appUser record.DataWithID[string, botsfwmodels.AppUserData], err error) { - //TODO implement me - panic("implement me") -} - -func (a appUserDal) GetAppUserByBotUserID(ctx context.Context, tx dal.ReadwriteTransaction, platform, botID, botUserID string) (appUser record.DataWithID[string, botsfwmodels.AppUserData], err error) { - //TODO implement me - panic("implement me") -} - -func (a appUserDal) UpdateAppUser(ctx context.Context, tx dal.ReadwriteTransaction, appUser record.DataWithID[string, botsfwmodels.AppUserData]) error { - //TODO implement me - panic("implement me") -} - -func (a appUserDal) LinkAppUserToBotUser(ctx context.Context, platform, botID, botUserID, appUserID string) (err error) { - //TODO implement me - panic("implement me") -} +//type appUserDal struct { +//} +// +//func DefaultAppUserDal() AppUserDal { +// return appUserDal{} +//} +// +//func (a appUserDal) CreateAppUserFromBotUser(ctx context.Context, tx dal.ReadwriteTransaction, platform, botID string, botUser botinput.WebhookUser) (appUser record.DataWithID[string, botsfwmodels.AppUserData], err error) { +// //TODO implement me +// panic("implement me") +//} +// +//func (a appUserDal) GetAppUserByBotUserID(ctx context.Context, tx dal.ReadwriteTransaction, platform, botID, botUserID string) (appUser record.DataWithID[string, botsfwmodels.AppUserData], err error) { +// //TODO implement me +// panic("implement me") +//} +// +//func (a appUserDal) UpdateAppUser(ctx context.Context, tx dal.ReadwriteTransaction, appUser record.DataWithID[string, botsfwmodels.AppUserData]) error { +// //TODO implement me +// panic("implement me") +//} +// +//func (a appUserDal) LinkAppUserToBotUser(ctx context.Context, platform, botID, botUserID, appUserID string) (err error) { +// //TODO implement me +// panic("implement me") +//} diff --git a/botsfsobject/bot_user.go b/botsfsobject/bot_user.go deleted file mode 100644 index 3e89d58..0000000 --- a/botsfsobject/bot_user.go +++ /dev/null @@ -1,13 +0,0 @@ -package botsfsobject - -// BotUser provides info about current bot user -type BotUser interface { - // GetBotUserID returns bot user ID - GetBotUserID() string - - // GetFirstName returns user's first name - GetFirstName() string - - // GetLastName returns user's last name - GetLastName() string -} diff --git a/botsfw/bot_records_fields_setter.go b/botsfw/bot_records_fields_setter.go index c008c4d..0ebb091 100644 --- a/botsfw/bot_records_fields_setter.go +++ b/botsfw/bot_records_fields_setter.go @@ -1,6 +1,9 @@ package botsfw -import "github.com/bots-go-framework/bots-fw-store/botsfwmodels" +import ( + "github.com/bots-go-framework/bots-fw-store/botsfwmodels" + "github.com/bots-go-framework/bots-fw/botinput" +) type BotRecordsFieldsSetter interface { @@ -11,12 +14,12 @@ type BotRecordsFieldsSetter interface { Platform() string // SetAppUserFields sets fields of app user record - SetAppUserFields(appUser botsfwmodels.AppUserData, sender WebhookSender) error + SetAppUserFields(appUser botsfwmodels.AppUserData, sender botinput.WebhookSender) error // SetBotUserFields sets fields of bot user record - SetBotUserFields(botUser botsfwmodels.PlatformUserData, sender WebhookSender, botID, botUserID, appUserID string) error + SetBotUserFields(botUser botsfwmodels.PlatformUserData, sender botinput.WebhookSender, botID, botUserID, appUserID string) error // SetBotChatFields sets fields of bot botChat record // TODO: document isAccessGranted parameter - SetBotChatFields(botChat botsfwmodels.BotChatData, chat WebhookChat, botID, botUserID, appUserID string, isAccessGranted bool) error + SetBotChatFields(botChat botsfwmodels.BotChatData, chat botinput.WebhookChat, botID, botUserID, appUserID string, isAccessGranted bool) error } diff --git a/botsfw/bot_user_creator.go b/botsfw/bot_user_creator.go index 3863ab3..3d0e3da 100644 --- a/botsfw/bot_user_creator.go +++ b/botsfw/bot_user_creator.go @@ -3,6 +3,7 @@ package botsfw import ( "context" "github.com/bots-go-framework/bots-fw-store/botsfwmodels" + "github.com/bots-go-framework/bots-fw/botinput" ) -type BotUserCreator func(c context.Context, botID string, apiUser WebhookActor) (botsfwmodels.PlatformUserData, error) +type BotUserCreator func(c context.Context, botID string, apiUser botinput.WebhookActor) (botsfwmodels.PlatformUserData, error) diff --git a/botsfw/commands.go b/botsfw/commands.go index 163ef83..68fb3cc 100644 --- a/botsfw/commands.go +++ b/botsfw/commands.go @@ -2,6 +2,7 @@ package botsfw import ( "fmt" + "github.com/bots-go-framework/bots-fw/botinput" "net/url" ) @@ -24,7 +25,7 @@ const ShortTitle = "short_title" // Command defines command metadata and action type Command struct { - InputTypes []WebhookInputType // Instant match if != WebhookInputUnknown && == whc.InputTypes() + InputTypes []botinput.WebhookInputType // Instant match if != WebhookInputUnknown && == whc.InputTypes() Icon string Replies []Command Code string @@ -41,7 +42,7 @@ type Command struct { func NewInlineQueryCommand(code string, action CommandAction) Command { return Command{ Code: code, - InputTypes: []WebhookInputType{WebhookInputInlineQuery}, + InputTypes: []botinput.WebhookInputType{botinput.WebhookInputInlineQuery}, Action: action, } } diff --git a/botsfw/context_auth.go b/botsfw/context_auth.go index e06ceb1..e75a91f 100644 --- a/botsfw/context_auth.go +++ b/botsfw/context_auth.go @@ -20,7 +20,7 @@ func SetAccessGranted(whc WebhookContext, value bool) (err error) { chatKey := botsfwmodels.ChatKey{ BotID: botID, } - if chatKey.ChatID, err = whc.BotChatID(); err != nil { + if chatKey.ChatID, err = whc.Input().BotChatID(); err != nil { return err } if changed := chatData.SetAccessGranted(value); changed { @@ -36,7 +36,7 @@ func SetAccessGranted(whc WebhookContext, value bool) (err error) { } } - botUserID := whc.GetSender().GetID() + botUserID := whc.Input().GetSender().GetID() botUserStrID := fmt.Sprintf("%v", botUserID) log.Debugf(c, "SetAccessGranted(): whc.GetSender().GetID() = %v", botUserID) tx := whc.Tx() diff --git a/botsfw/context_new.go b/botsfw/context_new.go index df4cd46..deb143f 100644 --- a/botsfw/context_new.go +++ b/botsfw/context_new.go @@ -1,7 +1,9 @@ package botsfw +import "github.com/bots-go-framework/bots-fw/botinput" + // WebhookNewContext TODO: needs to be checked & described type WebhookNewContext struct { BotContext - WebhookInput + botinput.WebhookInput } diff --git a/botsfw/handler.go b/botsfw/handler.go index 6c3e995..15684ea 100644 --- a/botsfw/handler.go +++ b/botsfw/handler.go @@ -2,6 +2,7 @@ package botsfw import ( "context" + "github.com/bots-go-framework/bots-fw/botinput" "github.com/dal-go/dalgo/dal" "net/http" ) @@ -41,7 +42,7 @@ type CreateWebhookContextArgs struct { HttpRequest *http.Request // TODO: Can we get rid of it? Needed for botHost.GetHTTPClient() AppContext AppContext BotContext BotContext - WebhookInput WebhookInput + WebhookInput botinput.WebhookInput Tx dal.ReadwriteTransaction //BotCoreStores botsfwdal.DataAccess GaMeasurement GaQueuer @@ -51,7 +52,7 @@ func NewCreateWebhookContextArgs( httpRequest *http.Request, appContext AppContext, botContext BotContext, - webhookInput WebhookInput, + webhookInput botinput.WebhookInput, tx dal.ReadwriteTransaction, //botCoreStores botsfwdal.DataAccess, gaMeasurement GaQueuer, diff --git a/botsfw/interfaces.go b/botsfw/interfaces.go index 22318b6..6f50a44 100644 --- a/botsfw/interfaces.go +++ b/botsfw/interfaces.go @@ -2,10 +2,7 @@ package botsfw import ( "context" - "fmt" "net/http" - "strconv" - "time" ) // BotPlatform describes current bot platform @@ -47,266 +44,6 @@ func NewBotContext(botHost BotHost, botSettings *BotSettings) *BotContext { } } -// WebhookEntry represents a single message from a messenger user -type WebhookEntry interface { - GetID() interface{} - GetTime() time.Time -} - -// WebhookInputType is enum of input type -type WebhookInputType int - -const ( - // WebhookInputUnknown is an unknown input type - WebhookInputUnknown WebhookInputType = iota - // WebhookInputNotImplemented is not implemented input type - WebhookInputNotImplemented - // WebhookInputText is a text input type - WebhookInputText // Facebook, Telegram, Viber - // WebhookInputVoice is voice input type - WebhookInputVoice - // WebhookInputPhoto is a photo input type - WebhookInputPhoto - // WebhookInputAudio is an audio input type - WebhookInputAudio - // WebhookInputContact is a contact input type - WebhookInputContact // Facebook, Telegram, Viber - // WebhookInputPostback is unknown input type - WebhookInputPostback - // WebhookInputDelivery is a postback input type - WebhookInputDelivery - // WebhookInputAttachment is a delivery report input type - WebhookInputAttachment - // WebhookInputInlineQuery is an attachment input type - WebhookInputInlineQuery // Telegram - // WebhookInputCallbackQuery is inline input type - WebhookInputCallbackQuery - // WebhookInputReferral is a callback input type - WebhookInputReferral // FBM - // WebhookInputChosenInlineResult is chosen inline result input type - WebhookInputChosenInlineResult // Telegram - // WebhookInputSubscribed is subscribed input type - WebhookInputSubscribed // Viber - // WebhookInputUnsubscribed is unsubscribed input type - WebhookInputUnsubscribed // Viber - // WebhookInputConversationStarted is conversation started input type - WebhookInputConversationStarted // Viber - // WebhookInputNewChatMembers is new botChat members input type - WebhookInputNewChatMembers // Telegram groups - // WebhookInputLeftChatMembers is left botChat members input type - WebhookInputLeftChatMembers - // WebhookInputSticker is a sticker input type - WebhookInputSticker // Telegram -) - -var webhookInputTypeNames = map[WebhookInputType]string{ - WebhookInputUnknown: "unknown", - WebhookInputNotImplemented: "NotImplemented", - WebhookInputText: "Text", - WebhookInputVoice: "Voice", - WebhookInputPhoto: "Photo", - WebhookInputAudio: "Audio", - WebhookInputReferral: "Referral", - WebhookInputContact: "Contact", - WebhookInputPostback: "Postback", - WebhookInputDelivery: "Delivery", - WebhookInputAttachment: "Attachment", - WebhookInputInlineQuery: "InlineQuery", - WebhookInputCallbackQuery: "CallbackQuery", - WebhookInputChosenInlineResult: "ChosenInlineResult", - WebhookInputSubscribed: "Subscribed", // Viber - WebhookInputUnsubscribed: "Unsubscribed", // Viber - WebhookInputConversationStarted: "ConversationStarted", // Telegram - WebhookInputNewChatMembers: "NewChatMembers", // Telegram - WebhookInputSticker: "Sticker", // Telegram - WebhookInputLeftChatMembers: "LeftChatMembers", // Telegram -} - -func GetWebhookInputTypeIdNameString(whInputType WebhookInputType) string { - name, ok := webhookInputTypeNames[whInputType] - if ok { - return fmt.Sprintf("%d:%s", whInputType, name) - } - return strconv.Itoa(int(whInputType)) -} - -// WebhookInput represent a single message -// '/entry/messaging' for Facebook Messenger -type WebhookInput interface { - GetSender() WebhookSender - GetRecipient() WebhookRecipient - GetTime() time.Time - InputType() WebhookInputType - BotChatID() (string, error) - Chat() WebhookChat - LogRequest() -} - -// WebhookActor represents sender -type WebhookActor interface { - Platform() string // TODO: Consider removing this? - GetID() interface{} - IsBotUser() bool - GetFirstName() string - GetLastName() string - GetUserName() string - GetLanguage() string -} - -// WebhookSender represents sender with avatar -type WebhookSender interface { - GetAvatar() string // Extension to support avatar (Viber) - WebhookActor -} - -// WebhookUser represents sender with country -type WebhookUser interface { - WebhookSender - - // GetCountry is an extension to support language & country (Viber) - GetCountry() string -} - -// WebhookRecipient represents receiver -type WebhookRecipient interface { - WebhookActor -} - -// WebhookMessage represents single message -type WebhookMessage interface { - IntID() int64 - StringID() string - Chat() WebhookChat - //Sequence() int // 'seq' for Facebook, '???' for Telegram -} - -// WebhookTextMessage represents single text message -type WebhookTextMessage interface { - WebhookMessage - Text() string - IsEdited() bool -} - -// WebhookStickerMessage represents single sticker message -type WebhookStickerMessage interface { - WebhookMessage - // TODO: Define sticker message interface -} - -// WebhookVoiceMessage represents single voice message -type WebhookVoiceMessage interface { - WebhookMessage - // TODO: Define voice message interface -} - -// WebhookPhotoMessage represents single photo message -type WebhookPhotoMessage interface { - WebhookMessage - // TODO: Define photo message interface -} - -// WebhookAudioMessage represents single audio message -type WebhookAudioMessage interface { - WebhookMessage - // TODO: Define audio message interface -} - -// WebhookReferralMessage represents single referral message -// https://developers.facebook.com/docs/messenger-platform/webhook-reference/referral -type WebhookReferralMessage interface { - Type() string - Source() string - RefData() string -} - -// WebhookContactMessage represents single contact message -type WebhookContactMessage interface { - PhoneNumber() string - FirstName() string - LastName() string - UserID() interface{} -} - -// WebhookNewChatMembersMessage represents single message about a new member of a botChat -type WebhookNewChatMembersMessage interface { - BotChatID() (string, error) - NewChatMembers() []WebhookActor -} - -// WebhookLeftChatMembersMessage represents single message about a member leaving a botChat -type WebhookLeftChatMembersMessage interface { - BotChatID() (string, error) - LeftChatMembers() []WebhookActor -} - -// WebhookChat represents botChat of a messenger -type WebhookChat interface { - GetID() string - GetType() string - IsGroupChat() bool -} - -// WebhookPostback represents single postback message -type WebhookPostback interface { - PostbackMessage() interface{} - Payload() string -} - -// WebhookSubscribed represents a subscription message -type WebhookSubscribed interface { - SubscribedMessage() interface{} -} - -// WebhookUnsubscribed represents a message when user unsubscribe -type WebhookUnsubscribed interface { - UnsubscribedMessage() interface{} -} - -// WebhookConversationStarted represents a single message about new conversation -type WebhookConversationStarted interface { - ConversationStartedMessage() interface{} -} - -// WebhookInlineQuery represents a single inline message -type WebhookInlineQuery interface { - GetID() interface{} - GetInlineQueryID() string - GetFrom() WebhookSender - GetQuery() string - GetOffset() string - //GetLocation() - TODO: Not implemented yet -} - -// WebhookDelivery represents a single delivery report message -type WebhookDelivery interface { - Payload() string -} - -// WebhookChosenInlineResult represents a single report message on chosen inline result -type WebhookChosenInlineResult interface { - GetResultID() string - GetInlineMessageID() string // Telegram only? - GetFrom() WebhookSender - GetQuery() string - //GetLocation() - TODO: Not implemented yet -} - -// WebhookCallbackQuery represents a single callback query message -type WebhookCallbackQuery interface { - GetID() string - GetInlineMessageID() string // Telegram only? - GetFrom() WebhookSender - GetMessage() WebhookMessage - GetData() string - Chat() WebhookChat -} - -// WebhookAttachment represents attachment to a message -type WebhookAttachment interface { - Type() string // Enum(image, video, audio) for Facebook - PayloadUrl() string // 'payload.url' for Facebook -} - // MessengerResponse represents response from a messenger type MessengerResponse interface { } diff --git a/botsfw/router.go b/botsfw/router.go index 3369508..1e478b2 100644 --- a/botsfw/router.go +++ b/botsfw/router.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/bots-go-framework/bots-fw-store/botsfwmodels" + "github.com/bots-go-framework/bots-fw/botinput" "github.com/strongo/gamp" "net/url" "strings" @@ -23,7 +24,7 @@ func newTypeCommands(commandsCount int) *TypeCommands { } } -func (v *TypeCommands) addCommand(command Command, commandType WebhookInputType) { +func (v *TypeCommands) addCommand(command Command, commandType botinput.WebhookInputType) { if command.Code == "" { panic(fmt.Sprintf("Command %v is missing required property ByCode", command)) } @@ -37,16 +38,16 @@ func (v *TypeCommands) addCommand(command Command, commandType WebhookInputType) // WebhooksRouter maps routes to commands type WebhooksRouter struct { - commandsByType map[WebhookInputType]*TypeCommands + commandsByType map[botinput.WebhookInputType]*TypeCommands errorFooterText func() string } // NewWebhookRouter creates new router // //goland:noinspection GoUnusedExportedFunction -func NewWebhookRouter(commandsByType map[WebhookInputType][]Command, errorFooterText func() string) WebhooksRouter { +func NewWebhookRouter(commandsByType map[botinput.WebhookInputType][]Command, errorFooterText func() string) WebhooksRouter { r := WebhooksRouter{ - commandsByType: make(map[WebhookInputType]*TypeCommands, len(commandsByType)), + commandsByType: make(map[botinput.WebhookInputType]*TypeCommands, len(commandsByType)), errorFooterText: errorFooterText, } @@ -66,28 +67,28 @@ func (whRouter *WebhooksRouter) CommandsCount() int { } // AddCommands add commands to a router -func (whRouter *WebhooksRouter) AddCommands(commandsType WebhookInputType, commands []Command) { +func (whRouter *WebhooksRouter) AddCommands(commandsType botinput.WebhookInputType, commands []Command) { typeCommands, ok := whRouter.commandsByType[commandsType] if !ok { typeCommands = newTypeCommands(len(commands)) whRouter.commandsByType[commandsType] = typeCommands - } else if commandsType == WebhookInputInlineQuery { + } else if commandsType == botinput.WebhookInputInlineQuery { panic("Duplicate add of WebhookInputInlineQuery") } - if commandsType == WebhookInputInlineQuery && len(commands) > 1 { + if commandsType == botinput.WebhookInputInlineQuery && len(commands) > 1 { panic("commandsType == WebhookInputInlineQuery && len(commands) > 1") } for _, command := range commands { typeCommands.addCommand(command, commandsType) } - if commandsType == WebhookInputInlineQuery && len(typeCommands.all) > 1 { + if commandsType == botinput.WebhookInputInlineQuery && len(typeCommands.all) > 1 { panic(fmt.Sprintf("commandsType == WebhookInputInlineQuery && len(typeCommands) > 1: %v", typeCommands.all[0])) } } // RegisterCommands is registering commands with router func (whRouter *WebhooksRouter) RegisterCommands(commands []Command) { - addCommand := func(t WebhookInputType, command Command) { + addCommand := func(t botinput.WebhookInputType, command Command) { typeCommands, ok := whRouter.commandsByType[t] if !ok { typeCommands = newTypeCommands(0) @@ -98,21 +99,21 @@ func (whRouter *WebhooksRouter) RegisterCommands(commands []Command) { for _, command := range commands { if len(command.InputTypes) == 0 { if command.Action != nil { - addCommand(WebhookInputText, command) + addCommand(botinput.WebhookInputText, command) } if command.CallbackAction != nil { - addCommand(WebhookInputCallbackQuery, command) + addCommand(botinput.WebhookInputCallbackQuery, command) } } else { callbackAdded := false for _, t := range command.InputTypes { addCommand(t, command) - if t == WebhookInputCallbackQuery { + if t == botinput.WebhookInputCallbackQuery { callbackAdded = true } } if command.CallbackAction != nil && !callbackAdded { - addCommand(WebhookInputCallbackQuery, command) + addCommand(botinput.WebhookInputCallbackQuery, command) } } } @@ -120,7 +121,7 @@ func (whRouter *WebhooksRouter) RegisterCommands(commands []Command) { var ErrNoCommandsMatched = errors.New("no commands matched") -func matchCallbackCommands(whc WebhookContext, input WebhookCallbackQuery, typeCommands *TypeCommands) (matchedCommand *Command, callbackURL *url.URL, err error) { +func matchCallbackCommands(whc WebhookContext, input botinput.WebhookCallbackQuery, typeCommands *TypeCommands) (matchedCommand *Command, callbackURL *url.URL, err error) { if len(typeCommands.all) > 0 { callbackData := input.GetData() callbackURL, err = url.Parse(callbackData) @@ -141,7 +142,7 @@ func matchCallbackCommands(whc WebhookContext, input WebhookCallbackQuery, typeC } //if matchedCommand == nil { log.Errorf(whc.Context(), fmt.Errorf("%w: %s", ErrNoCommandsMatched, fmt.Sprintf("callbackData=[%v]", callbackData)).Error()) - whc.LogRequest() + whc.Input().LogRequest() // TODO: LogRequest() should not be part of Input? //} } else { panic("len(typeCommands.all) == 0") @@ -149,7 +150,7 @@ func matchCallbackCommands(whc WebhookContext, input WebhookCallbackQuery, typeC return nil, callbackURL, err } -func (whRouter *WebhooksRouter) matchMessageCommands(whc WebhookContext, input WebhookMessage, isCommandText bool, messageText, parentPath string, commands []Command) (matchedCommand *Command) { +func (whRouter *WebhooksRouter) matchMessageCommands(whc WebhookContext, input botinput.WebhookMessage, isCommandText bool, messageText, parentPath string, commands []Command) (matchedCommand *Command) { c := whc.Context() var awaitingReplyCommand Command @@ -312,12 +313,12 @@ func (whRouter *WebhooksRouter) Dispatch(webhookHandler WebhookHandler, responde // } // }() - inputType := whc.InputType() + inputType := whc.Input().InputType() typeCommands, found := whRouter.commandsByType[inputType] if !found { - log.Debugf(c, "No commands found to match by inputType: %v", GetWebhookInputTypeIdNameString(inputType)) - whc.LogRequest() + log.Debugf(c, "No commands found to match by inputType: %v", botinput.GetWebhookInputTypeIdNameString(inputType)) + whc.Input().LogRequest() logInputDetails(whc, false) return } @@ -331,7 +332,7 @@ func (whRouter *WebhooksRouter) Dispatch(webhookHandler WebhookHandler, responde input := whc.Input() var isCommandText bool switch input := input.(type) { - case WebhookCallbackQuery: + case botinput.WebhookCallbackQuery: var callbackURL *url.URL matchedCommand, callbackURL, err = matchCallbackCommands(whc, input, typeCommands) if err == nil && matchedCommand != nil { @@ -349,13 +350,13 @@ func (whRouter *WebhooksRouter) Dispatch(webhookHandler WebhookHandler, responde } } } - case WebhookMessage: - if inputType == WebhookInputNewChatMembers && len(typeCommands.all) == 1 { + case botinput.WebhookMessage: + if inputType == botinput.WebhookInputNewChatMembers && len(typeCommands.all) == 1 { matchedCommand = &typeCommands.all[0] } if matchedCommand == nil { var messageText string - if textMessage, ok := input.(WebhookTextMessage); ok { + if textMessage, ok := input.(botinput.WebhookTextMessage); ok { messageText = textMessage.Text() isCommandText = strings.HasPrefix(messageText, "/") } @@ -368,7 +369,7 @@ func (whRouter *WebhooksRouter) Dispatch(webhookHandler WebhookHandler, responde commandAction = matchedCommand.Action } default: - if inputType == WebhookInputUnknown { + if inputType == botinput.WebhookInputUnknown { panic("Unknown input type") } matchedCommand = &typeCommands.all[0] @@ -380,13 +381,13 @@ func (whRouter *WebhooksRouter) Dispatch(webhookHandler WebhookHandler, responde } if matchedCommand == nil { - whc.LogRequest() + whc.Input().LogRequest() log.Debugf(c, "whr.matchMessageCommands() => matchedCommand == nil") if m = webhookHandler.HandleUnmatched(whc); m.Text != "" || m.BotMessage != nil { whRouter.processCommandResponse(matchedCommand, responder, whc, m, nil) return } - if chat := whc.Chat(); chat != nil && chat.IsGroupChat() { + if chat := whc.Input().Chat(); chat != nil && chat.IsGroupChat() { // m = MessageFromBot{Text: "@" + whc.GetBotCode() + ": " + whc.Translate(MessageTextBotDidNotUnderstandTheCommand), Format: MessageFormatHTML} // whr.processCommandResponse(matchedCommand, responder, whc, m, nil) } else { @@ -394,7 +395,7 @@ func (whRouter *WebhooksRouter) Dispatch(webhookHandler WebhookHandler, responde chatEntity := whc.ChatData() if chatEntity != nil { if awaitingReplyTo := chatEntity.GetAwaitingReplyTo(); awaitingReplyTo != "" { - m.Text += fmt.Sprintf("\n\nAwaitingReplyTo: %v", awaitingReplyTo) + m.Text += fmt.Sprintf("\n\nAwaitingReplyTo: %s", awaitingReplyTo) } } log.Debugf(c, "No command found for the message: %v", input) @@ -402,11 +403,10 @@ func (whRouter *WebhooksRouter) Dispatch(webhookHandler WebhookHandler, responde } } else { // matchedCommand != nil if matchedCommand.Code == "" { - log.Debugf(c, "Matched to: %+v", matchedCommand) + log.Debugf(c, "Matched to: command: %+v", matchedCommand) } else { - log.Debugf(c, "Matched to: %v", matchedCommand.Code) // runtime.FuncForPC(reflect.ValueOf(command.Action).Pointer()).Name() + log.Debugf(c, "Matched to: command.Code=%s", matchedCommand.Code) // runtime.FuncForPC(reflect.ValueOf(command.Action).Pointer()).Name() } - var err error if commandAction == nil { err = errors.New("No action for matched command") } else { @@ -439,14 +439,14 @@ func (whRouter *WebhooksRouter) Dispatch(webhookHandler WebhookHandler, responde func logInputDetails(whc WebhookContext, isKnownType bool) { c := whc.Context() - inputType := whc.InputType() + inputType := whc.Input().InputType() input := whc.Input() - inputTypeIdName := GetWebhookInputTypeIdNameString(inputType) + inputTypeIdName := botinput.GetWebhookInputTypeIdNameString(inputType) logMessage := fmt.Sprintf("WebhooksRouter.Dispatch() => WebhookIputType=%s, %T", inputTypeIdName, input) switch inputType { - case WebhookInputText: - textMessage := input.(WebhookTextMessage) - logMessage += fmt.Sprintf("message text: [%v]", textMessage.Text()) + case botinput.WebhookInputText: + textMessage := input.(botinput.WebhookTextMessage) + logMessage += fmt.Sprintf("message text: [%s]", textMessage.Text()) if textMessage.IsEdited() { // TODO: Should be in app logic, move out of botsfw m := whc.NewMessage("🙇 Sorry, editing messages is not supported. Please send a new message.") log.Warningf(c, "TODO: Edited messages are not supported by framework yet. Move check to app.") @@ -456,20 +456,23 @@ func logInputDetails(whc WebhookContext, isKnownType bool) { } return } - case WebhookInputContact: - logMessage += fmt.Sprintf("contact number: [%v]", input.(WebhookContactMessage)) - case WebhookInputInlineQuery: - logMessage += fmt.Sprintf("inline query: [%v]", input.(WebhookInlineQuery).GetQuery()) - case WebhookInputCallbackQuery: - logMessage += fmt.Sprintf("callback data: [%v]", input.(WebhookCallbackQuery).GetData()) - case WebhookInputChosenInlineResult: - chosenResult := input.(WebhookChosenInlineResult) - logMessage += fmt.Sprintf("ChosenInlineResult: ResultID=[%v], InlineMessageID=[%v], Query=[%v]", chosenResult.GetResultID(), chosenResult.GetInlineMessageID(), chosenResult.GetQuery()) - case WebhookInputReferral: - referralMessage := input.(WebhookReferralMessage) - logMessage += fmt.Sprintf("referralMessage: Type=[%v], Source=[%v], Ref=[%v]", referralMessage.Type(), referralMessage.Source(), referralMessage.RefData()) + case botinput.WebhookInputContact: + contact := input.(botinput.WebhookContactMessage) + contactFirstName := contact.GetFirstName() + contactBotUserID := contact.GetBotUserID() + logMessage += fmt.Sprintf("contact number: {UserID: %s, FirstName: %s}", contactBotUserID, contactFirstName) + case botinput.WebhookInputInlineQuery: + logMessage += fmt.Sprintf("inline query: [%s]", input.(botinput.WebhookInlineQuery).GetQuery()) + case botinput.WebhookInputCallbackQuery: + logMessage += fmt.Sprintf("callback data: [%s]", input.(botinput.WebhookCallbackQuery).GetData()) + case botinput.WebhookInputChosenInlineResult: + chosenResult := input.(botinput.WebhookChosenInlineResult) + logMessage += fmt.Sprintf("ChosenInlineResult: ResultID=[%s], InlineMessageID=[%s], Query=[%s]", chosenResult.GetResultID(), chosenResult.GetInlineMessageID(), chosenResult.GetQuery()) + case botinput.WebhookInputReferral: + referralMessage := input.(botinput.WebhookReferralMessage) + logMessage += fmt.Sprintf("referralMessage: Type=[%s], Source=[%s], Ref=[%s]", referralMessage.Type(), referralMessage.Source(), referralMessage.RefData()) default: - logMessage += "Unknown WebhookInputType=" + GetWebhookInputTypeIdNameString(inputType) + logMessage += "Unknown WebhookInputType=" + botinput.GetWebhookInputTypeIdNameString(inputType) } if isKnownType { log.Debugf(c, logMessage) @@ -504,7 +507,7 @@ func (whRouter *WebhooksRouter) processCommandResponse(matchedCommand *Command, switch { case strings.Contains(errText, "message is not modified"): // TODO: This checks are specific to Telegram and should be abstracted or moved to TG related package logText := failedToSendMessageToMessenger - if whc.InputType() == WebhookInputCallbackQuery { + if whc.Input().InputType() == botinput.WebhookInputCallbackQuery { logText += "(can be duplicate callback)" } log.Warningf(c, fmt.Errorf("%s: %w", logText, err).Error()) // TODO: Think how to get rid of warning on duplicate callbacks when users clicks multiple times @@ -523,7 +526,7 @@ func (whRouter *WebhooksRouter) processCommandResponse(matchedCommand *Command, gaHostName := fmt.Sprintf("%v.debtstracker.io", strings.ToLower(whc.BotPlatform().ID())) pathPrefix := "bot/" var pageView *gamp.Pageview - if inputType := whc.InputType(); inputType != WebhookInputCallbackQuery { + if inputType := whc.Input().InputType(); inputType != botinput.WebhookInputCallbackQuery { chatData := whc.ChatData() if chatData != nil { path := chatData.GetAwaitingReplyTo() @@ -534,7 +537,7 @@ func (whRouter *WebhooksRouter) processCommandResponse(matchedCommand *Command, } pageView = gamp.NewPageviewWithDocumentHost(gaHostName, pathPrefix+path, matchedCommand.Title) } else { - pageView = gamp.NewPageviewWithDocumentHost(gaHostName, pathPrefix+GetWebhookInputTypeIdNameString(inputType), matchedCommand.Title) + pageView = gamp.NewPageviewWithDocumentHost(gaHostName, pathPrefix+botinput.GetWebhookInputTypeIdNameString(inputType), matchedCommand.Title) } } @@ -569,8 +572,8 @@ func (whRouter *WebhooksRouter) processCommandResponseError(whc WebhookContext, err = nil } } - inputType := whc.InputType() - if inputType == WebhookInputText || inputType == WebhookInputContact { + inputType := whc.Input().InputType() + if inputType == botinput.WebhookInputText || inputType == botinput.WebhookInputContact { // TODO: Try to get botChat ID from user? m := whc.NewMessage( whc.Translate(MessageTextOopsSomethingWentWrong) + diff --git a/botsfw/structs.go b/botsfw/structs.go index 6447319..37eb5b6 100644 --- a/botsfw/structs.go +++ b/botsfw/structs.go @@ -4,6 +4,7 @@ package botsfw import ( "context" + "github.com/bots-go-framework/bots-fw/botinput" botsgocore "github.com/bots-go-framework/bots-go-core" "github.com/strongo/i18n" "strconv" @@ -12,14 +13,14 @@ import ( // EntryInputs provides information on parsed inputs from bot API request type EntryInputs struct { - Entry WebhookEntry - Inputs []WebhookInput + Entry botinput.WebhookEntry + Inputs []botinput.WebhookInput } // EntryInput provides information on parsed input from bot API request type EntryInput struct { - Entry WebhookEntry - Input WebhookInput + Entry botinput.WebhookEntry + Input botinput.WebhookInput } // TranslatorProvider translates texts diff --git a/botsfw/structs_ffjson.go b/botsfw/structs_ffjson.go index e11c2b1..81282c7 100644 --- a/botsfw/structs_ffjson.go +++ b/botsfw/structs_ffjson.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/bots-go-framework/bots-fw/botinput" fflib "github.com/pquerna/ffjson/fflib/v1" ) @@ -808,13 +809,13 @@ handle_Inputs: j.Inputs = nil } else { - j.Inputs = []WebhookInput{} + j.Inputs = []botinput.WebhookInput{} wantVal := true for { - var tmpJInputs WebhookInput + var tmpJInputs botinput.WebhookInput tok = fs.Scan() if tok == fflib.FFTok_error { diff --git a/botsfw/webhook_context.go b/botsfw/webhook_context.go index 4a67678..c67ae32 100644 --- a/botsfw/webhook_context.go +++ b/botsfw/webhook_context.go @@ -3,6 +3,7 @@ package botsfw import ( "context" "github.com/bots-go-framework/bots-fw-store/botsfwmodels" + "github.com/bots-go-framework/bots-fw/botinput" "github.com/dal-go/dalgo/dal" "github.com/dal-go/dalgo/record" "github.com/strongo/gamp" @@ -70,6 +71,7 @@ type WebhookContext interface { // TODO: Make interface much smaller? // BotUser returns record of current bot user BotUser() (botUser record.DataWithID[string, botsfwmodels.PlatformUserData], err error) + GetBotUserID() string // IsInGroup indicates if message was received in a group botChat IsInGroup() (bool, error) // We need to return an error as well (for Telegram chat instance). @@ -109,7 +111,7 @@ type WebhookContext interface { // TODO: Make interface much smaller? // RecordsFieldsSetter returns a helper that sets fields of bot related records RecordsFieldsSetter() BotRecordsFieldsSetter - WebhookInput // TODO: Should be removed!!! + //botinput.WebhookInput // TODO: Should be removed!!! i18n.SingleLocaleTranslator Responder() WebhookResponder @@ -125,5 +127,5 @@ type BotState interface { // BotInputProvider provides an input from a specific bot interface (Telegram, FB Messenger, Viber, etc.) type BotInputProvider interface { // Input returns a webhook input from a specific bot interface (Telegram, FB Messenger, Viber, etc.) - Input() WebhookInput + Input() botinput.WebhookInput } diff --git a/botsfw/webhook_context_base.go b/botsfw/webhook_context_base.go index 7031bcd..0809b34 100644 --- a/botsfw/webhook_context_base.go +++ b/botsfw/webhook_context_base.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "github.com/bots-go-framework/bots-fw-store/botsfwmodels" + "github.com/bots-go-framework/bots-fw/botinput" botsdal2 "github.com/bots-go-framework/bots-fw/botsdal" "github.com/dal-go/dalgo/dal" "github.com/dal-go/dalgo/record" @@ -54,7 +55,7 @@ type WebhookContextBase struct { appContext AppContext botContext BotContext // TODO: rename to something strongo botPlatform BotPlatform - input WebhookInput + input botinput.WebhookInput //recordsMaker botsfwmodels.BotRecordsMaker recordsFieldsSetter BotRecordsFieldsSetter @@ -194,7 +195,7 @@ func (whcb *WebhookContextBase) BotChatID() (botChatID string, err error) { } } switch input := input.(type) { - case WebhookCallbackQuery: + case botinput.WebhookCallbackQuery: data := input.GetData() if strings.Contains(data, "botChat=") { values, err := url.ParseQuery(data) @@ -204,9 +205,9 @@ func (whcb *WebhookContextBase) BotChatID() (botChatID string, err error) { chatID := values.Get("botChat") whcb.SetChatID(chatID) } - case WebhookInlineQuery: + case botinput.WebhookInlineQuery: // pass - case WebhookChosenInlineResult: + case botinput.WebhookChosenInlineResult: // pass default: whcb.LogRequest() @@ -353,24 +354,24 @@ func NewWebhookContextBase( } // Input returns webhook input -func (whcb *WebhookContextBase) Input() WebhookInput { +func (whcb *WebhookContextBase) Input() botinput.WebhookInput { return whcb.input } // Chat returns webhook botChat -func (whcb *WebhookContextBase) Chat() WebhookChat { // TODO: remove +func (whcb *WebhookContextBase) Chat() botinput.WebhookChat { // TODO: remove return whcb.input.Chat() } // GetRecipient returns receiver of the message -func (whcb *WebhookContextBase) GetRecipient() WebhookRecipient { // TODO: remove +func (whcb *WebhookContextBase) GetRecipient() botinput.WebhookRecipient { // TODO: remove return whcb.input.GetRecipient() } // GetSender returns sender of the message -func (whcb *WebhookContextBase) GetSender() WebhookSender { // TODO: remove - return whcb.input.GetSender() -} +//func (whcb *WebhookContextBase) GetSender() botinput.WebhookUser { // TODO: remove +// return whcb.input.GetSender() +//} // GetTime returns time of the message func (whcb *WebhookContextBase) GetTime() time.Time { // TODO: remove @@ -378,7 +379,7 @@ func (whcb *WebhookContextBase) GetTime() time.Time { // TODO: remove } // InputType returns input type -func (whcb *WebhookContextBase) InputType() WebhookInputType { // TODO: remove +func (whcb *WebhookContextBase) InputType() botinput.WebhookInputType { // TODO: remove return whcb.input.InputType() } @@ -714,7 +715,7 @@ func (whcb *WebhookContextBase) SetContext(c context.Context) { // MessageText returns text of a received message func (whcb *WebhookContextBase) MessageText() string { - if tm, ok := whcb.Input().(WebhookTextMessage); ok { + if tm, ok := whcb.Input().(botinput.WebhookTextMessage); ok { return tm.Text() } return "" diff --git a/botsfw/webhook_context_test.go b/botsfw/webhook_context_test.go index dddb69c..b623083 100644 --- a/botsfw/webhook_context_test.go +++ b/botsfw/webhook_context_test.go @@ -3,6 +3,8 @@ package botsfw import ( "fmt" "github.com/bots-go-framework/bots-fw-store/botsfwmodels" + "github.com/bots-go-framework/bots-fw/botinput" + //"github.com/dal-go/dalgo/dal" "github.com/strongo/i18n" "net/http" @@ -29,7 +31,7 @@ func (whc TestWebhookContext) Close(c context.Context) error { return nil } -func (whc TestWebhookContext) CreateBotUser(c context.Context, botID string, apiUser WebhookActor) (botsfwmodels.PlatformUserData, error) { +func (whc TestWebhookContext) CreateBotUser(c context.Context, botID string, apiUser botinput.WebhookActor) (botsfwmodels.PlatformUserData, error) { panic("Not implemented") } @@ -49,11 +51,11 @@ func (whc TestWebhookContext) GetBotUserByID(_ context.Context, botUserID string panic("Not implemented") } -func (whc TestWebhookContext) GetRecipient() WebhookRecipient { +func (whc TestWebhookContext) GetRecipient() botinput.WebhookRecipient { panic("Not implemented") } -func (whc TestWebhookContext) GetSender() WebhookSender { +func (whc TestWebhookContext) GetSender() botinput.WebhookSender { panic("Not implemented") } @@ -61,31 +63,31 @@ func (whc TestWebhookContext) GetTime() time.Time { panic("Not implemented") } -func (whc TestWebhookContext) InputChosenInlineResult() WebhookChosenInlineResult { +func (whc TestWebhookContext) InputChosenInlineResult() botinput.WebhookChosenInlineResult { panic("Not implemented") } -func (whc TestWebhookContext) InputCallbackQuery() WebhookCallbackQuery { +func (whc TestWebhookContext) InputCallbackQuery() botinput.WebhookCallbackQuery { panic("Not implemented") } -func (whc TestWebhookContext) InputDelivery() WebhookDelivery { +func (whc TestWebhookContext) InputDelivery() botinput.WebhookDelivery { panic("Not implemented") } -func (whc TestWebhookContext) InputInlineQuery() WebhookInlineQuery { +func (whc TestWebhookContext) InputInlineQuery() botinput.WebhookInlineQuery { panic("Not implemented") } -func (whc TestWebhookContext) InputMessage() WebhookMessage { +func (whc TestWebhookContext) InputMessage() botinput.WebhookMessage { panic("Not implemented") } -func (whc TestWebhookContext) InputPostback() WebhookPostback { +func (whc TestWebhookContext) InputPostback() botinput.WebhookPostback { panic("Not implemented") } -func (whc TestWebhookContext) InputType() WebhookInputType { +func (whc TestWebhookContext) InputType() botinput.WebhookInputType { panic("Not implemented") } diff --git a/botswebhook/driver.go b/botswebhook/driver.go index d1f0492..535b651 100644 --- a/botswebhook/driver.go +++ b/botswebhook/driver.go @@ -5,8 +5,13 @@ import ( "context" "errors" "fmt" + "github.com/bots-go-framework/bots-fw-store/botsfwmodels" + "github.com/bots-go-framework/bots-fw/botinput" + "github.com/bots-go-framework/bots-fw/botsdal" "github.com/bots-go-framework/bots-fw/botsfw" + "github.com/bots-go-framework/bots-fw/botsfwconst" "github.com/dal-go/dalgo/dal" + "github.com/dal-go/dalgo/record" "github.com/strongo/gamp" "github.com/strongo/logus" "net/http" @@ -20,9 +25,8 @@ var ErrorIcon = "🚨" // BotDriver keeps information about bots and map requests to appropriate handlers type BotDriver struct { - Analytics AnalyticsSettings - botHost botsfw.BotHost - //router *WebhooksRouter + Analytics AnalyticsSettings + botHost botsfw.BotHost panicTextFooter string } @@ -56,16 +60,8 @@ func (d BotDriver) RegisterWebhookHandlers(httpRouter botsfw.HttpRouter, pathPre // HandleWebhook takes and HTTP request and process it func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhookHandler botsfw.WebhookHandler) { - //c := d.botHost.Context(r) - c := context.Background() + ctx := d.botHost.Context(r) - handleError := func(err error, message string) { - logus.Errorf(c, "%s: %v", message, err) - errText := fmt.Sprintf("%s: %s: %v", http.StatusText(http.StatusInternalServerError), message, err) - http.Error(w, errText, http.StatusInternalServerError) - } - - started := time.Now() //log.Debugf(c, "BotDriver.HandleWebhook()") if w == nil { panic("Parameter 'w http.ResponseWriter' is nil") @@ -77,14 +73,51 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook panic("Parameter 'webhookHandler WebhookHandler' is nil") } - botContext, entriesWithInputs, err := webhookHandler.GetBotContextAndInputs(c, r) + // A bot can receiver multiple messages in a single request + botContext, entriesWithInputs, err := webhookHandler.GetBotContextAndInputs(ctx, r) - if d.invalidContextOrInputs(c, w, r, botContext, entriesWithInputs, err) { + if d.invalidContextOrInputs(ctx, w, r, botContext, entriesWithInputs, err) { return } - log.Debugf(c, "BotDriver.HandleWebhook() => botCode=%v, len(entriesWithInputs): %d", botContext.BotSettings.Code, len(entriesWithInputs)) + log.Debugf(ctx, "BotDriver.HandleWebhook() => botCode=%v, len(entriesWithInputs): %d", botContext.BotSettings.Code, len(entriesWithInputs)) + + //botCoreStores := webhookHandler.CreateBotCoreStores(d.appContext, r) + //defer func() { + // if whc != nil { // TODO: How do deal with Facebook multiple entries per request? + // //log.Debugf(c, "Closing BotChatStore...") + // //chatData := whc.ChatData() + // //if chatData != nil && chatData.GetPreferredLanguage() == "" { + // // chatData.SetPreferredLanguage(whc.DefaultLocale().Code5) + // //} + // } + //}() + + handleError := func(err error, message string) { + logus.Errorf(ctx, "%s: %v", message, err) + errText := fmt.Sprintf("%s: %s: %v", http.StatusText(http.StatusInternalServerError), message, err) + http.Error(w, errText, http.StatusInternalServerError) + } + + for _, entryWithInputs := range entriesWithInputs { + for i, input := range entryWithInputs.Inputs { + if err = d.processWebhookInput(ctx, w, r, webhookHandler, botContext, i, input, handleError); err != nil { + log.Errorf(ctx, "Failed to process input[%v]: %v", i, err) + } + } + } +} +func (d BotDriver) processWebhookInput( + ctx context.Context, + w http.ResponseWriter, r *http.Request, webhookHandler botsfw.WebhookHandler, + botContext *botsfw.BotContext, + i int, + input botinput.WebhookInput, + handleError func(err error, message string), +) ( + err error, +) { var ( whc botsfw.WebhookContext // TODO: How do deal with Facebook multiple entries per request? measurementSender *gamp.BufferedClient @@ -103,25 +136,27 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook } if sendStats { botHost := botContext.BotHost - measurementSender = gamp.NewBufferedClient("", botHost.GetHTTPClient(c), func(err error) { - log.Errorf(c, "Failed to log to GA: %v", err) + measurementSender = gamp.NewBufferedClient("", botHost.GetHTTPClient(ctx), func(err error) { + log.Errorf(ctx, "Failed to log to GA: %v", err) }) } else { - log.Debugf(c, "botContext.BotSettings.Env=%s, sendStats=%t", + log.Debugf(ctx, "botContext.BotSettings.Env=%s, sendStats=%t", botContext.BotSettings.Env, sendStats) } } + started := time.Now() + defer func() { - log.Debugf(c, "driver.deferred(recover) - checking for panic & flush GA") + log.Debugf(ctx, "driver.deferred(recover) - checking for panic & flush GA") if sendStats { if d.Analytics.GaTrackingID == "" { - log.Warningf(c, "driver.Analytics.GaTrackingID is not set") + log.Warningf(ctx, "driver.Analytics.GaTrackingID is not set") } else { timing := gamp.NewTiming(time.Since(started)) timing.TrackingID = d.Analytics.GaTrackingID // TODO: What to do if different FB bots have different Tacking IDs? Can FB handler get messages for different bots? If not (what probably is the case) can we get ID from bot settings instead of driver? if err := measurementSender.Queue(timing); err != nil { - log.Errorf(c, "Failed to log timing to GA: %v", err) + log.Errorf(ctx, "Failed to log timing to GA: %v", err) } } } @@ -129,17 +164,18 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook reportError := func(recovered interface{}) { messageText := fmt.Sprintf("Server error (panic): %v\n\n%v", recovered, d.panicTextFooter) stack := string(debug.Stack()) - log.Criticalf(c, "Panic recovered: %s\n%s", messageText, stack) + log.Criticalf(ctx, "Panic recovered: %s\n%s", messageText, stack) if sendStats { // Zero if GA is disabled - d.reportErrorToGA(c, whc, measurementSender, messageText) + d.reportErrorToGA(ctx, whc, measurementSender, messageText) } if whc != nil { - if chatID, err := whc.BotChatID(); err == nil && chatID != "" { + var chatID string + if chatID, err = whc.Input().BotChatID(); err == nil && chatID != "" { if responder := whc.Responder(); responder != nil { - if _, err := responder.SendMessage(c, whc.NewMessage(ErrorIcon+" "+messageText), botsfw.BotAPISendMessageOverResponse); err != nil { - log.Errorf(c, fmt.Errorf("failed to report error to user: %w", err).Error()) + if _, err = responder.SendMessage(ctx, whc.NewMessage(ErrorIcon+" "+messageText), botsfw.BotAPISendMessageOverResponse); err != nil { + log.Errorf(ctx, fmt.Errorf("failed to report error to user: %w", err).Error()) } } } @@ -149,56 +185,59 @@ func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhook if recovered := recover(); recovered != nil { reportError(recovered) } else if sendStats { - log.Debugf(c, "Flushing GA...") + log.Debugf(ctx, "Flushing GA...") if err = measurementSender.Flush(); err != nil { - log.Warningf(c, "Failed to flush to GA: %v", err) + log.Warningf(ctx, "Failed to flush to GA: %v", err) } else { - log.Debugf(c, "Sent to GA: %v items", measurementSender.QueueDepth()) + log.Debugf(ctx, "Sent to GA: %v items", measurementSender.QueueDepth()) } } else { - log.Debugf(c, "GA: sendStats=false") + log.Debugf(ctx, "GA: sendStats=false") } }() - //botCoreStores := webhookHandler.CreateBotCoreStores(d.appContext, r) - //defer func() { - // if whc != nil { // TODO: How do deal with Facebook multiple entries per request? - // //log.Debugf(c, "Closing BotChatStore...") - // //chatData := whc.ChatData() - // //if chatData != nil && chatData.GetPreferredLanguage() == "" { - // // chatData.SetPreferredLanguage(whc.DefaultLocale().Code5) - // //} - // } - //}() - - for _, entryWithInputs := range entriesWithInputs { - for i, input := range entryWithInputs.Inputs { - if input == nil { - panic(fmt.Sprintf("entryWithInputs.Inputs[%d] == nil", i)) - } - d.logInput(c, i, input) - var db dal.DB - if db, err = botContext.BotSettings.GetDatabase(c); err != nil { - err = fmt.Errorf("failed to get bot database: %w", err) - return + if input == nil { + panic(fmt.Sprintf("entryWithInputs.Inputs[%d] == nil", i)) + } + d.logInput(ctx, i, input) + var db dal.DB + if db, err = botContext.BotSettings.GetDatabase(ctx); err != nil { + err = fmt.Errorf("failed to get bot database: %w", err) + return + } + err = db.RunReadwriteTransaction(ctx, func(ctx context.Context, tx dal.ReadwriteTransaction) (err error) { + whcArgs := botsfw.NewCreateWebhookContextArgs(r, botContext.AppContext, *botContext, input, tx, measurementSender) + if whc, err = webhookHandler.CreateWebhookContext(whcArgs); err != nil { + handleError(err, "Failed to create WebhookContext") + return + } + chatData := whc.ChatData() + if chatData.GetAppUserID() == "" { + platformID := whc.BotPlatform().ID() + botID := whc.GetBotCode() + appContext := whc.AppContext() + var appUser record.DataWithID[string, botsfwmodels.AppUserData] + bot := botsdal.Bot{ + Platform: botsfwconst.Platform(platformID), + ID: botID, + User: whc.Input().GetSender(), } - err = db.RunReadwriteTransaction(c, func(ctx context.Context, tx dal.ReadwriteTransaction) error { - whcArgs := botsfw.NewCreateWebhookContextArgs(r, botContext.AppContext, *botContext, input, tx, measurementSender) - if whc, err = webhookHandler.CreateWebhookContext(whcArgs); err != nil { - handleError(err, "Failed to create WebhookContext") - return err - } - responder := webhookHandler.GetResponder(w, whc) // TODO: Move inside webhookHandler.CreateWebhookContext()? - router := botContext.BotSettings.Profile.Router() - router.Dispatch(webhookHandler, responder, whc) // TODO: Should we return err and handle it here? - return nil - }) - if err != nil { - handleError(err, fmt.Sprintf("Failed to run transaction for entriesWithInputs[%d]", i)) + if appUser, err = appContext.CreateAppUserFromBotUser(ctx, tx, bot); err != nil { return } + chatData.SetAppUserID(appUser.ID) } + + responder := webhookHandler.GetResponder(w, whc) // TODO: Move inside webhookHandler.CreateWebhookContext()? + router := botContext.BotSettings.Profile.Router() + router.Dispatch(webhookHandler, responder, whc) // TODO: Should we return err and handle it here? + return + }) + if err != nil { + handleError(err, fmt.Sprintf("Failed to run transaction for entriesWithInputs[%d]", i)) + return } + return } func (BotDriver) invalidContextOrInputs(c context.Context, w http.ResponseWriter, r *http.Request, botContext *botsfw.BotContext, entriesWithInputs []botsfw.EntryInputs, err error) bool { @@ -274,12 +313,12 @@ func (BotDriver) reportErrorToGA(c context.Context, whc botsfw.WebhookContext, m } } -func (BotDriver) logInput(c context.Context, i int, input botsfw.WebhookInput) { +func (BotDriver) logInput(c context.Context, i int, input botinput.WebhookInput) { sender := input.GetSender() switch input := input.(type) { - case botsfw.WebhookTextMessage: + case botinput.WebhookTextMessage: log.Debugf(c, "BotUser#%v(%v %v) => text: %v", sender.GetID(), sender.GetFirstName(), sender.GetLastName(), input.Text()) - case botsfw.WebhookNewChatMembersMessage: + case botinput.WebhookNewChatMembersMessage: newMembers := input.NewChatMembers() var b bytes.Buffer b.WriteString(fmt.Sprintf("NewChatMembers: %d", len(newMembers))) @@ -287,17 +326,17 @@ func (BotDriver) logInput(c context.Context, i int, input botsfw.WebhookInput) { b.WriteString(fmt.Sprintf("\t%d: (%v) - %v %v", i+1, member.GetUserName(), member.GetFirstName(), member.GetLastName())) } log.Debugf(c, b.String()) - case botsfw.WebhookContactMessage: - log.Debugf(c, "BotUser#%v(%v %v) => Contact(name: %v|%v, phone number: %v)", sender.GetID(), sender.GetFirstName(), sender.GetLastName(), input.FirstName(), input.LastName(), input.PhoneNumber()) - case botsfw.WebhookCallbackQuery: + case botinput.WebhookContactMessage: + log.Debugf(c, "BotUser#%v(%v %v) => Contact(botUserID=%s, firstName=%s)", sender.GetID(), sender.GetFirstName(), sender.GetLastName(), input.GetBotUserID(), input.GetFirstName()) + case botinput.WebhookCallbackQuery: callbackData := input.GetData() log.Debugf(c, "BotUser#%v(%v %v) => callback: %v", sender.GetID(), sender.GetFirstName(), sender.GetLastName(), callbackData) - case botsfw.WebhookInlineQuery: + case botinput.WebhookInlineQuery: log.Debugf(c, "BotUser#%v(%v %v) => inline query: %v", sender.GetID(), sender.GetFirstName(), sender.GetLastName(), input.GetQuery()) - case botsfw.WebhookChosenInlineResult: + case botinput.WebhookChosenInlineResult: log.Debugf(c, "BotUser#%v(%v %v) => chosen InlineMessageID: %v", sender.GetID(), sender.GetFirstName(), sender.GetLastName(), input.GetInlineMessageID()) - case botsfw.WebhookReferralMessage: - log.Debugf(c, "BotUser#%v(%v %v) => text: %v", sender.GetID(), sender.GetFirstName(), sender.GetLastName(), input.(botsfw.WebhookTextMessage).Text()) + case botinput.WebhookReferralMessage: + log.Debugf(c, "BotUser#%v(%v %v) => text: %v", sender.GetID(), sender.GetFirstName(), sender.GetLastName(), input.(botinput.WebhookTextMessage).Text()) default: log.Warningf(c, "Unhandled input[%v] type: %T", i, input) }