Merge remote-tracking branch 'upstream/dev' into dev-handlers

pull/40/head
Abhinav Dahiya 9 years ago
commit 6302d59931
  1. 8
      README.md
  2. 14
      bot.go
  3. 52
      bot_test.go
  4. 31
      configs.go
  5. 46
      helpers.go
  6. 39
      types.go
  7. 10
      types_test.go

@ -17,7 +17,7 @@ something with plugins and command handlers without having to design
all that yourself. all that yourself.
Use `github.com/go-telegram-bot-api/telegram-bot-api` for the latest Use `github.com/go-telegram-bot-api/telegram-bot-api` for the latest
version, or use `gopkg.in/telegram-bot-api.v1` for the stable build. version, or use `gopkg.in/telegram-bot-api.v2` for the stable build.
## Example ## Example
@ -29,7 +29,7 @@ package main
import ( import (
"log" "log"
"gopkg.in/telegram-bot-api.v1" "gopkg.in/telegram-bot-api.v2"
) )
func main() { func main() {
@ -65,7 +65,7 @@ you may use a slightly different method.
package main package main
import ( import (
"gopkg.in/telegram-bot-api.v1" "gopkg.in/telegram-bot-api.v2"
"log" "log"
"net/http" "net/http"
) )
@ -85,7 +85,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
updates, _ := bot.ListenForWebhook("/" + bot.Token) updates := bot.ListenForWebhook("/" + bot.Token)
go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)
for update := range updates { for update := range updates {

@ -135,7 +135,7 @@ func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse,
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusForbidden { if resp.StatusCode == http.StatusForbidden {
return APIResponse{}, errors.New(APIForbidden) return APIResponse{}, errors.New(ErrAPIForbidden)
} }
bytes, err := ioutil.ReadAll(resp.Body) bytes, err := ioutil.ReadAll(resp.Body)
@ -217,7 +217,7 @@ func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldna
ms.WriteReader(fieldname, f.Name, int64(len(data)), buf) ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
default: default:
return APIResponse{}, errors.New("bad file type") return APIResponse{}, errors.New(ErrBadFileType)
} }
method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint) method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
@ -523,10 +523,10 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
} }
// ListenForWebhook registers a http handler for a webhook. // ListenForWebhook registers a http handler for a webhook.
func (bot *BotAPI) ListenForWebhook(pattern string) (<-chan Update, http.Handler) { func (bot *BotAPI) ListenForWebhook(pattern string) <-chan Update {
updatesChan := make(chan Update, 100) updatesChan := make(chan Update, 100)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
bytes, _ := ioutil.ReadAll(r.Body) bytes, _ := ioutil.ReadAll(r.Body)
var update Update var update Update
@ -535,9 +535,7 @@ func (bot *BotAPI) ListenForWebhook(pattern string) (<-chan Update, http.Handler
updatesChan <- update updatesChan <- update
}) })
http.HandleFunc(pattern, handler) return updatesChan
return updatesChan, handler
} }
// AnswerInlineQuery sends a response to an inline query. // AnswerInlineQuery sends a response to an inline query.
@ -556,5 +554,7 @@ func (bot *BotAPI) AnswerInlineQuery(config InlineConfig) (APIResponse, error) {
} }
v.Add("results", string(data)) v.Add("results", string(data))
bot.debugLog("answerInlineQuery", v, nil)
return bot.MakeRequest("answerInlineQuery", v) return bot.MakeRequest("answerInlineQuery", v)
} }

