Move broker + lib to its own repo
This commit is contained in:
commit
cc45fb5209
11 changed files with 922 additions and 0 deletions
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Bot API for Telegram bots
|
||||
|
||||
Should work if you can put the time to figure out how it works (check client_test.go or better yet, [clessy](https://github.com/hamcha/clessy))
|
154
api.go
Normal file
154
api.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
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"`
|
||||
}
|
||||
|
||||
// ChatType defines the type of chat
|
||||
type ChatType string
|
||||
|
||||
const (
|
||||
// ChatTypePrivate is a private chat (between user and bot)
|
||||
ChatTypePrivate ChatType = "private"
|
||||
|
||||
// ChatTypeGroup is a group chat (<100 members)
|
||||
ChatTypeGroup ChatType = "group"
|
||||
|
||||
// ChatTypeSupergroup is a supergroup chat (>=100 members)
|
||||
ChatTypeSupergroup ChatType = "supergroup"
|
||||
|
||||
// ChatTypeChannel is a channel (Read-only)
|
||||
ChatTypeChannel ChatType = "channel"
|
||||
)
|
||||
|
||||
// APIChat represents the "Chat" JSON structure
|
||||
type APIChat struct {
|
||||
ChatID int64 `json:"id"`
|
||||
Type ChatType `json:"type"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Username *string `json:"username,omitempty"`
|
||||
FirstName *string `json:"first_name,omitempty"`
|
||||
LastName *string `json:"last_name,omitempty"`
|
||||
}
|
||||
|
||||
// APIMessage represents the "Message" JSON structure
|
||||
type APIMessage struct {
|
||||
MessageID int64 `json:"message_id"`
|
||||
User APIUser `json:"from"`
|
||||
Time int64 `json:"date"`
|
||||
Chat *APIChat `json:"chat"`
|
||||
FwdUser *APIUpdate `json:"forward_from,omitempty"`
|
||||
FwdTime *int `json:"forward_date,omitempty"`
|
||||
ReplyTo *APIMessage `json:"reply_to_message,omitempty"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
Audio *APIAudio `json:"audio,omitempty"`
|
||||
Document *APIDocument `json:"document,omitempty"`
|
||||
Photo []APIPhotoSize `json:"photo,omitempty"`
|
||||
Sticker *APISticker `json:"sticker,omitempty"`
|
||||
Video *APIVideo `json:"video,omitempty"`
|
||||
Voice *APIVoice `json:"voice,omitempty"`
|
||||
Caption *string `json:"caption,omitempty"`
|
||||
Contact *APIContact `json:"contact,omitempty"`
|
||||
Location *APILocation `json:"location,omitempty"`
|
||||
NewUser *APIUser `json:"new_chat_partecipant,omitempty"`
|
||||
LeftUser *APIUser `json:"left_chat_partecipant,omitempty"`
|
||||
PhotoDeleted *bool `json:"delete_chat_photo,omitempty"`
|
||||
GroupCreated *bool `json:"group_chat_created,omitempty"`
|
||||
SupergroupCreated *bool `json:"supergroup_chat_created,omitempty"`
|
||||
ChannelCreated *bool `json:"channel_chat_created,omitempty"`
|
||||
GroupToSuper *int64 `json:"migrate_to_chat_id,omitempty"`
|
||||
GroupFromSuper *int64 `json:"migrate_from_chat_id,omitempty"`
|
||||
}
|
||||
|
||||
// APIPhotoSize represents the "PhotoSize" JSON structure
|
||||
type APIPhotoSize struct {
|
||||
FileID string `json:"file_id"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
FileSize *int `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// APIAudio represents the "Audio" JSON structure
|
||||
type APIAudio struct {
|
||||
FileID string `json:"file_id"`
|
||||
Duration int `json:"duration"`
|
||||
Performer *string `json:"performer,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
MimeType *string `json:"mime_type,omitempty"`
|
||||
FileSize *int `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// APIDocument represents the "Document" JSON structure
|
||||
type APIDocument struct {
|
||||
FileID string `json:"file_id"`
|
||||
Thumbnail *APIPhotoSize `json:"thumb,omitempty"`
|
||||
Filename string `json:"file_name"`
|
||||
MimeType *string `json:"mime_type,omitempty"`
|
||||
FileSize *int `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// APISticker represents the "Sticker" JSON structure
|
||||
type APISticker struct {
|
||||
FileID string `json:"file_id"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Thumbnail *APIPhotoSize `json:"thumb,omitempty"`
|
||||
FileSize *int `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// APIVideo represents the "Video" JSON structure
|
||||
type APIVideo struct {
|
||||
FileID string `json:"file_id"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Duration int `json:"duration"`
|
||||
Thumbnail *APIPhotoSize `json:"thumb,omitempty"`
|
||||
MimeType *string `json:"mime_type,omitempty"`
|
||||
FileSize *int `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// APIVoice represents the "Voice" JSON structure
|
||||
type APIVoice struct {
|
||||
FileID string `json:"file_id"`
|
||||
Duration int `json:"duration"`
|
||||
MimeType *string `json:"mime_type,omitempty"`
|
||||
FileSize *int `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// APIContact represents the "Contact" JSON structure
|
||||
type APIContact struct {
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName *string `json:"last_name,omitempty"`
|
||||
UserID *int64 `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
// APILocation represents the "Location" JSON structure
|
||||
type APILocation struct {
|
||||
Longitude float64 `json:"longitude"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
}
|
||||
|
||||
// APIUpdate represents the "Update" JSON structure
|
||||
type APIUpdate struct {
|
||||
UpdateID int64 `json:"update_id"`
|
||||
Message APIMessage `json:"message"`
|
||||
}
|
||||
|
||||
// APIFile represents the "File" JSON structure
|
||||
type APIFile struct {
|
||||
FileID string `json:"file_id"`
|
||||
Size *int `json:"file_size,omitempty"`
|
||||
Path *string `json:"file_path,omitempty"`
|
||||
}
|
||||
|
||||
// APIResponse represents a response from the Telegram API
|
||||
type APIResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
ErrCode *int `json:"error_code,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
166
broker.go
Normal file
166
broker.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Broker is a broker connection handler with callback management functions
|
||||
type Broker struct {
|
||||
Socket net.Conn
|
||||
Callbacks []BrokerCallback
|
||||
|
||||
cbFree int
|
||||
}
|
||||
|
||||
// ConnectToBroker creates a Broker connection
|
||||
func ConnectToBroker(brokerAddr string) (*Broker, error) {
|
||||
sock, err := net.Dial("tcp", brokerAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
broker := new(Broker)
|
||||
broker.Socket = sock
|
||||
broker.Callbacks = make([]BrokerCallback, 0)
|
||||
broker.cbFree = 0
|
||||
return broker, nil
|
||||
}
|
||||
|
||||
// Close closes a broker connection
|
||||
func (b *Broker) Close() {
|
||||
b.Socket.Close()
|
||||
}
|
||||
|
||||
// SendTextMessage sends a HTML-styles text message to a chat.
|
||||
// A reply_to message ID can be specified as optional parameter.
|
||||
func (b *Broker) SendTextMessage(chat *APIChat, text string, original *int64) {
|
||||
b.sendCmd(ClientCommand{
|
||||
Type: CmdSendTextMessage,
|
||||
TextMessageData: &ClientTextMessageData{
|
||||
Text: text,
|
||||
ChatID: chat.ChatID,
|
||||
ReplyID: original,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 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, original *int64) {
|
||||
b.sendCmd(ClientCommand{
|
||||
Type: CmdSendPhoto,
|
||||
PhotoData: &ClientPhotoData{
|
||||
ChatID: chat.ChatID,
|
||||
Filename: filename,
|
||||
Bytes: base64.StdEncoding.EncodeToString(data),
|
||||
Caption: caption,
|
||||
ReplyID: original,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ForwardMessage forwards a message between chats.
|
||||
func (b *Broker) ForwardMessage(chat *APIChat, message APIMessage) {
|
||||
b.sendCmd(ClientCommand{
|
||||
Type: CmdForwardMessage,
|
||||
ForwardMessageData: &ClientForwardMessageData{
|
||||
ChatID: chat.ChatID,
|
||||
FromChatID: message.Chat.ChatID,
|
||||
MessageID: message.MessageID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SendChatAction sets a chat action for 5 seconds or less (canceled at first message sent)
|
||||
func (b *Broker) SendChatAction(chat *APIChat, action ChatAction) {
|
||||
b.sendCmd(ClientCommand{
|
||||
Type: CmdSendChatAction,
|
||||
ChatActionData: &ClientChatActionData{
|
||||
ChatID: chat.ChatID,
|
||||
Action: action,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
cid := b.RegisterCallback(fn)
|
||||
b.sendCmd(ClientCommand{
|
||||
Type: CmdGetFile,
|
||||
FileRequestData: &FileRequestData{
|
||||
FileID: fileID,
|
||||
},
|
||||
Callback: &cid,
|
||||
})
|
||||
return cid
|
||||
}
|
||||
|
||||
// RegisterCallback assigns a callback ID to the given callback and puts it on the callback list.
|
||||
// This function should never be called by clients.
|
||||
func (b *Broker) RegisterCallback(fn BrokerCallback) int {
|
||||
cblen := len(b.Callbacks)
|
||||
// List is full, append to end
|
||||
if b.cbFree == cblen {
|
||||
b.Callbacks = append(b.Callbacks, fn)
|
||||
b.cbFree++
|
||||
return cblen
|
||||
}
|
||||
// List is not full, use empty slot and find next one
|
||||
id := b.cbFree
|
||||
b.Callbacks[id] = fn
|
||||
next := b.cbFree + 1
|
||||
for ; next < cblen; next++ {
|
||||
if b.Callbacks[next] == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
b.cbFree = next
|
||||
return id
|
||||
}
|
||||
|
||||
// RemoveCallback removes a callback from the callback list by ID.
|
||||
// This function should never be called by clients.
|
||||
func (b *Broker) RemoveCallback(id int) {
|
||||
b.Callbacks[id] = nil
|
||||
if id < b.cbFree {
|
||||
b.cbFree = id
|
||||
}
|
||||
b.resizeCbArray()
|
||||
}
|
||||
|
||||
// SpliceCallback retrieves a callback by ID and removes it from the list.
|
||||
// This function should never be called by clients.
|
||||
func (b *Broker) SpliceCallback(id int) BrokerCallback {
|
||||
defer b.RemoveCallback(id)
|
||||
return b.Callbacks[id]
|
||||
}
|
||||
|
||||
func (b *Broker) sendCmd(cmd ClientCommand) {
|
||||
data, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
log.Printf("[sendCmd] JSON Encode error: %s\n", err.Error())
|
||||
}
|
||||
fmt.Fprintln(b.Socket, string(data))
|
||||
}
|
||||
|
||||
func (b *Broker) resizeCbArray() {
|
||||
var i int
|
||||
cut := false
|
||||
for i = len(b.Callbacks); i > 0; i-- {
|
||||
if b.Callbacks[i-1] != nil {
|
||||
break
|
||||
}
|
||||
cut = true
|
||||
}
|
||||
if cut {
|
||||
b.Callbacks = b.Callbacks[0:i]
|
||||
if b.cbFree > i {
|
||||
b.cbFree = i
|
||||
}
|
||||
}
|
||||
}
|
64
client.go
Normal file
64
client.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package tg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
// UpdateHandler is an update handler for webhook updates
|
||||
type UpdateHandler func(broker *Broker, message APIMessage)
|
||||
|
||||
// BrokerCallback is a callback for broker responses to client requests
|
||||
type BrokerCallback func(broker *Broker, update BrokerUpdate)
|
||||
|
||||
// CreateBrokerClient creates a connection to a broker and sends all webhook updates to a given function.
|
||||
// This is the intended way to create clients, please refer to examples for how to make a simple client.
|
||||
func CreateBrokerClient(brokerAddr string, updateFn UpdateHandler) error {
|
||||
broker, err := ConnectToBroker(brokerAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer broker.Close()
|
||||
|
||||
return RunBrokerClient(broker, updateFn)
|
||||
}
|
||||
|
||||
// RunBrokerClient is a slimmer version of CreateBrokerClient for who wants to keep its own broker connection
|
||||
func RunBrokerClient(broker *Broker, updateFn UpdateHandler) error {
|
||||
in := bufio.NewReader(broker.Socket)
|
||||
var buf []byte
|
||||
for {
|
||||
bytes, isPrefix, err := in.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
buf = append(buf, bytes...)
|
||||
|
||||
if isPrefix {
|
||||
continue
|
||||
}
|
||||
|
||||
var update BrokerUpdate
|
||||
err = json.Unmarshal(buf, &update)
|
||||
if err != nil {
|
||||
log.Printf("[tg - CreateBrokerClient] ERROR reading JSON: %s\r\n", err.Error())
|
||||
log.Printf("%s\n", string(buf))
|
||||
continue
|
||||
}
|
||||
|
||||
if update.Callback == nil {
|
||||
// It's a generic message: dispatch to UpdateHandler
|
||||
go updateFn(broker, *(update.Message))
|
||||
} else {
|
||||
// It's a response to a request: retrieve callback and call it
|
||||
go broker.SpliceCallback(*(update.Callback))(broker, update)
|
||||
}
|
||||
|
||||
// Empty buffer
|
||||
buf = []byte{}
|
||||
}
|
||||
|
||||
return io.EOF
|
||||
}
|
16
client_test.go
Normal file
16
client_test.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package tg_test
|
||||
|
||||
// This example creates a basic client that connects to a broker and checks for message containing greetings.
|
||||
// If it finds a greeting message it will greet back the user (using the reply_to parameter)
|
||||
func ExampleCreateBrokerClient() {
|
||||
CreateBrokerClient("localhost:7314", func(broker *Broker, message APIMessage) {
|
||||
// Check if it's a text message
|
||||
if message.Text != nil {
|
||||
// Check that it's a greeting
|
||||
if *(message.Text) == "hello" || *(message.Text) == "hi" {
|
||||
// Reply with a greeting!
|
||||
broker.SendTextMessage(message.Chat, "Hello!", message.MessageID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
27
cmd/tg-broker/action.go
Normal file
27
cmd/tg-broker/action.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/hamcha/clessy/tg"
|
||||
)
|
||||
|
||||
func executeClientCommand(action tg.ClientCommand, client net.Conn) {
|
||||
switch action.Type {
|
||||
case tg.CmdSendTextMessage:
|
||||
data := *(action.TextMessageData)
|
||||
api.SendTextMessage(data)
|
||||
case tg.CmdGetFile:
|
||||
data := *(action.FileRequestData)
|
||||
api.GetFile(data, client, *action.Callback)
|
||||
case tg.CmdSendPhoto:
|
||||
data := *(action.PhotoData)
|
||||
api.SendPhoto(data)
|
||||
case tg.CmdForwardMessage:
|
||||
data := *(action.ForwardMessageData)
|
||||
api.ForwardMessage(data)
|
||||
case tg.CmdSendChatAction:
|
||||
data := *(action.ChatActionData)
|
||||
api.SendChatAction(data)
|
||||
}
|
||||
}
|
79
cmd/tg-broker/clients.go
Normal file
79
cmd/tg-broker/clients.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/hamcha/clessy/tg"
|
||||
)
|
||||
|
||||
var clients []net.Conn
|
||||
|
||||
func startClientsServer(bind string) {
|
||||
listener, err := net.Listen("tcp", bind)
|
||||
assert(err)
|
||||
|
||||
// Accept loop
|
||||
for {
|
||||
c, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Printf("Can't accept client: %s\n", err.Error())
|
||||
continue
|
||||
}
|
||||
clients = append(clients, c)
|
||||
go handleClient(c)
|
||||
}
|
||||
}
|
||||
|
||||
func handleClient(c net.Conn) {
|
||||
b := bufio.NewReader(c)
|
||||
defer c.Close()
|
||||
|
||||
// Start reading messages
|
||||
buf := make([]byte, 0)
|
||||
for {
|
||||
bytes, isPrefix, err := b.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
buf = append(buf, bytes...)
|
||||
if isPrefix {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get command
|
||||
var cmd tg.ClientCommand
|
||||
err = json.Unmarshal(buf, &cmd)
|
||||
if err != nil {
|
||||
log.Printf("[handleClient] Can't parse JSON: %s\r\n", err.Error())
|
||||
log.Printf("%s\n", string(buf))
|
||||
continue
|
||||
}
|
||||
|
||||
// Empty buffer
|
||||
buf = []byte{}
|
||||
|
||||
executeClientCommand(cmd, c)
|
||||
}
|
||||
removeCon(c)
|
||||
}
|
||||
|
||||
func removeCon(c net.Conn) {
|
||||
for i, con := range clients {
|
||||
if c == con {
|
||||
clients = append(clients[:i], clients[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func broadcast(message string) {
|
||||
for _, c := range clients {
|
||||
_, err := fmt.Fprintln(c, message)
|
||||
if err != nil {
|
||||
removeCon(c)
|
||||
}
|
||||
}
|
||||
}
|
57
cmd/tg-broker/main.go
Normal file
57
cmd/tg-broker/main.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// The Config data (parsed from JSON)
|
||||
type Config struct {
|
||||
BindServer string /* Address:Port to bind for Telegram */
|
||||
BindClients string /* Address:Port to bind for clients */
|
||||
Token string /* Telegram bot token */
|
||||
BaseURL string /* Base URL for webhook */
|
||||
WebhookURL string /* Webhook URL */
|
||||
}
|
||||
|
||||
func assert(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
var api *Telegram
|
||||
|
||||
func main() {
|
||||
cfgpath := flag.String("config", "config.json", "Path to configuration file")
|
||||
flag.Parse()
|
||||
|
||||
file, err := os.Open(*cfgpath)
|
||||
assert(err)
|
||||
|
||||
var config Config
|
||||
err = json.NewDecoder(file).Decode(&config)
|
||||
assert(err)
|
||||
|
||||
// Create Telegram API object
|
||||
api = mkAPI(config.Token)
|
||||
|
||||
// Setup webhook handler
|
||||
go func() {
|
||||
log.Println("Starting webserver..")
|
||||
http.HandleFunc(config.WebhookURL, webhook)
|
||||
err := http.ListenAndServe(config.BindServer, nil)
|
||||
assert(err)
|
||||
}()
|
||||
|
||||
// Register webhook @ Telegram
|
||||
log.Println("Registering webhook..")
|
||||
api.SetWebhook(config.BaseURL + config.WebhookURL)
|
||||
|
||||
// Create server for clients
|
||||
log.Println("Starting clients server..")
|
||||
startClientsServer(config.BindClients)
|
||||
}
|
220
cmd/tg-broker/telegram.go
Normal file
220
cmd/tg-broker/telegram.go
Normal file
|
@ -0,0 +1,220 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/hamcha/clessy/tg"
|
||||
)
|
||||
|
||||
// APIEndpoint is Telegram's current Bot API base url endpoint
|
||||
const APIEndpoint = "https://api.telegram.org/"
|
||||
|
||||
// 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 {
|
||||
tg := new(Telegram)
|
||||
tg.Token = token
|
||||
return tg
|
||||
}
|
||||
|
||||
// SetWebhook sets the webhook address so that Telegram knows where to send updates
|
||||
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
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
log.Println("[SetWebhook] Could not read reply: " + err.Error())
|
||||
return
|
||||
}
|
||||
if result.Ok {
|
||||
log.Println("Webhook successfully set!")
|
||||
} else {
|
||||
log.Printf("[SetWebhook] Error setting webhook (errcode %d): %s\n", *(result.ErrCode), *(result.Description))
|
||||
panic(errors.New("Cannot set webhook"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendTextMessage sends an HTML-styled text message to a specified chat
|
||||
func (t Telegram) SendTextMessage(data tg.ClientTextMessageData) {
|
||||
postdata := url.Values{
|
||||
"chat_id": {strconv.FormatInt(data.ChatID, 10)},
|
||||
"text": {data.Text},
|
||||
"parse_mode": {"HTML"},
|
||||
}
|
||||
if data.ReplyID != nil {
|
||||
postdata["reply_to_message_id"] = []string{strconv.FormatInt(*(data.ReplyID), 10)}
|
||||
}
|
||||
|
||||
_, err := http.PostForm(t.apiURL("sendMessage"), postdata)
|
||||
checkerr("SendTextMessage/http.PostForm", err)
|
||||
}
|
||||
|
||||
func (t Telegram) SendPhoto(data tg.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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
part.Write(photobytes[0:decoded])
|
||||
|
||||
// Write other fields
|
||||
writer.WriteField("chat_id", strconv.FormatInt(data.ChatID, 10))
|
||||
|
||||
if data.ReplyID != nil {
|
||||
writer.WriteField("reply_to_message_id", strconv.FormatInt(*data.ReplyID, 10))
|
||||
}
|
||||
|
||||
if data.Caption != "" {
|
||||
writer.WriteField("caption", data.Caption)
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if checkerr("SendPhoto/writer.Close", err) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
|
||||
_, err = client.Do(req)
|
||||
checkerr("SendPhoto/http.Do", err)
|
||||
}
|
||||
|
||||
func (t Telegram) ForwardMessage(data tg.ClientForwardMessageData) {
|
||||
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)
|
||||
}
|
||||
|
||||
func (t Telegram) SendChatAction(data tg.ClientChatActionData) {
|
||||
postdata := url.Values{
|
||||
"chat_id": {strconv.FormatInt(data.ChatID, 10)},
|
||||
"action": {string(data.Action)},
|
||||
}
|
||||
|
||||
_, err := http.PostForm(t.apiURL("sendChatAction"), postdata)
|
||||
checkerr("SendChatAction/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) {
|
||||
fail := func(msg string) {
|
||||
errmsg, _ := json.Marshal(tg.BrokerUpdate{
|
||||
Type: tg.BError,
|
||||
Error: &msg,
|
||||
Callback: &callback,
|
||||
})
|
||||
fmt.Fprintln(client, string(errmsg))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var filespecs = struct {
|
||||
Ok bool `json:"ok"`
|
||||
Result *tg.APIFile `json:"result,omitempty"`
|
||||
}{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&filespecs)
|
||||
if checkerr("GetFile/json.Decode", err) {
|
||||
fail("Server sent garbage (or error)")
|
||||
return
|
||||
}
|
||||
if filespecs.Result == nil {
|
||||
fail("Server didn't send a file info, does the file exist?")
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
defer fileresp.Body.Close()
|
||||
|
||||
rawdata, err := ioutil.ReadAll(fileresp.Body)
|
||||
if checkerr("GetFile/ioutil.ReadAll", err) {
|
||||
fail("Could not read file data")
|
||||
return
|
||||
}
|
||||
|
||||
rawlen := len(rawdata)
|
||||
if 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)
|
||||
|
||||
clientmsg, err := json.Marshal(tg.BrokerUpdate{
|
||||
Type: tg.BFile,
|
||||
Bytes: &b64data,
|
||||
Callback: &callback,
|
||||
})
|
||||
if checkerr("GetFile/json.Marshal", err) {
|
||||
fail("Could not serialize reply JSON")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(client, string(clientmsg))
|
||||
}
|
||||
|
||||
func (t Telegram) apiURL(method string) string {
|
||||
return APIEndpoint + "bot" + t.Token + "/" + method
|
||||
}
|
||||
|
||||
func checkerr(method string, err error) bool {
|
||||
if err != nil {
|
||||
log.Printf("[%s] Error: %s\n", method, err.Error())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
33
cmd/tg-broker/webhook.go
Normal file
33
cmd/tg-broker/webhook.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/hamcha/clessy/tg"
|
||||
)
|
||||
|
||||
func webhook(rw http.ResponseWriter, req *http.Request) {
|
||||
defer req.Body.Close()
|
||||
|
||||
// Re-encode request to ensure conformity
|
||||
var update tg.APIUpdate
|
||||
err := json.NewDecoder(req.Body).Decode(&update)
|
||||
if err != nil {
|
||||
log.Println("[webhook] Received incorrect request: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(tg.BrokerUpdate{
|
||||
Type: tg.BMessage,
|
||||
Message: &(update.Message),
|
||||
Callback: nil,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("[webhook] Cannot re-encode json (??) : " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
broadcast(string(data))
|
||||
}
|
103
command.go
Normal file
103
command.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package tg
|
||||
|
||||
// BrokerUpdateType distinguishes update types coming from the broker
|
||||
type BrokerUpdateType string
|
||||
|
||||
const (
|
||||
// BMessage is a message update (mostly webhook updates)
|
||||
BMessage BrokerUpdateType = "message"
|
||||
|
||||
// BFile is a file retrieval response update
|
||||
BFile BrokerUpdateType = "file"
|
||||
|
||||
// BError is an error the broker occurred while fulfilling a request
|
||||
BError BrokerUpdateType = "error"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// ClientCommandType distinguishes requests sent by clients to the broker
|
||||
type ClientCommandType string
|
||||
|
||||
const (
|
||||
// CmdSendTextMessage requests the broker to send a text message to a chat
|
||||
CmdSendTextMessage ClientCommandType = "sendText"
|
||||
|
||||
// CmdSendPhoto requests the broker to send a photo to a chat
|
||||
CmdSendPhoto ClientCommandType = "sendPhoto"
|
||||
|
||||
// CmdForwardMessage requests the broker to forward a message between chats
|
||||
CmdForwardMessage ClientCommandType = "forwardMessage"
|
||||
|
||||
// CmdGetFile requests the broker to get a file from Telegram
|
||||
CmdGetFile ClientCommandType = "getFile"
|
||||
|
||||
// CmdSendChatAction requests the broker to set a chat action (typing, etc.)
|
||||
CmdSendChatAction ClientCommandType = "sendChatAction"
|
||||
)
|
||||
|
||||
// ClientTextMessageData is the required data for a CmdSendTextMessage request
|
||||
type ClientTextMessageData struct {
|
||||
ChatID int64
|
||||
Text string
|
||||
ReplyID *int64 `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ClientPhotoData is the required data for a CmdSendPhoto request
|
||||
type ClientPhotoData struct {
|
||||
ChatID int64
|
||||
Bytes string
|
||||
Filename string
|
||||
Caption string `json:",omitempty"`
|
||||
ReplyID *int64 `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ClientForwardMessageData is the required data for a CmdForwardMessage request
|
||||
type ClientForwardMessageData struct {
|
||||
ChatID int64
|
||||
FromChatID int64
|
||||
MessageID int64
|
||||
}
|
||||
|
||||
// ClientChatActionData is the required data for a CmdSendChatAction request
|
||||
type ClientChatActionData struct {
|
||||
ChatID int64
|
||||
Action ChatAction
|
||||
}
|
||||
|
||||
// ChatAction is the action name for CmdSendChatAction requests
|
||||
type ChatAction string
|
||||
|
||||
const (
|
||||
ActionTyping ChatAction = "typing"
|
||||
ActionUploadingPhoto ChatAction = "upload_photo"
|
||||
ActionRecordingVideo ChatAction = "record_video"
|
||||
ActionUploadingVideo ChatAction = "upload_video"
|
||||
ActionRecordingAudio ChatAction = "record_audio"
|
||||
ActionUploadingAudio ChatAction = "upload_audio"
|
||||
ActionUploadingDocument ChatAction = "upload_document"
|
||||
ActionFindingLocation ChatAction = "find_location"
|
||||
)
|
||||
|
||||
// FileRequestData is the required data for a CmdGetFile request
|
||||
type FileRequestData struct {
|
||||
FileID string
|
||||
}
|
||||
|
||||
// ClientCommand is a request sent by clients to the broker
|
||||
type ClientCommand struct {
|
||||
Type ClientCommandType
|
||||
TextMessageData *ClientTextMessageData `json:",omitempty"`
|
||||
PhotoData *ClientPhotoData `json:",omitempty"`
|
||||
ForwardMessageData *ClientForwardMessageData `json:",omitempty"`
|
||||
ChatActionData *ClientChatActionData `json:",omitempty"`
|
||||
FileRequestData *FileRequestData `json:",omitempty"`
|
||||
Callback *int `json:",omitempty"`
|
||||
}
|
Loading…
Reference in a new issue