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
type APIUser struct {
UserID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name,omitempty"`
Username string `json:"username,omitempty"`
UserID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name,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
@ -41,7 +46,7 @@ type APIMessage struct {
User APIUser `json:"from"`
Time int64 `json:"date"`
Chat *APIChat `json:"chat"`
FwdUser *APIUpdate `json:"forward_from,omitempty"`
FwdUser *APIUser `json:"forward_from,omitempty"`
FwdTime *int `json:"forward_date,omitempty"`
ReplyTo *APIMessage `json:"reply_to_message,omitempty"`
Text *string `json:"text,omitempty"`
@ -227,9 +232,24 @@ type APIInputMediaPhoto struct {
// APICallbackQuery is a callback query triggered by an inline button
type APICallbackQuery struct {
ID string `json:"id"`
User APIAudio `json:"from"`
User APIUser `json:"from"`
Message *APIMessage `json:"message,omitempty"`
InlineID *string `json:"inline_message_id,omitempty"`
ChatID string `json:"chat_instance"`
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 (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"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.
// 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) {
photodata := &ClientPhotoData{
photodata := &ClientPhotoDataNet{
ChatID: chat.ChatID,
Filename: filename,
Bytes: base64.StdEncoding.EncodeToString(data),

View File

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

View File

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

View File

@ -68,6 +68,15 @@ type ClientTextMessageData struct {
// ClientPhotoData is the required data for a CmdSendPhoto request
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
Bytes string
Filename string
@ -156,7 +165,7 @@ type FileRequestData struct {
type ClientCommand struct {
Type ClientCommandType
TextMessageData *ClientTextMessageData `json:",omitempty"`
PhotoData *ClientPhotoData `json:",omitempty"`
PhotoData *ClientPhotoDataNet `json:",omitempty"`
ForwardMessageData *ClientForwardMessageData `json:",omitempty"`
ChatActionData *ClientChatActionData `json:",omitempty"`
InlineQueryResults *InlineQueryResponse `json:",omitempty"`

2
go.mod
View File

@ -1,3 +1,5 @@
module git.fromouter.space/hamcha/tg
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 (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
@ -13,8 +12,12 @@ import (
"net/http"
"net/url"
"strconv"
jsoniter "github.com/json-iterator/go"
)
var json = jsoniter.ConfigFastest
// APIEndpoint is Telegram's current Bot API base url endpoint
const APIEndpoint = "https://api.telegram.org/"
@ -101,22 +104,30 @@ func (t Telegram) SendTextMessage(data ClientTextMessageData) (APIMessage, error
}
defer resp.Body.Close()
var out APIMessage
var out apiMessageResponse
err = json.NewDecoder(resp.Body).Decode(&out)
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
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
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
@ -124,7 +135,7 @@ func (t Telegram) SendPhoto(data ClientPhotoData) (APIMessage, error) {
if checkerr("SendPhoto/multipart.CreateFormFile", err) {
return APIMessage{}, err
}
part.Write(photobytes[0:decoded])
part.Write(data.Bytes)
// Write other fields
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()
var out APIMessage
var out apiMessageResponse
err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("SendPhoto/json.Decode", err)
return out, err
return out.Result, err
}
// SendAlbum sends an album of photos or videos
@ -187,10 +198,10 @@ func (t Telegram) SendAlbum(data ClientAlbumData) ([]APIMessage, error) {
}
defer resp.Body.Close()
var out []APIMessage
var out apiAlbumResponse
err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("SendAlbum/json.Decode", err)
return out, err
return out.Result, err
}
// 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
// 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 FileRequestData, client net.Conn, callback int) {
fail := func(msg string) {
errmsg, _ := json.Marshal(BrokerUpdate{
Type: BError,
Error: &msg,
Callback: &callback,
})
fmt.Fprintln(client, string(errmsg))
}
func (t Telegram) GetFile(data FileRequestData) ([]byte, error) {
postdata := url.Values{
"file_id": {data.FileID},
}
resp, err := http.PostForm(t.apiURL("getFile"), postdata)
if checkerr("GetFile/post", err) {
fail("Server didn't like my request")
return
return nil, fmt.Errorf("Server didn't like my request: %w", err)
}
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)
if checkerr("GetFile/json.Decode", err) {
fail("Server sent garbage (or error)")
return
return nil, fmt.Errorf("Server sent garbage (or error): %w", err)
}
if filespecs.Result == nil {
fail("Server didn't send a file info, does the file exist?")
return
return nil, fmt.Errorf("Server didn't send a file info, does the file exist?")
}
result := *filespecs.Result
path := APIEndpoint + "file/bot" + t.Token + "/" + *result.Path
fileresp, err := http.Get(path)
if checkerr("GetFile/get", err) {
fail("Could not retrieve file from Telegram's servers")
return
return nil, fmt.Errorf("Could not retrieve file from Telegram's servers: %w", err)
}
defer fileresp.Body.Close()
rawdata, err := ioutil.ReadAll(fileresp.Body)
if checkerr("GetFile/ioutil.ReadAll", err) {
fail("Could not read file data")
return
return nil, fmt.Errorf("Could not read file data: %w", err)
}
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)
}
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{
Type: BFile,
Bytes: &b64data,
Callback: &callback,
})
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
}
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 {
return APIEndpoint + "bot" + t.Token + "/" + method
}