@ -5,9 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"net/http/httptest"
"os" "os"
"strings"
"testing" "testing"
) )
@ -351,20 +349,6 @@ func TestGetUserProfilePhotos(t *testing.T) {
} }
} }
func TestListenForWebhook(t *testing.T) {
bot, _ := getBot(t)
_, handler := bot.ListenForWebhook("/")
req, _ := http.NewRequest("GET", "", strings.NewReader("{}"))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Home page didn't return %v", http.StatusOK)
}
}
func TestSetWebhookWithCert(t *testing.T) { func TestSetWebhookWithCert(t *testing.T) {
bot, _ := getBot(t) bot, _ := getBot(t)
@ -445,10 +429,44 @@ func ExampleNewWebhook() {
log.Fatal(err) log.Fatal(err)
} }
updates, _ := bot.ListenForWebhook("/" + bot.Token) updates := bot.ListenForWebhook("/" + bot.Token)
go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)
for update := range updates { for update := range updates {
log.Printf("%+v\n", update) log.Printf("%+v\n", update)
} }
} }
func ExampleAnswerInlineQuery() {
bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") // create new bot
if err != nil {
log.Panic(err)
}
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates, err := bot.GetUpdatesChan(u)
for update := range updates {
if update.InlineQuery.Query == "" { // if no inline query, ignore it
continue
}
article := tgbotapi.NewInlineQueryResultArticle(update.InlineQuery.ID, "Echo", update.InlineQuery.Query)
article.Description = update.InlineQuery.Query
inlineConf := tgbotapi.InlineConfig{
InlineQueryID: update.InlineQuery.ID,
IsPersonal: true,
CacheTime: 0,
Results: []interface{}{article},
}
if _, err := bot.AnswerInlineQuery(inlineConf); err != nil {
log.Println(err)
}
}
}

@ -3,7 +3,6 @@ package tgbotapi
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"log"
"net/url" "net/url"
"strconv" "strconv"
) )
@ -31,13 +30,20 @@ const (
// API errors // API errors
const ( const (
// APIForbidden happens when a token is bad // ErrAPIForbidden happens when a token is bad
APIForbidden = "forbidden" ErrAPIForbidden = "forbidden"
) )
// Constant values for ParseMode in MessageConfig // Constant values for ParseMode in MessageConfig
const ( const (
ModeMarkdown = "Markdown" ModeMarkdown = "Markdown"
ModeHTML = "HTML"
)
// Library errors
const (
// ErrBadFileType happens when you pass an unknown type
ErrBadFileType = "bad file type"
) )
// Chattable is any config type that can be sent. // Chattable is any config type that can be sent.
@ -61,6 +67,7 @@ type BaseChat struct {
ChannelUsername string ChannelUsername string
ReplyToMessageID int ReplyToMessageID int
ReplyMarkup interface{} ReplyMarkup interface{}
DisableNotification bool
} }
// values returns url.Values representation of BaseChat // values returns url.Values representation of BaseChat
@ -85,13 +92,14 @@ func (chat *BaseChat) values() (url.Values, error) {
v.Add("reply_markup", string(data)) v.Add("reply_markup", string(data))
} }
v.Add("disable_notification", strconv.FormatBool(chat.DisableNotification))
return v, nil return v, nil
} }
// BaseFile is a base type for all file config types. // BaseFile is a base type for all file config types.
type BaseFile struct { type BaseFile struct {
BaseChat BaseChat
FilePath string
File interface{} File interface{}
FileID string FileID string
UseExisting bool UseExisting bool
@ -130,21 +138,14 @@ func (file BaseFile) params() (map[string]string, error) {
params["file_size"] = strconv.Itoa(file.FileSize) params["file_size"] = strconv.Itoa(file.FileSize)
} }
params["disable_notification"] = strconv.FormatBool(file.DisableNotification)
return params, nil return params, nil
} }
// getFile returns the file. // getFile returns the file.
func (file BaseFile) getFile() interface{} { func (file BaseFile) getFile() interface{} {
var result interface{} return file.File
if file.FilePath == "" {
result = file.File
} else {
log.Println("FilePath is deprecated.")
log.Println("Please use BaseFile.File instead.")
result = file.FilePath
}
return result
} }
// useExistingFile returns if the BaseFile has already been uploaded. // useExistingFile returns if the BaseFile has already been uploaded.
@ -516,7 +517,7 @@ type FileReader struct {
// InlineConfig contains information on making an InlineQuery response. // InlineConfig contains information on making an InlineQuery response.
type InlineConfig struct { type InlineConfig struct {
InlineQueryID string `json:"inline_query_id"` InlineQueryID string `json:"inline_query_id"`
Results []InlineQueryResult `json:"results"` Results []interface{} `json:"results"`
CacheTime int `json:"cache_time"` CacheTime int `json:"cache_time"`
IsPersonal bool `json:"is_personal"` IsPersonal bool `json:"is_personal"`
NextOffset string `json:"next_offset"` NextOffset string `json:"next_offset"`

@ -281,3 +281,49 @@ func NewWebhookWithCert(link string, file interface{}) WebhookConfig {
Certificate: file, Certificate: file,
} }
} }
// NewInlineQueryResultArticle creates a new inline query article.
func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle {
return InlineQueryResultArticle{
Type: "article",
ID: id,
Title: title,
MessageText: messageText,
}
}
// NewInlineQueryResultGIF creates a new inline query GIF.
func NewInlineQueryResultGIF(id, url string) InlineQueryResultGIF {
return InlineQueryResultGIF{
Type: "gif",
ID: id,
URL: url,
}
}
// NewInlineQueryResultMPEG4GIF creates a new inline query MPEG4 GIF.
func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF {
return InlineQueryResultMPEG4GIF{
Type: "mpeg4_gif",
ID: id,
URL: url,
}
}
// NewInlineQueryResultPhoto creates a new inline query photo.
func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto {
return InlineQueryResultPhoto{
Type: "photo",
ID: id,
URL: url,
}
}
// NewInlineQueryResultVideo creates a new inline query video.
func NewInlineQueryResultVideo(id, url string) InlineQueryResultVideo {
return InlineQueryResultVideo{
Type: "video",
ID: id,
URL: url,
}
}

