Introducing Sendable interface (see #93.)

This commit refactors lots of duplicated code from bot.go, so
instead of having N redundant SendX methods, now it's the
responsibility of all Sendable objects to implement Send(..)
instead. Impl in types_send.go, 150 LOC only!
pull/108/head
Ian Byrd 7 years ago
parent 127bf18d79
commit ef59af6db7
No known key found for this signature in database
GPG Key ID: 598F598CA3B8055F

@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
@ -93,38 +94,24 @@ func (b *Bot) sendFile(method, name, path string, params map[string]string) ([]b
return json, nil
}
func embedSendOptions(params map[string]string, options *SendOptions) {
if options == nil {
return
}
func (b *Bot) sendObject(f *File, what string, params map[string]string) (*Message, error) {
sendWhat := "send" + strings.Title(what)
if options.ReplyTo.ID != 0 {
params["reply_to_message_id"] = strconv.Itoa(options.ReplyTo.ID)
}
var respJSON []byte
var err error
if options.DisableWebPagePreview {
params["disable_web_page_preview"] = "true"
if f.Exists() {
params[what] = f.FileID
respJSON, err = b.sendCommand(sendWhat, params)
} else {
respJSON, err = b.sendFile(sendWhat, what, f.filename, params)
}
if options.DisableNotification {
params["disable_notification"] = "true"
}
if options.ParseMode != ModeDefault {
params["parse_mode"] = string(options.ParseMode)
if err != nil {
return nil, err
}
// Processing force_reply:
{
forceReply := options.ReplyMarkup.ForceReply
customKeyboard := (options.ReplyMarkup.CustomKeyboard != nil)
inlineKeyboard := options.ReplyMarkup.InlineKeyboard != nil
hiddenKeyboard := options.ReplyMarkup.HideCustomKeyboard
if forceReply || customKeyboard || hiddenKeyboard || inlineKeyboard {
replyMarkup, _ := json.Marshal(options.ReplyMarkup)
params["reply_markup"] = string(replyMarkup)
}
}
return extractMsgResponse(respJSON)
}
func (b *Bot) getMe() (User, error) {

405
bot.go

@ -104,404 +104,49 @@ func (b *Bot) poll(
}
}
// SendMessage sends a text message to recipient.
func (b *Bot) SendMessage(recipient Recipient, message string, options *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": recipient.Destination(),
"text": message,
}
if options != nil {
embedSendOptions(params, options)
}
responseJSON, err := b.sendCommand("sendMessage", params)
if err != nil {
return nil, err
}
var responseRecieved struct {
Ok bool
Result Message
Description string
}
func (b *Bot) Send(to Recipient, what interface{}, how ...interface{}) (*Message, error) {
options := extractOptions(how)
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return nil, errors.Wrap(err, "bad response json")
switch object := what.(type) {
case string:
return b.sendText(to, object, options)
case Sendable:
return object.Send(b, to, options)
default:
panic(fmt.Sprintf("telebot: object %v is not Sendable", object))
}
if !responseRecieved.Ok {
return nil, errors.Errorf("api error: %s", responseRecieved.Description)
}
return &responseRecieved.Result, nil
}
// ForwardMessage forwards a message to recipient.
func (b *Bot) ForwardMessage(recipient Recipient, message Message) error {
params := map[string]string{
"chat_id": recipient.Destination(),
"from_chat_id": strconv.Itoa(message.Origin().ID),
"message_id": strconv.Itoa(message.ID),
func (b *Bot) Reply(to *Message, what interface{}, how ...interface{}) (*Message, error) {
options := extractOptions(how)
if options == nil {
options = &SendOptions{}
}
responseJSON, err := b.sendCommand("forwardMessage", params)
if err != nil {
return err
}
options.ReplyTo = to
var responseRecieved struct {
Ok bool
Description string
}
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return errors.Wrap(err, "bad response json")
}
if !responseRecieved.Ok {
return errors.Errorf("api error: %s", responseRecieved.Description)
}
return nil
return b.Send(to.Chat, what, options)
}
// SendPhoto sends a photo object to recipient.
//
// On success, photo object would be aliased to its copy on
// the Telegram servers, so sending the same photo object
// again, won't issue a new upload, but would make a use
// of existing file on Telegram servers.
func (b *Bot) SendPhoto(recipient Recipient, photo *Photo, options *SendOptions) (*Message, error) {
func (b *Bot) Forward(to Recipient, what *Message, how ...interface{}) (*Message, error) {
params := map[string]string{
"chat_id": recipient.Destination(),
"caption": photo.Caption,
"chat_id": to.Destination(),
"from_chat_id": strconv.Itoa(what.Origin().ID),
"message_id": strconv.Itoa(what.ID),
}
if options != nil {
embedSendOptions(params, options)
}
var responseJSON []byte
var err error
if photo.Exists() {
params["photo"] = photo.FileID
responseJSON, err = b.sendCommand("sendPhoto", params)
} else {
responseJSON, err = b.sendFile("sendPhoto", "photo", photo.filename, params)
options := extractOptions(how)
if options == nil {
options = &SendOptions{}
}
embedSendOptions(params, options)
respJSON, err := b.sendCommand("forwardMessage", params)
if err != nil {
return nil, err
}
var responseRecieved struct {
Ok bool
Result Message
Description string
}
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return nil, errors.Wrap(err, "bad response json")
}
if !responseRecieved.Ok {
return nil, errors.Errorf("api error: %s", responseRecieved.Description)
}
thumbnails := &responseRecieved.Result.Photo
filename := photo.filename
photo.File = (*thumbnails)[len(*thumbnails)-1].File
photo.filename = filename
return &responseRecieved.Result, nil
}
// SendAudio sends an audio object to recipient.
//
// On success, audio object would be aliased to its copy on
// the Telegram servers, so sending the same audio object
// again, won't issue a new upload, but would make a use
// of existing file on Telegram servers.
func (b *Bot) SendAudio(recipient Recipient, audio *Audio, options *SendOptions) error {
params := map[string]string{
"chat_id": recipient.Destination(),
}
if options != nil {
embedSendOptions(params, options)
}
var responseJSON []byte
var err error
if audio.Exists() {
params["audio"] = audio.FileID
responseJSON, err = b.sendCommand("sendAudio", params)
} else {
responseJSON, err = b.sendFile("sendAudio", "audio", audio.filename, params)
}
if err != nil {
return err
}
var responseRecieved struct {
Ok bool
Result Message
Description string
}
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return errors.Wrap(err, "bad response json")
}
if !responseRecieved.Ok {
return errors.Errorf("api error: %s", responseRecieved.Description)
}
filename := audio.filename
*audio = *responseRecieved.Result.Audio
audio.filename = filename
return nil
}
// SendDocument sends a general document object to recipient.
//
// On success, document object would be aliased to its copy on
// the Telegram servers, so sending the same document object
// again, won't issue a new upload, but would make a use
// of existing file on Telegram servers.
func (b *Bot) SendDocument(recipient Recipient, doc *Document, options *SendOptions) error {
params := map[string]string{
"chat_id": recipient.Destination(),
}
if options != nil {
embedSendOptions(params, options)
}
var responseJSON []byte
var err error
if doc.Exists() {
params["document"] = doc.FileID
responseJSON, err = b.sendCommand("sendDocument", params)
} else {
responseJSON, err = b.sendFile("sendDocument", "document", doc.filename, params)
}
if err != nil {
return err
}
var responseRecieved struct {
Ok bool
Result Message
Description string
}
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return errors.Wrap(err, "bad response json")
}
if !responseRecieved.Ok {
return errors.Errorf("api error: %s", responseRecieved.Description)
}
filename := doc.filename
*doc = *responseRecieved.Result.Document
doc.filename = filename
return nil
}
// SendSticker sends a general document object to recipient.
//
// On success, sticker object would be aliased to its copy on
// the Telegram servers, so sending the same sticker object
// again, won't issue a new upload, but would make a use
// of existing file on Telegram servers.
func (b *Bot) SendSticker(recipient Recipient, sticker *Sticker, options *SendOptions) error {
params := map[string]string{
"chat_id": recipient.Destination(),
}
if options != nil {
embedSendOptions(params, options)
}
var responseJSON []byte
var err error
if sticker.Exists() {
params["sticker"] = sticker.FileID
responseJSON, err = b.sendCommand("sendSticker", params)
} else {
responseJSON, err = b.sendFile("sendSticker", "sticker", sticker.filename, params)
}
if err != nil {
return err
}
var responseRecieved struct {
Ok bool
Result Message
Description string
}
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return errors.Wrap(err, "bad response json")
}
if !responseRecieved.Ok {
return errors.Errorf("api error: %s", responseRecieved.Description)
}
filename := sticker.filename
*sticker = *responseRecieved.Result.Sticker
sticker.filename = filename
return nil
}
// SendVideo sends a general document object to recipient.
//
// On success, video object would be aliased to its copy on
// the Telegram servers, so sending the same video object
// again, won't issue a new upload, but would make a use
// of existing file on Telegram servers.
func (b *Bot) SendVideo(recipient Recipient, video *Video, options *SendOptions) error {
params := map[string]string{
"chat_id": recipient.Destination(),
}
if options != nil {
embedSendOptions(params, options)
}
var responseJSON []byte
var err error
if video.Exists() {
params["video"] = video.FileID
responseJSON, err = b.sendCommand("sendVideo", params)
} else {
responseJSON, err = b.sendFile("sendVideo", "video", video.filename, params)
}
if err != nil {
return err
}
var responseRecieved struct {
Ok bool
Result Message
Description string
}
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return errors.Wrap(err, "bad response json")
}
if !responseRecieved.Ok {
return errors.Errorf("api error: %s", responseRecieved.Description)
}
filename := video.filename
*video = *responseRecieved.Result.Video
video.filename = filename
return nil
}
// SendLocation sends a general document object to recipient.
//
// On success, video object would be aliased to its copy on
// the Telegram servers, so sending the same video object
// again, won't issue a new upload, but would make a use
// of existing file on Telegram servers.
func (b *Bot) SendLocation(recipient Recipient, geo *Location, options *SendOptions) error {
params := map[string]string{
"chat_id": recipient.Destination(),
"latitude": fmt.Sprintf("%f", geo.Latitude),
"longitude": fmt.Sprintf("%f", geo.Longitude),
}
if options != nil {
embedSendOptions(params, options)
}
responseJSON, err := b.sendCommand("sendLocation", params)
if err != nil {
return err
}
var responseRecieved struct {
Ok bool
Result Message
Description string
}
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return errors.Wrap(err, "bad response json")
}
if !responseRecieved.Ok {
return errors.Errorf("api error: %s", responseRecieved.Description)
}
return nil
}
// SendVenue sends a venue object to recipient.
func (b *Bot) SendVenue(recipient Recipient, venue *Venue, options *SendOptions) error {
params := map[string]string{
"chat_id": recipient.Destination(),
"latitude": fmt.Sprintf("%f", venue.Location.Latitude),
"longitude": fmt.Sprintf("%f", venue.Location.Longitude),
"title": venue.Title,
"address": venue.Address}
if venue.FoursquareID != "" {
params["foursquare_id"] = venue.FoursquareID
}
if options != nil {
embedSendOptions(params, options)
}
responseJSON, err := b.sendCommand("sendVenue", params)
if err != nil {
return err
}
var responseRecieved struct {
Ok bool
Result Message
Description string
}
err = json.Unmarshal(responseJSON, &responseRecieved)
if err != nil {
return errors.Wrap(err, "bad response json")
}
if !responseRecieved.Ok {
return errors.Errorf("api error: %s", responseRecieved.Description)
}
return nil
return extractMsgResponse(respJSON)
}
// SendChatAction updates a chat action for recipient.

@ -53,7 +53,7 @@ type Message struct {
Location *Location `json:"location"`
// A group chat message belongs to.
Chat Chat `json:"chat"`
Chat *Chat `json:"chat"`
// For a service message, represents a user,
// that just got added to chat, this message came from.

@ -1,5 +1,27 @@
package telebot
type Option int
const (
// SendOptions.DisableWebPagePreview
NoPreview Option = iota
// SendOptions.DisableNotification
Silent
// ReplyMarkup.ForceReply
ForceReply
// ReplyMarkup.HideCustomKeyboard
HideKeyboard
// ReplyMarkup.ResizeKeyboard
ResizeKeyboard
// ReplyMarkup.OneTimeKeyboard
OneTimeKeyboard
)
// SendOptions represents a set of custom options that could
// be appled to messages sent.
type SendOptions struct {
@ -21,6 +43,8 @@ type SendOptions struct {
// ReplyMarkup specifies convenient options for bot-user communications.
type ReplyMarkup struct {
InlineKeyboard [][]KeyboardButton `json:"inline_keyboard,omitempty"`
// ForceReply forces Telegram clients to display
// a reply interface to the user (act as if the user
// has selected the bots message and tapped "Reply").
@ -31,20 +55,20 @@ type ReplyMarkup struct {
// Note: you don't need to set HideCustomKeyboard field to show custom keyboard.
CustomKeyboard [][]string `json:"keyboard,omitempty"`
InlineKeyboard [][]KeyboardButton `json:"inline_keyboard,omitempty"`
// Requests clients to hide the custom keyboard.
//
// Note: You dont need to set CustomKeyboard field to hide custom keyboard.
HideCustomKeyboard bool `json:"hide_keyboard,omitempty"`
// Requests clients to resize the keyboard vertically for optimal fit
// (e.g., make the keyboard smaller if there are just two rows of buttons).
// Defaults to false, in which case the custom keyboard is always of the
// same height as the app's standard keyboard.
ResizeKeyboard bool `json:"resize_keyboard,omitempty"`
// Requests clients to hide the keyboard as soon as it's been used. Defaults to false.
OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"`
// Requests clients to hide the custom keyboard.
//
// Note: You dont need to set CustomKeyboard field to hide custom keyboard.
HideCustomKeyboard bool `json:"hide_keyboard,omitempty"`
// Requests clients to hide the keyboard as soon as it's been used.
// Defaults to false.
OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"`
// Use this param if you want to force reply from
// specific users only.

@ -2,6 +2,11 @@ package telebot
import "strconv"
// Sendable is any object that can send itself.
type Sendable interface {
Send(*Bot, Recipient, *SendOptions) (*Message, error)
}
// Recipient is basically any possible endpoint you can send
// messages to. It's usually a distinct user or a chat.
type Recipient interface {
@ -21,7 +26,7 @@ type User struct {
}
// Destination is internal user ID.
func (u User) Destination() string {
func (u *User) Destination() string {
return strconv.Itoa(u.ID)
}
@ -43,7 +48,7 @@ type Chat struct {
}
// Destination is internal chat ID.
func (c Chat) Destination() string {
func (c *Chat) Destination() string {
ret := "@" + c.Username
if c.Type != "channel" {
ret = strconv.FormatInt(c.ID, 10)
@ -52,7 +57,7 @@ func (c Chat) Destination() string {
}
// IsGroupChat returns true if chat object represents a group chat.
func (c Chat) IsGroupChat() bool {
func (c *Chat) IsGroupChat() bool {
return c.Type != "private"
}

@ -0,0 +1,146 @@
package telebot
import "fmt"
func (b *Bot) sendText(to Recipient, text string, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Destination(),
"text": text,
}
embedSendOptions(params, opt)
respJSON, err := b.sendCommand("sendMessage", params)
if err != nil {
return nil, err
}
return extractMsgResponse(respJSON)
}
func (p *Photo) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Destination(),
"caption": p.Caption,
}
embedSendOptions(params, opt)
msg, err := b.sendObject(&p.File, "photo", params)
if err != nil {
return nil, err
}
thumbnails := msg.Photo
fname := p.filename
p.File = thumbnails[len(thumbnails)-1].File
p.filename = fname
return msg, nil
}
func (a *Audio) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Destination(),
}
embedSendOptions(params, opt)
msg, err := b.sendObject(&a.File, "audio", params)
if err != nil {
return nil, err
}
fname := a.filename
*a = *msg.Audio
a.filename = fname
return msg, nil
}
func (d *Document) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Destination(),
}
embedSendOptions(params, opt)
msg, err := b.sendObject(&d.File, "audio", params)
if err != nil {
return nil, err
}
fname := d.filename
*d = *msg.Document
d.filename = fname
return msg, nil
}
func (s *Sticker) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Destination(),
}
embedSendOptions(params, opt)
msg, err := b.sendObject(&s.File, "sticker", params)
if err != nil {
return nil, err
}
fname := s.filename
*s = *msg.Sticker
s.filename = fname
return msg, nil
}
func (v *Video) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Destination(),
}
embedSendOptions(params, opt)
msg, err := b.sendObject(&v.File, "video", params)
if err != nil {
return nil, err
}
fname := v.filename
*v = *msg.Video
v.filename = fname
return msg, nil
}
func (x *Location) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Destination(),
"latitude": fmt.Sprintf("%f", x.Latitude),
"longitude": fmt.Sprintf("%f", x.Longitude),
}
embedSendOptions(params, opt)
respJSON, err := b.sendCommand("sendLocation", params)
if err != nil {
return nil, err
}
return extractMsgResponse(respJSON)
}
func (v *Venue) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Destination(),
"latitude": fmt.Sprintf("%f", v.Location.Latitude),
"longitude": fmt.Sprintf("%f", v.Location.Longitude),
"title": v.Title,
"address": v.Address,
"foursquare_id": v.FoursquareID,
}
embedSendOptions(params, opt)
respJSON, err := b.sendCommand("sendLocation", params)
if err != nil {
return nil, err
}
return extractMsgResponse(respJSON)
}

