MOVE STUFF AROUND OH YEAH LIKE THAT

This commit is contained in:
Hamcha 2018-04-03 12:07:06 +02:00
parent b29294fcbf
commit 8551fbbe2c
Signed by: hamcha
GPG key ID: A40413D21021EAEE
31 changed files with 51 additions and 955 deletions

View file

@ -1,4 +1,4 @@
all: clessy-broker clessy-mods clessy-stats all: tg-broker clessy-mods clessy-stats
deps: deps:
go get github.com/boltdb/bolt/... go get github.com/boltdb/bolt/...
@ -8,10 +8,10 @@ deps:
go get github.com/disintegration/imaging go get github.com/disintegration/imaging
install-tg: install-tg:
go install github.com/hamcha/clessy/tg go get -u github.com/hamcha/tg
clessy-broker: install-tg tg-broker: install-tg
go build -o clessy-broker github.com/hamcha/clessy/broker go get -u github.com/hamcha/tg/cmd/tg-broker
clessy-mods: install-tg clessy-mods: install-tg
go build -o clessy-mods github.com/hamcha/clessy/mods go build -o clessy-mods github.com/hamcha/clessy/mods
@ -23,4 +23,4 @@ clessy-stats-import: install-tg
go build -o clessy-stats-import github.com/hamcha/clessy/stats-import go build -o clessy-stats-import github.com/hamcha/clessy/stats-import
clean: clean:
rm -f clessy-broker clessy-mods clessy-stats rm -f clessy-mods clessy-stats

View file

@ -1,27 +0,0 @@
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)
}
}

View file

@ -1,79 +0,0 @@
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)
}
}
}

View file

@ -1,57 +0,0 @@
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)
}

View file

@ -1,220 +0,0 @@
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
}

View file

@ -1,33 +0,0 @@
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))
}

View file

@ -1,7 +0,0 @@
{
"BindServer" : ":7313",
"BindClients": "127.0.0.1:7314",
"Token" : "Bot token here",
"BaseURL" : "https://my.bot.host",
"WebhookURL" : "/secret_url_here"
}

View file

@ -10,7 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
const KYMURL = "http://knowyourmeme.com/memes/it-s-finally-a-friday/photos/page/2" const KYMURL = "http://knowyourmeme.com/memes/it-s-finally-a-friday/photos/page/2"

View file

@ -4,7 +4,7 @@ import (
"math/rand" "math/rand"
"strings" "strings"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
var dantes_frasiacaso = []string{ var dantes_frasiacaso = []string{

21
mods-toupdate/roll.go Normal file
View file

@ -0,0 +1,21 @@
package main
import (
"strings"
"github.com/hamcha/tg"
)
func roll(broker *tg.Broker, update tg.APIMessage) {
if isCommand(update, "roll") {
parts := strings.Split(*(update.Text), " ")
if len(parts) < 2 {
broker.SendTextMessage(update.Chat, "<b>Sintassi</b>\n/roll <i>cosa1</i> <i>[cosa2]</i> ..\n\n<b>Cose supportate</b>\nNumero intero casuale (es. 300, 10-20, 34-100)\nLancio di dadi (es. d6, 2d20, 100d12+10)\nUtente casuale (\"user\")", &update.MessageID)
return
}
}
}
func rolltoken(token string) {
}

View file

@ -11,7 +11,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
type QRequest struct { type QRequest struct {

View file

@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
type Macro struct { type Macro struct {

View file

@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
type Mod struct { type Mod struct {
@ -47,10 +47,12 @@ var mods = map[string]Mod{
OnInit: proverbio_init, OnInit: proverbio_init,
OnMessage: proverbio_message, OnMessage: proverbio_message,
}, },
"talk": { /*
OnInit: talk_init, "talk": {
OnMessage: talk_message, OnInit: inittalk,
}, OnMessage: talk,
},
*/
"stt": { "stt": {
OnInit: stt_init, OnInit: stt_init,
OnMessage: stt_message, OnMessage: stt_message,

View file

@ -13,7 +13,7 @@ import (
"strings" "strings"
"github.com/golang/freetype" "github.com/golang/freetype"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
"github.com/llgcode/draw2d" "github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg" "github.com/llgcode/draw2d/draw2dimg"
) )

View file

@ -3,7 +3,7 @@ package main
import ( import (
"math/rand" "math/rand"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
var metaactions = []string{ var metaactions = []string{

View file

@ -8,7 +8,7 @@ import (
"log" "log"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
type ProverbioData struct { type ProverbioData struct {

View file

@ -10,7 +10,7 @@ import (
"time" "time"
"unicode" "unicode"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
var remindpath *string var remindpath *string

View file

@ -9,7 +9,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
type SearchResult struct { type SearchResult struct {

View file

@ -18,7 +18,7 @@ import (
"time" "time"
"github.com/golang/freetype" "github.com/golang/freetype"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
"github.com/llgcode/draw2d" "github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg" "github.com/llgcode/draw2d/draw2dimg"
) )

View file

@ -8,7 +8,7 @@ import (
"encoding/base64" "encoding/base64"
speech "cloud.google.com/go/speech/apiv1" speech "cloud.google.com/go/speech/apiv1"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
"google.golang.org/api/option" "google.golang.org/api/option"
speechpb "google.golang.org/genproto/googleapis/cloud/speech/v1" speechpb "google.golang.org/genproto/googleapis/cloud/speech/v1"
) )

View file

@ -13,7 +13,7 @@ import (
"strings" "strings"
"github.com/golang/freetype" "github.com/golang/freetype"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
"github.com/llgcode/draw2d" "github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg" "github.com/llgcode/draw2d/draw2dimg"

View file

@ -10,7 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
const viaggiurl = "http://free.rome2rio.com/api/1.2/json/Search?key=X5JMLHNc&languageCode=IT&currencyCode=EUR" const viaggiurl = "http://free.rome2rio.com/api/1.2/json/Search?key=X5JMLHNc&languageCode=IT&currencyCode=EUR"

View file

@ -9,7 +9,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
func assert(err error) { func assert(err error) {

View file

@ -4,7 +4,7 @@ import (
"flag" "flag"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
func assert(err error) { func assert(err error) {
@ -42,6 +42,5 @@ func main() {
go startWebServer(*webBind) go startWebServer(*webBind)
_, err = tg.CreateBrokerClient(*brokerAddr, process) assert(tg.CreateBrokerClient(*brokerAddr, process))
assert(err)
} }

View file

@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
const ( const (

View file

@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/hamcha/clessy/tg" "github.com/hamcha/tg"
) )
var users map[string]string var users map[string]string

154
tg/api.go
View file

@ -1,154 +0,0 @@
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"`
}

View file

@ -1,166 +0,0 @@
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
}
}
}

View file

@ -1,64 +0,0 @@
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
}

View file

@ -1,16 +0,0 @@
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)
}
}
})
}

View file

@ -1,103 +0,0 @@
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"`
}