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
|
// 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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
23
command.go
23
command.go
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
Loading…
Reference in a new issue