Compare commits

...

8 Commits

9 changed files with 205 additions and 61 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
@ -105,6 +114,15 @@ type ClientAlbumData struct {
ReplyID *int64 `json:",omitempty"`
}
// ClientEditTextData is the required data for a CmdEditText request
type ClientEditTextData struct {
ChatID int64
MessageID int64
InlineID string
Text string
ReplyMarkup interface{} `json:",omitempty"`
}
// ClientEditCaptionData is the required data for a CmdEditCaption request
type ClientEditCaptionData struct {
ChatID int64
@ -126,6 +144,7 @@ type ClientEditMediaData struct {
// ChatAction is the action name for CmdSendChatAction requests
type ChatAction string
// Telegram chat actions
const (
ActionTyping ChatAction = "typing"
ActionUploadingPhoto ChatAction = "upload_photo"
@ -146,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/"
@ -78,7 +81,7 @@ func (t Telegram) HandleWebhook(bind string, webhook string, handler WebhookHand
}
// SendTextMessage sends an HTML-styled text message to a specified chat
func (t Telegram) SendTextMessage(data ClientTextMessageData) {
func (t Telegram) SendTextMessage(data ClientTextMessageData) (APIMessage, error) {
postdata := url.Values{
"chat_id": {strconv.FormatInt(data.ChatID, 10)},
"text": {data.Text},
@ -89,34 +92,50 @@ func (t Telegram) SendTextMessage(data ClientTextMessageData) {
}
if data.ReplyMarkup != nil {
replyjson, err := json.Marshal(data.ReplyMarkup)
if !checkerr("SendTextMessage/json.Marshal", err) {
return
if checkerr("SendTextMessage/json.Marshal", err) {
return APIMessage{}, err
}
postdata["reply_markup"] = []string{string(replyjson)}
}
_, err := http.PostForm(t.apiURL("sendMessage"), postdata)
checkerr("SendTextMessage/http.PostForm", err)
resp, err := http.PostForm(t.apiURL("sendMessage"), postdata)
if checkerr("SendTextMessage/http.PostForm", err) {
return APIMessage{}, err
}
defer resp.Body.Close()
var out apiMessageResponse
err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("SendTextMessage/json.Decode", 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) {
// 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
}
func (t Telegram) SendPhoto(data ClientPhotoData) (APIMessage, error) {
// Write file into multipart buffer
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("photo", data.Filename)
if checkerr("SendPhoto/multipart.CreateFormFile", err) {
return
return APIMessage{}, err
}
part.Write(photobytes[0:decoded])
part.Write(data.Bytes)
// Write other fields
writer.WriteField("chat_id", strconv.FormatInt(data.ChatID, 10))
@ -131,27 +150,36 @@ func (t Telegram) SendPhoto(data ClientPhotoData) {
err = writer.Close()
if checkerr("SendPhoto/writer.Close", err) {
return
return APIMessage{}, err
}
// Create HTTP client and execute request
client := &http.Client{}
req, err := http.NewRequest("POST", t.apiURL("sendPhoto"), body)
if checkerr("SendPhoto/http.NewRequest", err) {
return
return APIMessage{}, err
}
req.Header.Add("Content-Type", writer.FormDataContentType())
_, err = client.Do(req)
checkerr("SendPhoto/http.Do", err)
resp, err := client.Do(req)
if checkerr("SendPhoto/http.Do", err) {
return APIMessage{}, err
}
defer resp.Body.Close()
var out apiMessageResponse
err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("SendPhoto/json.Decode", err)
return out.Result, err
}
// SendAlbum sends an album of photos or videos
func (t Telegram) SendAlbum(data ClientAlbumData) {
func (t Telegram) SendAlbum(data ClientAlbumData) ([]APIMessage, error) {
jsonmedia, err := json.Marshal(data.Media)
if err != nil {
checkerr("SendAlbum/json.Marshal", err)
return nil, err
}
postdata := url.Values{
"chat_id": {strconv.FormatInt(data.ChatID, 10)},
@ -164,20 +192,36 @@ func (t Telegram) SendAlbum(data ClientAlbumData) {
postdata.Set("reply_to_message_id", strconv.FormatInt(*(data.ReplyID), 10))
}
_, err = http.PostForm(t.apiURL("sendMediaGroup"), postdata)
checkerr("SendAlbum/http.PostForm", err)
resp, err := http.PostForm(t.apiURL("sendMediaGroup"), postdata)
if checkerr("SendAlbum/http.PostForm", err) {
return nil, err
}
defer resp.Body.Close()
var out apiAlbumResponse
err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("SendAlbum/json.Decode", err)
return out.Result, err
}
// ForwardMessage forwards an existing message to a chat
func (t Telegram) ForwardMessage(data ClientForwardMessageData) {
func (t Telegram) ForwardMessage(data ClientForwardMessageData) (APIMessage, error) {
postdata := url.Values{
"chat_id": {strconv.FormatInt(data.ChatID, 10)},
"from_chat_id": {strconv.FormatInt(data.FromChatID, 10)},
"message_id": {strconv.FormatInt(data.MessageID, 10)},
}
_, err := http.PostForm(t.apiURL("forwardMessage"), postdata)
checkerr("ForwardMessage/http.PostForm", err)
resp, err := http.PostForm(t.apiURL("forwardMessage"), postdata)
if checkerr("ForwardMessage/http.PostForm", err) {
return APIMessage{}, err
}
defer resp.Body.Close()
var out APIMessage
err = json.NewDecoder(resp.Body).Decode(&out)
checkerr("ForwardMessage/json.Decode", err)
return out, err
}
// SendChatAction sends a 5 second long action (X is writing, sending a photo ecc.)
@ -213,6 +257,32 @@ func (t Telegram) AnswerCallback(data ClientCallbackQueryData) {
checkerr("AnswerCallback/http.PostForm", err)
}
// EditText modifies a text message
func (t Telegram) EditText(data ClientEditTextData) error {
postdata := url.Values{
"text": {data.Text},
"parse_mode": {"HTML"},
}
if data.InlineID != "" {
postdata.Set("inline_message_id", data.InlineID)
} else {
postdata.Set("chat_id", strconv.FormatInt(data.ChatID, 10))
postdata.Set("message_id", strconv.FormatInt(data.MessageID, 10))
}
if data.ReplyMarkup != nil {
replyjson, err := json.Marshal(data.ReplyMarkup)
if checkerr("EditText/json.Marshal", err) {
return ErrMalformed
}
postdata["reply_markup"] = []string{string(replyjson)}
}
_, err := http.PostForm(t.apiURL("editMessageText"), postdata)
checkerr("EditText/http.PostForm", err)
return nil
}
// EditCaption modifies the caption of a photo/document/etc message
func (t Telegram) EditCaption(data ClientEditCaptionData) error {
postdata := url.Values{
@ -315,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()
@ -341,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)
@ -369,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
}