Add inline query and standalone client support

This commit is contained in:
Hamcha 2018-09-14 18:01:37 +02:00
parent 9804f85677
commit 923d9e245f
Signed by: hamcha
GPG Key ID: A40413D21021EAEE
8 changed files with 134 additions and 29 deletions

29
api.go
View File

@ -135,8 +135,9 @@ type APILocation struct {
// APIUpdate represents the "Update" JSON structure // APIUpdate represents the "Update" JSON structure
type APIUpdate struct { type APIUpdate struct {
UpdateID int64 `json:"update_id"` UpdateID int64 `json:"update_id"`
Message APIMessage `json:"message"` Message *APIMessage `json:"message"`
Inline *APIInlineQuery `json:"inline_query,omitempty"`
} }
// APIFile represents the "File" JSON structure // APIFile represents the "File" JSON structure
@ -152,3 +153,27 @@ type APIResponse struct {
ErrCode *int `json:"error_code,omitempty"` ErrCode *int `json:"error_code,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
} }
// APIInlineQuery represents an inline query from telegram
type APIInlineQuery struct {
QueryID string `json:"id"`
From APIUser `json:"from"`
Location *APILocation `json:"location,omitempty"`
Query string `json:"query"`
Offset string `json:"offset"`
}
// APIInlineQueryResultPhoto is an image result for an inline query
type APIInlineQueryResultPhoto struct {
Type string `json:"type"`
ResultID string `json:"id"`
PhotoURL string `json:"photo_url"`
ThumbURL string `json:"thumb_url"`
Width int `json:"photo_width,omitempty"`
Height int `json:"photo_height,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
//TODO replyMarkup / inputMessageContent
}

View File

@ -86,6 +86,14 @@ func (b *Broker) SendChatAction(chat *APIChat, action ChatAction) {
}) })
} }
// AnswerInlineQuery sends the results of an inline query
func (b *Broker) AnswerInlineQuery(response InlineQueryResponse) {
b.sendCmd(ClientCommand{
Type: CmdAnswerInlineQuery,
InlineQueryResults: &response,
})
}
// GetFile sends a file retrieval request to the Broker. // GetFile sends a file retrieval request to the Broker.
// This function is asynchronous as data will be delivered to the given callback. // This function is asynchronous as data will be delivered to the given callback.
func (b *Broker) GetFile(fileID string, fn BrokerCallback) int { func (b *Broker) GetFile(fileID string, fn BrokerCallback) int {

View File

@ -8,7 +8,7 @@ import (
) )
// UpdateHandler is an update handler for webhook updates // UpdateHandler is an update handler for webhook updates
type UpdateHandler func(broker *Broker, message APIMessage) type UpdateHandler func(broker *Broker, data APIUpdate)
// BrokerCallback is a callback for broker responses to client requests // BrokerCallback is a callback for broker responses to client requests
type BrokerCallback func(broker *Broker, update BrokerUpdate) type BrokerCallback func(broker *Broker, update BrokerUpdate)
@ -50,7 +50,7 @@ func RunBrokerClient(broker *Broker, updateFn UpdateHandler) error {
if update.Callback == nil { if update.Callback == nil {
// It's a generic message: dispatch to UpdateHandler // It's a generic message: dispatch to UpdateHandler
go updateFn(broker, *(update.Message)) go updateFn(broker, *(update.Data))
} else { } else {
// It's a response to a request: retrieve callback and call it // It's a response to a request: retrieve callback and call it
go broker.SpliceCallback(*(update.Callback))(broker, update) go broker.SpliceCallback(*(update.Callback))(broker, update)

View File

@ -23,5 +23,8 @@ func executeClientCommand(action tg.ClientCommand, client net.Conn) {
case tg.CmdSendChatAction: case tg.CmdSendChatAction:
data := *(action.ChatActionData) data := *(action.ChatActionData)
api.SendChatAction(data) api.SendChatAction(data)
case tg.CmdAnswerInlineQuery:
data := *(action.InlineQueryResults)
api.AnswerInlineQuery(data)
} }
} }

View File

@ -6,6 +6,8 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"github.com/hamcha/tg"
) )
// The Config data (parsed from JSON) // The Config data (parsed from JSON)
@ -23,7 +25,7 @@ func assert(err error) {
} }
} }
var api *Telegram var api *tg.Telegram
func main() { func main() {
cfgpath := flag.String("config", "config.json", "Path to configuration file") cfgpath := flag.String("config", "config.json", "Path to configuration file")
@ -37,7 +39,7 @@ func main() {
assert(err) assert(err)
// Create Telegram API object // Create Telegram API object
api = mkAPI(config.Token) api = tg.MakeAPIClient(config.Token)
// Setup webhook handler // Setup webhook handler
go func() { go func() {

View File

@ -20,8 +20,7 @@ func webhook(rw http.ResponseWriter, req *http.Request) {
} }
data, err := json.Marshal(tg.BrokerUpdate{ data, err := json.Marshal(tg.BrokerUpdate{
Type: tg.BMessage, Data: update,
Message: &(update.Message),
Callback: nil, Callback: nil,
}) })
if err != nil { if err != nil {

View File

@ -17,10 +17,10 @@ const (
// BrokerUpdate is what is sent by the broker as update // BrokerUpdate is what is sent by the broker as update
type BrokerUpdate struct { type BrokerUpdate struct {
Type BrokerUpdateType Type BrokerUpdateType
Callback *int `json:",omitempty"` Callback *int `json:",omitempty"`
Error *string `json:",omitempty"` Error *string `json:",omitempty"`
Message *APIMessage `json:",omitempty"` Data *APIUpdate `json:",omitempty"`
Bytes *string `json:",omitempty"` Bytes *string `json:",omitempty"`
} }
// ClientCommandType distinguishes requests sent by clients to the broker // ClientCommandType distinguishes requests sent by clients to the broker
@ -41,6 +41,9 @@ const (
// CmdSendChatAction requests the broker to set a chat action (typing, etc.) // CmdSendChatAction requests the broker to set a chat action (typing, etc.)
CmdSendChatAction ClientCommandType = "sendChatAction" CmdSendChatAction ClientCommandType = "sendChatAction"
// CmdAnswerInlineQuery requests the broker sends results of an inline query
CmdAnswerInlineQuery ClientCommandType = "answerInlineQuery"
) )
// ClientTextMessageData is the required data for a CmdSendTextMessage request // ClientTextMessageData is the required data for a CmdSendTextMessage request
@ -98,6 +101,18 @@ type ClientCommand struct {
PhotoData *ClientPhotoData `json:",omitempty"` PhotoData *ClientPhotoData `json:",omitempty"`
ForwardMessageData *ClientForwardMessageData `json:",omitempty"` ForwardMessageData *ClientForwardMessageData `json:",omitempty"`
ChatActionData *ClientChatActionData `json:",omitempty"` ChatActionData *ClientChatActionData `json:",omitempty"`
InlineQueryResults *InlineQueryResponse `json:",omitempty"`
FileRequestData *FileRequestData `json:",omitempty"` FileRequestData *FileRequestData `json:",omitempty"`
Callback *int `json:",omitempty"` Callback *int `json:",omitempty"`
} }
// InlineQueryResponse is the response to an inline query
type InlineQueryResponse struct {
QueryID string
Results []interface{}
CacheTime *int `json:",omitempty"`
IsPersonal bool `json:",omitempty"`
NextOffset string `json:",omitempty"`
PMText string `json:",omitempty"`
PMParam string `json:",omitempty"`
}

View File

@ -1,4 +1,4 @@
package main package tg
import ( import (
"bytes" "bytes"
@ -13,20 +13,21 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"github.com/hamcha/tg"
) )
// APIEndpoint is Telegram's current Bot API base url endpoint // APIEndpoint is Telegram's current Bot API base url endpoint
const APIEndpoint = "https://api.telegram.org/" const APIEndpoint = "https://api.telegram.org/"
// WebhookHandler is a function that handles updates
type WebhookHandler func(APIUpdate)
// Telegram is the API client for the Telegram Bot API // Telegram is the API client for the Telegram Bot API
type Telegram struct { type Telegram struct {
Token string Token string
} }
// mkAPI creates a Telegram instance from a Bot API token // MakeAPIClient creates a Telegram instance from a Bot API token
func mkAPI(token string) *Telegram { func MakeAPIClient(token string) *Telegram {
tg := new(Telegram) tg := new(Telegram)
tg.Token = token tg.Token = token
return tg return tg
@ -37,7 +38,7 @@ func (t Telegram) SetWebhook(webhook string) {
resp, err := http.PostForm(t.apiURL("setWebhook"), url.Values{"url": {webhook}}) resp, err := http.PostForm(t.apiURL("setWebhook"), url.Values{"url": {webhook}})
if !checkerr("SetWebhook/http.PostForm", err) { if !checkerr("SetWebhook/http.PostForm", err) {
defer resp.Body.Close() defer resp.Body.Close()
var result tg.APIResponse var result APIResponse
err = json.NewDecoder(resp.Body).Decode(&result) err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil { if err != nil {
log.Println("[SetWebhook] Could not read reply: " + err.Error()) log.Println("[SetWebhook] Could not read reply: " + err.Error())
@ -52,8 +53,27 @@ func (t Telegram) SetWebhook(webhook string) {
} }
} }
// HandleWebhook is a webhook HTTP handler for standalone bots
func (t Telegram) HandleWebhook(bind string, webhook string, handler WebhookHandler) error {
whmux := http.NewServeMux()
whmux.HandleFunc(webhook, func(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
// Re-encode request to ensure conformity
var update APIUpdate
err := json.NewDecoder(req.Body).Decode(&update)
if err != nil {
log.Println("[webhook] Received incorrect request: " + err.Error())
return
}
handler(update)
})
return http.ListenAndServe(bind, whmux)
}
// SendTextMessage sends an HTML-styled text message to a specified chat // SendTextMessage sends an HTML-styled text message to a specified chat
func (t Telegram) SendTextMessage(data tg.ClientTextMessageData) { func (t Telegram) SendTextMessage(data ClientTextMessageData) {
postdata := url.Values{ postdata := url.Values{
"chat_id": {strconv.FormatInt(data.ChatID, 10)}, "chat_id": {strconv.FormatInt(data.ChatID, 10)},
"text": {data.Text}, "text": {data.Text},
@ -67,7 +87,8 @@ func (t Telegram) SendTextMessage(data tg.ClientTextMessageData) {
checkerr("SendTextMessage/http.PostForm", err) checkerr("SendTextMessage/http.PostForm", err)
} }
func (t Telegram) SendPhoto(data tg.ClientPhotoData) { // SendPhoto sends a picture to a chat as a photo
func (t Telegram) SendPhoto(data ClientPhotoData) {
// Decode photo from b64 // Decode photo from b64
photolen := base64.StdEncoding.DecodedLen(len(data.Bytes)) photolen := base64.StdEncoding.DecodedLen(len(data.Bytes))
photobytes := make([]byte, photolen) photobytes := make([]byte, photolen)
@ -114,7 +135,8 @@ func (t Telegram) SendPhoto(data tg.ClientPhotoData) {
checkerr("SendPhoto/http.Do", err) checkerr("SendPhoto/http.Do", err)
} }
func (t Telegram) ForwardMessage(data tg.ClientForwardMessageData) { // ForwardMessage forwards an existing message to a chat
func (t Telegram) ForwardMessage(data ClientForwardMessageData) {
postdata := url.Values{ postdata := url.Values{
"chat_id": {strconv.FormatInt(data.ChatID, 10)}, "chat_id": {strconv.FormatInt(data.ChatID, 10)},
"from_chat_id": {strconv.FormatInt(data.FromChatID, 10)}, "from_chat_id": {strconv.FormatInt(data.FromChatID, 10)},
@ -125,7 +147,8 @@ func (t Telegram) ForwardMessage(data tg.ClientForwardMessageData) {
checkerr("ForwardMessage/http.PostForm", err) checkerr("ForwardMessage/http.PostForm", err)
} }
func (t Telegram) SendChatAction(data tg.ClientChatActionData) { // SendChatAction sends a 5 second long action (X is writing, sending a photo ecc.)
func (t Telegram) SendChatAction(data ClientChatActionData) {
postdata := url.Values{ postdata := url.Values{
"chat_id": {strconv.FormatInt(data.ChatID, 10)}, "chat_id": {strconv.FormatInt(data.ChatID, 10)},
"action": {string(data.Action)}, "action": {string(data.Action)},
@ -135,13 +158,43 @@ func (t Telegram) SendChatAction(data tg.ClientChatActionData) {
checkerr("SendChatAction/http.PostForm", err) checkerr("SendChatAction/http.PostForm", err)
} }
// AnswerInlineQuery replies to an inline query
func (t Telegram) AnswerInlineQuery(data InlineQueryResponse) {
jsonresults, err := json.Marshal(data.Results)
if checkerr("AnswerInlineQuery/json.Marshal", err) {
return
}
postdata := url.Values{
"inline_query_id": {data.QueryID},
"results": {string(jsonresults)},
}
if data.CacheTime != nil {
postdata["cache_time"] = []string{strconv.Itoa(*data.CacheTime)}
}
if data.IsPersonal {
postdata["is_personal"] = []string{"true"}
}
if data.NextOffset != "" {
postdata["next_offset"] = []string{data.NextOffset}
}
if data.PMText != "" {
postdata["switch_pm_text"] = []string{data.PMText}
}
if data.PMParam != "" {
postdata["switch_pm_parameter"] = []string{data.PMParam}
}
_, err = http.PostForm(t.apiURL("answerInlineQuery"), postdata)
checkerr("AnswerInlineQuery/http.PostForm", err)
}
// GetFile sends a "getFile" API call to Telegram's servers and fetches the file // GetFile sends a "getFile" API call to Telegram's servers and fetches the file
// specified afterward. The file will be then send back to the client that requested it // specified afterward. The file will be then send back to the client that requested it
// with the specified callback id. // with the specified callback id.
func (t Telegram) GetFile(data tg.FileRequestData, client net.Conn, callback int) { func (t Telegram) GetFile(data FileRequestData, client net.Conn, callback int) {
fail := func(msg string) { fail := func(msg string) {
errmsg, _ := json.Marshal(tg.BrokerUpdate{ errmsg, _ := json.Marshal(BrokerUpdate{
Type: tg.BError, Type: BError,
Error: &msg, Error: &msg,
Callback: &callback, Callback: &callback,
}) })
@ -159,8 +212,8 @@ func (t Telegram) GetFile(data tg.FileRequestData, client net.Conn, callback int
defer resp.Body.Close() defer resp.Body.Close()
var filespecs = struct { var filespecs = struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Result *tg.APIFile `json:"result,omitempty"` Result *APIFile `json:"result,omitempty"`
}{} }{}
err = json.NewDecoder(resp.Body).Decode(&filespecs) err = json.NewDecoder(resp.Body).Decode(&filespecs)
if checkerr("GetFile/json.Decode", err) { if checkerr("GetFile/json.Decode", err) {
@ -194,8 +247,8 @@ func (t Telegram) GetFile(data tg.FileRequestData, client net.Conn, callback int
} }
b64data := base64.StdEncoding.EncodeToString(rawdata) b64data := base64.StdEncoding.EncodeToString(rawdata)
clientmsg, err := json.Marshal(tg.BrokerUpdate{ clientmsg, err := json.Marshal(BrokerUpdate{
Type: tg.BFile, Type: BFile,
Bytes: &b64data, Bytes: &b64data,
Callback: &callback, Callback: &callback,
}) })