diff --git a/api.go b/api.go index 274d291..c772d4a 100644 --- a/api.go +++ b/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 +} diff --git a/broker.go b/broker.go index 8ed3e3e..0884cbc 100644 --- a/broker.go +++ b/broker.go @@ -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 { diff --git a/client.go b/client.go index af3002b..60b6d0b 100644 --- a/client.go +++ b/client.go @@ -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) diff --git a/cmd/tg-broker/action.go b/cmd/tg-broker/action.go index 1c45e33..4d51988 100644 --- a/cmd/tg-broker/action.go +++ b/cmd/tg-broker/action.go @@ -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) } } diff --git a/cmd/tg-broker/main.go b/cmd/tg-broker/main.go index df2469e..c6deb4f 100644 --- a/cmd/tg-broker/main.go +++ b/cmd/tg-broker/main.go @@ -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() { diff --git a/cmd/tg-broker/webhook.go b/cmd/tg-broker/webhook.go index 5423d65..f63e157 100644 --- a/cmd/tg-broker/webhook.go +++ b/cmd/tg-broker/webhook.go @@ -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 { diff --git a/command.go b/command.go index 021108c..05250cb 100644 --- a/command.go +++ b/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"` +} diff --git a/cmd/tg-broker/telegram.go b/telegram.go similarity index 68% rename from cmd/tg-broker/telegram.go rename to telegram.go index f6b3d98..ceaef35 100644 --- a/cmd/tg-broker/telegram.go +++ b/telegram.go @@ -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, })