@ -0,0 +1,85 @@
package telebot
import (
"encoding/json"
"fmt"
"strconv"
"github.com/pkg/errors"
)
func extractMsgResponse(respJSON []byte) (*Message, error) {
var resp struct {
Ok bool
Result *Message
Description string
}
err := json.Unmarshal(respJSON, &resp)
if err != nil {
return nil, errors.Wrap(err, "bad response json")
}
if !resp.Ok {
return nil, errors.Errorf("api error: %s", resp.Description)
}
return resp.Result, nil
}
func extractOptions(how []interface{}) *SendOptions {
var options *SendOptions
for _, object := range how {
switch option := object.(type) {
case *SendOptions:
options = option
break
case *ReplyMarkup:
if options == nil {
options = &SendOptions{}
}
options.ReplyMarkup = option
break
default:
panic(fmt.Sprintf("telebot: %v is not a send-option", option))
}
}
return options
}
func embedSendOptions(params map[string]string, opt *SendOptions) {
if opt == nil {
return
}
if opt.ReplyTo.ID != 0 {
params["reply_to_message_id"] = strconv.Itoa(opt.ReplyTo.ID)
}
if opt.DisableWebPagePreview {
params["disable_web_page_preview"] = "true"
}
if opt.DisableNotification {
params["disable_notification"] = "true"
}
if opt.ParseMode != ModeDefault {
params["parse_mode"] = string(opt.ParseMode)
}
if opt.ReplyMarkup != nil {
forceReply := opt.ReplyMarkup.ForceReply
customKeyboard := (opt.ReplyMarkup.CustomKeyboard != nil)
inlineKeyboard := opt.ReplyMarkup.InlineKeyboard != nil
hiddenKeyboard := opt.ReplyMarkup.HideCustomKeyboard
if forceReply || customKeyboard || hiddenKeyboard || inlineKeyboard {
replyMarkup, _ := json.Marshal(opt.ReplyMarkup)
params["reply_markup"] = string(replyMarkup)
}
}
}
Loading…
Cancel
Save