Compare commits

...

6 Commits

Author SHA1 Message Date
Hamcha 400e756356
Better SendPhoto for non-network 2022-03-27 22:22:20 +02:00
Hamcha b27494aa52
Sane GetFile API 2022-03-27 21:58:52 +02:00
Hamcha 9abe50bebd
Add GetMe, use jsoniter 2022-03-24 12:31:55 +01:00
Hamcha ab9dc80b06
Fix fwduser signature 2021-03-18 13:06:50 +01:00
Hamcha bcfc5c2d64
Fix typo on callback query structure 2020-06-09 16:12:08 +02:00
Hamcha 27464fcee7
Fix message parsing from requests 2019-08-26 10:40:36 +02:00
9 changed files with 125 additions and 50 deletions

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
ISC License
Copyright 2022 Hamcha
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

32
api.go
View File

@ -2,10 +2,15 @@ package tg
// APIUser represents the "User" JSON structure // APIUser represents the "User" JSON structure
type APIUser struct { type APIUser struct {
UserID int64 `json:"id"` UserID int64 `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name,omitempty"` LastName string `json:"last_name,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
IsBot bool `json:"is_bot"`
LanguageCode string `json:"language_code,omitempty"`
CanJoinGroups bool `json:"can_join_groups,omitempty"`
CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"`
SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"`
} }
// ChatType defines the type of chat // ChatType defines the type of chat
@ -41,7 +46,7 @@ type APIMessage struct {
User APIUser `json:"from"` User APIUser `json:"from"`
Time int64 `json:"date"` Time int64 `json:"date"`
Chat *APIChat `json:"chat"` Chat *APIChat `json:"chat"`
FwdUser *APIUpdate `json:"forward_from,omitempty"` FwdUser *APIUser `json:"forward_from,omitempty"`
FwdTime *int `json:"forward_date,omitempty"` FwdTime *int `json:"forward_date,omitempty"`
ReplyTo *APIMessage `json:"reply_to_message,omitempty"` ReplyTo *APIMessage `json:"reply_to_message,omitempty"`
Text *string `json:"text,omitempty"` Text *string `json:"text,omitempty"`
@ -227,9 +232,24 @@ type APIInputMediaPhoto struct {
// APICallbackQuery is a callback query triggered by an inline button // APICallbackQuery is a callback query triggered by an inline button
type APICallbackQuery struct { type APICallbackQuery struct {
ID string `json:"id"` ID string `json:"id"`
User APIAudio `json:"from"` User APIUser `json:"from"`
Message *APIMessage `json:"message,omitempty"` Message *APIMessage `json:"message,omitempty"`
InlineID *string `json:"inline_message_id,omitempty"` InlineID *string `json:"inline_message_id,omitempty"`
ChatID string `json:"chat_instance"` ChatID string `json:"chat_instance"`
Data *string `json:"data,omitempty"` Data *string `json:"data,omitempty"`
} }
type apiMessageResponse struct {
Ok bool `json:"ok"`
Result APIMessage `json:"result"`
}
type apiAlbumResponse struct {
Ok bool `json:"ok"`
Result []APIMessage `json:"result"`
}
type apiUserResponse struct {
Ok bool `json:"ok"`
Result APIUser `json:"result"`
}

View File

@ -2,7 +2,6 @@ package tg
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"log" "log"
"net" "net"
@ -62,7 +61,7 @@ func (b *Broker) SendTextMessage(chat *APIChat, text string, extra *MessageOptio
// SendPhoto sends a photo with an optional caption to a chat. // SendPhoto sends a photo with an optional caption to a chat.
// A reply_to message ID can be specified as optional parameter. // A reply_to message ID can be specified as optional parameter.
func (b *Broker) SendPhoto(chat *APIChat, data []byte, filename string, caption string, extra *MessageOptions) { func (b *Broker) SendPhoto(chat *APIChat, data []byte, filename string, caption string, extra *MessageOptions) {
photodata := &ClientPhotoData{ photodata := &ClientPhotoDataNet{
ChatID: chat.ChatID, ChatID: chat.ChatID,
Filename: filename, Filename: filename,
Bytes: base64.StdEncoding.EncodeToString(data), Bytes: base64.StdEncoding.EncodeToString(data),

View File

@ -2,7 +2,6 @@ package tg
import ( import (
"bufio" "bufio"
"encoding/json"
"io" "io"
"log" "log"
) )

View File

@ -13,10 +13,10 @@ func executeClientCommand(action tg.ClientCommand, client net.Conn) {
api.SendTextMessage(data) api.SendTextMessage(data)
case tg.CmdGetFile: case tg.CmdGetFile:
data := *(action.FileRequestData) data := *(action.FileRequestData)
api.GetFile(data, client, *action.Callback) api.GetFileNet(data, client, *action.Callback)
case tg.CmdSendPhoto: case tg.CmdSendPhoto:
data := *(action.PhotoData) data := *(action.PhotoData)
api.SendPhoto(data) api.SendPhotoNet(data)
case tg.CmdForwardMessage: case tg.CmdForwardMessage:
data := *(action.ForwardMessageData) data := *(action.ForwardMessageData)
api.ForwardMessage(data) api.ForwardMessage(data)

View File

@ -68,6 +68,15 @@ type ClientTextMessageData struct {
// ClientPhotoData is the required data for a CmdSendPhoto request // ClientPhotoData is the required data for a CmdSendPhoto request
type ClientPhotoData struct { type ClientPhotoData struct {
ChatID int64
Bytes []byte
Filename string
Caption string `json:",omitempty"`
ReplyID *int64 `json:",omitempty"`
}
// ClientPhotoDataNet is the required data for a CmdSendPhoto request over the wire
type ClientPhotoDataNet struct {
ChatID int64 ChatID int64
Bytes string Bytes string
Filename string Filename string
@ -156,7 +165,7 @@ type FileRequestData struct {
type ClientCommand struct { type ClientCommand struct {
Type ClientCommandType Type ClientCommandType
TextMessageData *ClientTextMessageData `json:",omitempty"` TextMessageData *ClientTextMessageData `json:",omitempty"`
PhotoData *ClientPhotoData `json:",omitempty"` PhotoData *ClientPhotoDataNet `json:",omitempty"`
ForwardMessageData *ClientForwardMessageData `json:",omitempty"` ForwardMessageData *ClientForwardMessageData `json:",omitempty"`
ChatActionData *ClientChatActionData `json:",omitempty"` ChatActionData *ClientChatActionData `json:",omitempty"`
InlineQueryResults *InlineQueryResponse `json:",omitempty"` InlineQueryResults *InlineQueryResponse `json:",omitempty"`

2
go.mod
View File

@ -1,3 +1,5 @@
module git.fromouter.space/hamcha/tg module git.fromouter.space/hamcha/tg
go 1.12 go 1.12
require github.com/json-iterator/go v1.1.12 // indirect

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

View File

@ -3,7 +3,6 @@ package tg
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -13,8 +12,12 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
jsoniter "github.com/json-iterator/go"
) )
var json = jsoniter.ConfigFastest
// 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/"
@ -101,22 +104,30 @@ func (t Telegram) SendTextMessage(data ClientTextMessageData) (APIMessage, error
} }
defer resp.Body.Close() defer resp.Body.Close()
var out APIMessage var out apiMessageResponse
err = json.NewDecoder(resp.Body).Decode(&out) err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("SendTextMessage/json.Decode", err) checkerr("SendTextMessage/json.Decode", err)
return out, err return out.Result, err
}
// SendPhotoNet decodes a network SendPhoto request and executes it
func (t Telegram) SendPhotoNet(data ClientPhotoDataNet) (APIMessage, error) {
byt, err := base64.StdEncoding.DecodeString(data.Bytes)
if checkerr("SendPhotoNet/base64.Decode", err) {
return APIMessage{}, err
}
return t.SendPhoto(ClientPhotoData{
ChatID: data.ChatID,
Bytes: byt,
Filename: data.Filename,
Caption: data.Caption,
ReplyID: data.ReplyID,
})
} }
// SendPhoto sends a picture to a chat as a photo // SendPhoto sends a picture to a chat as a photo
func (t Telegram) SendPhoto(data ClientPhotoData) (APIMessage, error) { func (t Telegram) SendPhoto(data ClientPhotoData) (APIMessage, error) {
// Decode photo from b64
photolen := base64.StdEncoding.DecodedLen(len(data.Bytes))
photobytes := make([]byte, photolen)
decoded, err := base64.StdEncoding.Decode(photobytes, []byte(data.Bytes))
if checkerr("SendPhoto/base64.Decode", err) {
return APIMessage{}, err
}
// Write file into multipart buffer // Write file into multipart buffer
body := new(bytes.Buffer) body := new(bytes.Buffer)
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
@ -124,7 +135,7 @@ func (t Telegram) SendPhoto(data ClientPhotoData) (APIMessage, error) {
if checkerr("SendPhoto/multipart.CreateFormFile", err) { if checkerr("SendPhoto/multipart.CreateFormFile", err) {
return APIMessage{}, err return APIMessage{}, err
} }
part.Write(photobytes[0:decoded]) part.Write(data.Bytes)
// Write other fields // Write other fields
writer.WriteField("chat_id", strconv.FormatInt(data.ChatID, 10)) writer.WriteField("chat_id", strconv.FormatInt(data.ChatID, 10))
@ -157,10 +168,10 @@ func (t Telegram) SendPhoto(data ClientPhotoData) (APIMessage, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
var out APIMessage var out apiMessageResponse
err = json.NewDecoder(resp.Body).Decode(&out) err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("SendPhoto/json.Decode", err) checkerr("SendPhoto/json.Decode", err)
return out, err return out.Result, err
} }
// SendAlbum sends an album of photos or videos // SendAlbum sends an album of photos or videos
@ -187,10 +198,10 @@ func (t Telegram) SendAlbum(data ClientAlbumData) ([]APIMessage, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
var out []APIMessage var out apiAlbumResponse
err = json.NewDecoder(resp.Body).Decode(&out) err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("SendAlbum/json.Decode", err) checkerr("SendAlbum/json.Decode", err)
return out, err return out.Result, err
} }
// ForwardMessage forwards an existing message to a chat // ForwardMessage forwards an existing message to a chat
@ -374,23 +385,13 @@ func (t Telegram) AnswerInlineQuery(data InlineQueryResponse) error {
// 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 FileRequestData, client net.Conn, callback int) { func (t Telegram) GetFile(data FileRequestData) ([]byte, error) {
fail := func(msg string) {
errmsg, _ := json.Marshal(BrokerUpdate{
Type: BError,
Error: &msg,
Callback: &callback,
})
fmt.Fprintln(client, string(errmsg))
}
postdata := url.Values{ postdata := url.Values{
"file_id": {data.FileID}, "file_id": {data.FileID},
} }
resp, err := http.PostForm(t.apiURL("getFile"), postdata) resp, err := http.PostForm(t.apiURL("getFile"), postdata)
if checkerr("GetFile/post", err) { if checkerr("GetFile/post", err) {
fail("Server didn't like my request") return nil, fmt.Errorf("Server didn't like my request: %w", err)
return
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -400,27 +401,23 @@ func (t Telegram) GetFile(data FileRequestData, client net.Conn, callback int) {
}{} }{}
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) {
fail("Server sent garbage (or error)") return nil, fmt.Errorf("Server sent garbage (or error): %w", err)
return
} }
if filespecs.Result == nil { if filespecs.Result == nil {
fail("Server didn't send a file info, does the file exist?") return nil, fmt.Errorf("Server didn't send a file info, does the file exist?")
return
} }
result := *filespecs.Result result := *filespecs.Result
path := APIEndpoint + "file/bot" + t.Token + "/" + *result.Path path := APIEndpoint + "file/bot" + t.Token + "/" + *result.Path
fileresp, err := http.Get(path) fileresp, err := http.Get(path)
if checkerr("GetFile/get", err) { if checkerr("GetFile/get", err) {
fail("Could not retrieve file from Telegram's servers") return nil, fmt.Errorf("Could not retrieve file from Telegram's servers: %w", err)
return
} }
defer fileresp.Body.Close() defer fileresp.Body.Close()
rawdata, err := ioutil.ReadAll(fileresp.Body) rawdata, err := ioutil.ReadAll(fileresp.Body)
if checkerr("GetFile/ioutil.ReadAll", err) { if checkerr("GetFile/ioutil.ReadAll", err) {
fail("Could not read file data") return nil, fmt.Errorf("Could not read file data: %w", err)
return
} }
rawlen := len(rawdata) rawlen := len(rawdata)
@ -428,21 +425,51 @@ func (t Telegram) GetFile(data FileRequestData, client net.Conn, callback int) {
// ??? // ???
log.Printf("[GetFile] WARN ?? Downloaded file does not match provided filesize: %d != %d\n", rawlen, *result.Size) log.Printf("[GetFile] WARN ?? Downloaded file does not match provided filesize: %d != %d\n", rawlen, *result.Size)
} }
b64data := base64.StdEncoding.EncodeToString(rawdata) return rawdata, nil
}
func (t Telegram) GetFileNet(data FileRequestData, client net.Conn, callback int) {
byt, err := t.GetFile(data)
if err != nil {
errstr := err.Error()
errmsg, _ := json.Marshal(BrokerUpdate{
Type: BError,
Error: &errstr,
Callback: &callback,
})
fmt.Fprintln(client, string(errmsg))
}
b64data := base64.StdEncoding.EncodeToString(byt)
clientmsg, err := json.Marshal(BrokerUpdate{ clientmsg, err := json.Marshal(BrokerUpdate{
Type: BFile, Type: BFile,
Bytes: &b64data, Bytes: &b64data,
Callback: &callback, Callback: &callback,
}) })
if checkerr("GetFile/json.Marshal", err) { if checkerr("GetFile/json.Marshal", err) {
fail("Could not serialize reply JSON") errstr := "Could not serialize reply JSON"
errmsg, _ := json.Marshal(BrokerUpdate{
Type: BError,
Error: &errstr,
Callback: &callback,
})
fmt.Fprintln(client, string(errmsg))
return return
} }
fmt.Fprintln(client, string(clientmsg)) fmt.Fprintln(client, string(clientmsg))
} }
func (t Telegram) GetMe() (APIUser, error) {
resp, err := http.Get(t.apiURL("getMe"))
if checkerr("GetMe/get", err) {
return APIUser{}, err
}
var result apiUserResponse
err = json.NewDecoder(resp.Body).Decode(&result)
return result.Result, err
}
func (t Telegram) apiURL(method string) string { func (t Telegram) apiURL(method string) string {
return APIEndpoint + "bot" + t.Token + "/" + method return APIEndpoint + "bot" + t.Token + "/" + method
} }