Add inline query and standalone client support
This commit is contained in:
parent
9804f85677
commit
923d9e245f
8 changed files with 134 additions and 29 deletions
29
api.go
29
api.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
23
command.go
23
command.go
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
Loading…
Reference in a new issue