@ -3,7 +3,6 @@ package tgbotapi
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"strings" "strings"
"time" "time"
) )
@ -147,15 +146,6 @@ func (m *Message) Time() time.Time {
return time.Unix(int64(m.Date), 0) return time.Unix(int64(m.Date), 0)
} }
// IsGroup returns if the message was sent to a group.
//
// Deprecated in favor of Chat.IsGroup.
func (m *Message) IsGroup() bool {
log.Println("Message.IsGroup is deprecated.")
log.Println("Please use Chat.IsGroup instead.")
return m.Chat.IsGroup()
}
// IsCommand returns true if message starts with '/'. // IsCommand returns true if message starts with '/'.
func (m *Message) IsCommand() bool { func (m *Message) IsCommand() bool {
return m.Text != "" && m.Text[0] == '/' return m.Text != "" && m.Text[0] == '/'
@ -163,11 +153,13 @@ func (m *Message) IsCommand() bool {
// Command checks if the message was a command and if it was, returns the // Command checks if the message was a command and if it was, returns the
// command. If the Message was not a command, it returns an empty string. // command. If the Message was not a command, it returns an empty string.
//
// If the command contains the at bot syntax, it removes the bot name.
func (m *Message) Command() string { func (m *Message) Command() string {
if !m.IsCommand() { if !m.IsCommand() {
return "" return ""
} }
command := strings.SplitN(m.Text, " ", 2)[0] command := strings.SplitN(m.Text, " ", 2)[0][1:]
if i := strings.Index(command, "@"); i != -1 { if i := strings.Index(command, "@"); i != -1 {
command = command[:i] command = command[:i]
@ -263,7 +255,7 @@ type Location struct {
Latitude float32 `json:"latitude"` Latitude float32 `json:"latitude"`
} }
// UserProfilePhotos contains information a set of user profile photos. // UserProfilePhotos contains a set of user profile photos.
type UserProfilePhotos struct { type UserProfilePhotos struct {
TotalCount int `json:"total_count"` TotalCount int `json:"total_count"`
Photos []PhotoSize `json:"photos"` Photos []PhotoSize `json:"photos"`
@ -307,20 +299,15 @@ type ForceReply struct {
// InlineQuery is a Query from Telegram for an inline request. // InlineQuery is a Query from Telegram for an inline request.
type InlineQuery struct { type InlineQuery struct {
ID string `json:"id"` ID string `json:"id"`
From User `json:"user"` From User `json:"from"`
Query string `json:"query"` Query string `json:"query"`
Offset string `json:"offset"` Offset string `json:"offset"`
} }
// InlineQueryResult is the base type that all InlineQuery Results have.
type InlineQueryResult struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
}
// InlineQueryResultArticle is an inline query response article. // InlineQueryResultArticle is an inline query response article.
type InlineQueryResultArticle struct { type InlineQueryResultArticle struct {
InlineQueryResult Type string `json:"type"` // required
ID string `json:"id"` // required
Title string `json:"title"` // required Title string `json:"title"` // required
MessageText string `json:"message_text"` // required MessageText string `json:"message_text"` // required
ParseMode string `json:"parse_mode"` ParseMode string `json:"parse_mode"`
@ -335,7 +322,8 @@ type InlineQueryResultArticle struct {
// InlineQueryResultPhoto is an inline query response photo. // InlineQueryResultPhoto is an inline query response photo.
type InlineQueryResultPhoto struct { type InlineQueryResultPhoto struct {
InlineQueryResult Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"photo_url"` // required URL string `json:"photo_url"` // required
MimeType string `json:"mime_type"` MimeType string `json:"mime_type"`
Width int `json:"photo_width"` Width int `json:"photo_width"`
@ -351,7 +339,8 @@ type InlineQueryResultPhoto struct {
// InlineQueryResultGIF is an inline query response GIF. // InlineQueryResultGIF is an inline query response GIF.
type InlineQueryResultGIF struct { type InlineQueryResultGIF struct {
InlineQueryResult Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"gif_url"` // required URL string `json:"gif_url"` // required
Width int `json:"gif_width"` Width int `json:"gif_width"`
Height int `json:"gif_height"` Height int `json:"gif_height"`
@ -365,7 +354,8 @@ type InlineQueryResultGIF struct {
// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. // InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF.
type InlineQueryResultMPEG4GIF struct { type InlineQueryResultMPEG4GIF struct {
InlineQueryResult Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"mpeg4_url"` // required URL string `json:"mpeg4_url"` // required
Width int `json:"mpeg4_width"` Width int `json:"mpeg4_width"`
Height int `json:"mpeg4_height"` Height int `json:"mpeg4_height"`
@ -379,7 +369,8 @@ type InlineQueryResultMPEG4GIF struct {
// InlineQueryResultVideo is an inline query response video. // InlineQueryResultVideo is an inline query response video.
type InlineQueryResultVideo struct { type InlineQueryResultVideo struct {
InlineQueryResult Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"video_url"` // required URL string `json:"video_url"` // required
MimeType string `json:"mime_type"` // required MimeType string `json:"mime_type"` // required
MessageText string `json:"message_text"` // required MessageText string `json:"message_text"` // required

@ -58,7 +58,7 @@ func TestIsCommandWithEmptyText(t *testing.T) {
func TestCommandWithCommand(t *testing.T) { func TestCommandWithCommand(t *testing.T) {
message := tgbotapi.Message{Text: "/command"} message := tgbotapi.Message{Text: "/command"}
if message.Command() != "/command" { if message.Command() != "command" {
t.Fail() t.Fail()
} }
} }
@ -79,6 +79,14 @@ func TestCommandWithNonCommand(t *testing.T) {
} }
} }
func TestCommandWithBotName(t *testing.T) {
message := tgbotapi.Message{Text: "/command@testbot"}
if message.Command() != "command" {
t.Fail()
}
}
func TestMessageCommandArgumentsWithArguments(t *testing.T) { func TestMessageCommandArgumentsWithArguments(t *testing.T) {
message := tgbotapi.Message{Text: "/command with arguments"} message := tgbotapi.Message{Text: "/command with arguments"}
if message.CommandArguments() != "with arguments" { if message.CommandArguments() != "with arguments" {