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
type APIUpdate struct {
UpdateID int64 `json:"update_id"`
Message APIMessage `json:"message"`
UpdateID int64 `json:"update_id"`
Message *APIMessage `json:"message"`
Inline *APIInlineQuery `json:"inline_query,omitempty"`
}
// APIFile represents the "File" JSON structure
@ -152,3 +153,27 @@ type APIResponse struct {
ErrCode *int `json:"error_code,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.
// This function is asynchronous as data will be delivered to the given callback.
func (b *Broker) GetFile(fileID string, fn BrokerCallback) int {

View File

@ -8,7 +8,7 @@ import (
)
// 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
type BrokerCallback func(broker *Broker, update BrokerUpdate)
@ -50,7 +50,7 @@ func RunBrokerClient(broker *Broker, updateFn UpdateHandler) error {
if update.Callback == nil {
// It's a generic message: dispatch to UpdateHandler
go updateFn(broker, *(update.Message))
go updateFn(broker, *(update.Data))
} else {
// It's a response to a request: retrieve callback and call it
go broker.SpliceCallback(*(update.Callback))(broker, update)

View File

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

View File

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

View File

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

View File

@ -17,10 +17,10 @@ const (
// BrokerUpdate is what is sent by the broker as update
type BrokerUpdate struct {
Type BrokerUpdateType
Callback *int `json:",omitempty"`
Error *string `json:",omitempty"`
Message *APIMessage `json:",omitempty"`
Bytes *string `json:",omitempty"`
Callback *int `json:",omitempty"`
Error *string `json:",omitempty"`
Data *APIUpdate `json:",omitempty"`
Bytes *string `json:",omitempty"`
}
// 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 ClientCommandType = "sendChatAction"
// CmdAnswerInlineQuery requests the broker sends results of an inline query
CmdAnswerInlineQuery ClientCommandType = "answerInlineQuery"
)
// ClientTextMessageData is the required data for a CmdSendTextMessage request
@ -98,6 +101,18 @@ type ClientCommand struct {
PhotoData *ClientPhotoData `json:",omitempty"`
ForwardMessageData *ClientForwardMessageData `json:",omitempty"`
ChatActionData *ClientChatActionData `json:",omitempty"`
InlineQueryResults *InlineQueryResponse `json:",omitempty"`
FileRequestData *FileRequestData `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 (
"bytes"
@ -13,20 +13,21 @@ import (
"net/http"
"net/url"
"strconv"
"github.com/hamcha/tg"
)
// APIEndpoint is Telegram's current Bot API base url endpoint
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
type Telegram struct {
Token string
}
// mkAPI creates a Telegram instance from a Bot API token
func mkAPI(token string) *Telegram {
// MakeAPIClient creates a Telegram instance from a Bot API token
func MakeAPIClient(token string) *Telegram {
tg := new(Telegram)
tg.Token = token
return tg
@ -37,7 +38,7 @@ func (t Telegram) SetWebhook(webhook string) {
resp, err := http.PostForm(t.apiURL("setWebhook"), url.Values{"url": {webhook}})
if !checkerr("SetWebhook/http.PostForm", err) {
defer resp.Body.Close()
var result tg.APIResponse
var result APIResponse
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
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
func (t Telegram) SendTextMessage(data tg.ClientTextMessageData) {
func (t Telegram) SendTextMessage(data ClientTextMessageData) {
postdata := url.Values{
"chat_id": {strconv.FormatInt(data.ChatID, 10)},
"text": {data.Text},
@ -67,7 +87,8 @@ func (t Telegram) SendTextMessage(data tg.ClientTextMessageData) {
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
photolen := base64.StdEncoding.DecodedLen(len(data.Bytes))
photobytes := make([]byte, photolen)
@ -114,7 +135,8 @@ func (t Telegram) SendPhoto(data tg.ClientPhotoData) {
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{
"chat_id": {strconv.FormatInt(data.ChatID, 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)
}
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{
"chat_id": {strconv.FormatInt(data.ChatID, 10)},
"action": {string(data.Action)},
@ -135,13 +158,43 @@ func (t Telegram) SendChatAction(data tg.ClientChatActionData) {
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
// specified afterward. The file will be then send back to the client that requested it
// 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) {
errmsg, _ := json.Marshal(tg.BrokerUpdate{
Type: tg.BError,
errmsg, _ := json.Marshal(BrokerUpdate{
Type: BError,
Error: &msg,
Callback: &callback,
})
@ -159,8 +212,8 @@ func (t Telegram) GetFile(data tg.FileRequestData, client net.Conn, callback int
defer resp.Body.Close()
var filespecs = struct {
Ok bool `json:"ok"`
Result *tg.APIFile `json:"result,omitempty"`
Ok bool `json:"ok"`
Result *APIFile `json:"result,omitempty"`
}{}
err = json.NewDecoder(resp.Body).Decode(&filespecs)
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)
clientmsg, err := json.Marshal(tg.BrokerUpdate{
Type: tg.BFile,
clientmsg, err := json.Marshal(BrokerUpdate{
Type: BFile,
Bytes: &b64data,
Callback: &callback,
})