diff --git a/Makefile b/Makefile
index 5984105..f5cd0da 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,8 @@ all: clessy-broker clessy-mods clessy-stats
deps:
go get github.com/boltdb/bolt/...
+ go get github.com/golang/freetype
+ go get github.com/llgcode/draw2d
install-tg:
go install github.com/hamcha/clessy/tg
diff --git a/broker/main.go b/broker/main.go
index 764cf56..df2469e 100644
--- a/broker/main.go
+++ b/broker/main.go
@@ -8,6 +8,7 @@ import (
"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 */
diff --git a/broker/telegram.go b/broker/telegram.go
index a54df4e..ea5ef35 100644
--- a/broker/telegram.go
+++ b/broker/telegram.go
@@ -11,18 +11,22 @@ import (
"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", err) {
@@ -42,6 +46,7 @@ func (t Telegram) SetWebhook(webhook string) {
}
}
+// SendTextMessage sends an HTML-styled text message to a specified chat
func (t Telegram) SendTextMessage(data tg.ClientTextMessageData) {
postdata := url.Values{
"chat_id": {strconv.Itoa(data.ChatID)},
diff --git a/broker/webhook.go b/broker/webhook.go
index 8cd037a..8e999d1 100644
--- a/broker/webhook.go
+++ b/broker/webhook.go
@@ -19,7 +19,11 @@ func webhook(rw http.ResponseWriter, req *http.Request) {
return
}
- data, err := json.Marshal(update)
+ 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
diff --git a/mods/main.go b/mods/main.go
index d9fec01..3cf7a5c 100644
--- a/mods/main.go
+++ b/mods/main.go
@@ -7,8 +7,14 @@ import (
"github.com/hamcha/clessy/tg"
)
+func initmods() {
+ initviaggi()
+ initmeme()
+}
+
func dispatch(broker *tg.Broker, update tg.APIMessage) {
metafora(broker, update)
+ viaggi(broker, update)
}
func isCommand(update tg.APIMessage, cmdname string) bool {
@@ -21,14 +27,24 @@ func isCommand(update tg.APIMessage, cmdname string) bool {
}
var botname *string
+var impact *string
func main() {
brokerAddr := flag.String("broker", "localhost:7314", "Broker address:port")
botname = flag.String("botname", "maudbot", "Bot name for /targetet@commands")
+ impact = flag.String("impact", "impact.ttf", "Path to impact.ttf (Impact font)")
flag.Parse()
+ initmods()
+
err := tg.CreateBrokerClient(*brokerAddr, dispatch)
if err != nil {
panic(err)
}
}
+
+func assert(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/mods/memegen.go b/mods/memegen.go
new file mode 100644
index 0000000..5f1831c
--- /dev/null
+++ b/mods/memegen.go
@@ -0,0 +1,196 @@
+package main
+
+import (
+ "bytes"
+ "encoding/base64"
+ "image"
+ _ "image/gif"
+ "image/jpeg"
+ _ "image/png"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+
+ "github.com/golang/freetype"
+ "github.com/hamcha/clessy/tg"
+ "github.com/llgcode/draw2d"
+ "github.com/llgcode/draw2d/draw2dimg"
+)
+
+var memeFontData draw2d.FontData
+
+func initmeme() {
+ fontfile, err := os.Open(*impact)
+ assert(err)
+ defer fontfile.Close()
+
+ bytes, err := ioutil.ReadAll(fontfile)
+ assert(err)
+
+ font, err := freetype.ParseFont(bytes)
+ assert(err)
+
+ memeFontData := draw2d.FontData{"impact", draw2d.FontFamilySans, 0}
+ draw2d.RegisterFont(memeFontData, font)
+}
+
+func memegen(broker *tg.Broker, update tg.APIMessage) {
+ if update.Photo != nil && update.Caption != nil {
+ caption := *(update.Caption)
+ if strings.HasPrefix(caption, "/meme ") && len(caption) > 6 {
+ idx := strings.Index(caption, ";")
+ if idx < 0 {
+ broker.SendTextMessage(update.Chat, "Formato: /meme TESTO IN ALTO;TESTO IN BASSO", &update.MessageID)
+ return
+ }
+
+ txtup := caption[6:idx]
+ txtdw := caption[idx+1:]
+
+ maxsz := 0
+ photo := tg.APIPhotoSize{}
+ for _, curphoto := range update.Photo {
+ if curphoto.Width > maxsz {
+ maxsz = curphoto.Width
+ photo = curphoto
+ }
+ }
+ broker.GetFile(photo.FileID, func(broker *tg.Broker, data tg.BrokerUpdate) {
+ pbytes, err := base64.StdEncoding.DecodeString(*data.Bytes)
+ if err != nil {
+ log.Println("[memegen] Base64 decode error: %s\n", err.Error())
+ broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID)
+ return
+ }
+
+ img, _, err := image.Decode(bytes.NewReader(pbytes))
+ if err != nil {
+ log.Println("[memegen] Image decode error: %s\n", err.Error())
+ broker.SendTextMessage(update.Chat, "ERROR: Non riesco a leggere l'immagine", &update.MessageID)
+ return
+ }
+
+ //TODO Clean up this mess
+
+ // Create target image
+ bounds := img.Bounds()
+ iwidth := float64(bounds.Size().X)
+ iheight := float64(bounds.Size().Y)
+
+ timg := image.NewRGBA(bounds)
+ gc := draw2dimg.NewGraphicContext(timg)
+ gc.SetStrokeColor(image.Black)
+ gc.SetFillColor(image.White)
+ gc.SetFontData(memeFontData)
+ gc.DrawImage(img)
+
+ write := func(text string, istop bool) {
+ text = strings.ToUpper(strings.TrimSpace(text))
+ gc.Restore()
+ gc.Save()
+
+ // Detect appropriate font size
+ scale := iwidth / iheight * 200
+ gc.SetFontSize(scale)
+ gc.SetLineWidth(scale / 15)
+
+ // Get NEW bounds
+ left, top, right, bottom := gc.GetStringBounds(text)
+
+ width := right - left
+ texts := []string{text}
+ if width > iwidth {
+ // Split text
+ texts = splitCenter(text)
+
+ // Get longest line
+ longer := float64(0)
+ longid := 0
+ widths := make([]float64, len(texts))
+ for id := range texts {
+ tleft, _, tright, _ := gc.GetStringBounds(texts[id])
+ widths[id] = tright - tleft
+ if width > longer {
+ longer = widths[id]
+ longid = id
+ }
+ }
+
+ // Still too big? Decrease font size again
+ iter := 0
+ for width > iwidth && iter < 10 {
+ left, _, right, _ = gc.GetStringBounds(texts[longid])
+ gc.SetFontSize(scale * 0.8)
+ width = right - left
+ iter++
+ }
+ }
+
+ height := bottom - top
+ margin := float64(10)
+ lines := float64(len(texts) - 1)
+
+ gc.Save()
+ for id, txt := range texts {
+ gc.Save()
+ left, _, right, _ = gc.GetStringBounds(txt)
+ width = right - left
+
+ y := float64(0)
+ if istop {
+ y = (height-margin)*float64(id+1) + margin*5
+ } else {
+ y = iheight - (height * lines) + (height * float64(id)) - margin*5
+ }
+
+ gc.Translate((iwidth-width)/2, y)
+ gc.StrokeString(txt)
+ gc.FillString(txt)
+ gc.Restore()
+ }
+ }
+ write(txtup, true)
+ write(txtdw, false)
+
+ buf := new(bytes.Buffer)
+ err = jpeg.Encode(buf, timg, &(jpeg.Options{Quality: 80}))
+ if err != nil {
+ log.Println("[memegen] Image encode error: %s\n", err.Error())
+ broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID)
+ return
+ }
+ })
+ }
+ }
+}
+
+func splitCenter(text string) []string {
+ cindx := int(len(text) / 2)
+ bs := 0
+ md := 9999
+ id := -1
+ for i := 0; ; i++ {
+ idx := strings.Index(text[bs:], " ")
+ if idx < 0 {
+ break
+ }
+ bs += idx + 1
+ diff := abs(cindx - bs)
+ if diff < md {
+ md = diff
+ id = bs - 1
+ }
+ }
+ if id >= 0 {
+ return []string{text[0:id], text[id+1:]}
+ }
+ return []string{text}
+}
+
+func abs(i int) int {
+ if i < 0 {
+ return -i
+ }
+ return i
+}
diff --git a/mods/metafora.go b/mods/metafora.go
index a76cc20..8469d1f 100644
--- a/mods/metafora.go
+++ b/mods/metafora.go
@@ -6,7 +6,7 @@ import (
"github.com/hamcha/clessy/tg"
)
-var actions []string = []string{
+var actions = []string{
"Puppami", "Degustami", "Lucidami", "Manipolami", "Disidratami", "Irritami", "Martorizzami",
"Lustrami", "Osannami", "Sorseggiami", "Assaporami", "Apostrofami", "Spremimi", "Dimenami",
"Agitami", "Stimolami", "Suonami", "Strimpellami", "Stuzzicami", "Spintonami", "Sguinzagliami",
@@ -14,7 +14,7 @@ var actions []string = []string{
"Accordami", "Debuggami",
}
-var objects []string = []string{
+var objects = []string{
"il birillo", "il bastone", "l'ombrello", "il malloppo", "il manico", "il manganello",
"il ferro", "la mazza", "l'archibugio", "il timone", "l'arpione", "il flauto", "la reliquia",
"il fiorino", "lo scettro", "il campanile", "la proboscide", "il pino", "il maritozzo", "il perno",
diff --git a/mods/viaggi.go b/mods/viaggi.go
new file mode 100644
index 0000000..93f1b13
--- /dev/null
+++ b/mods/viaggi.go
@@ -0,0 +1,140 @@
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/hamcha/clessy/tg"
+)
+
+const viaggiurl = "http://free.rome2rio.com/api/1.2/json/Search?key=X5JMLHNc&languageCode=IT¤cyCode=EUR"
+
+var reg *regexp.Regexp
+
+func initviaggi() {
+ reg = regexp.MustCompile("([^-]+) -> (.+)")
+}
+
+func viaggi(broker *tg.Broker, update tg.APIMessage) {
+ if isCommand(update, "viaggi") {
+ usage := func() {
+ broker.SendTextMessage(update.Chat, "Formato: /viaggi <PARTENZA> -> <DESTINAZIONE>", &update.MessageID)
+ }
+ oops := func(err error) {
+ log.Println("[viaggi] GET error:" + err.Error())
+ broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID)
+ }
+
+ parts := strings.SplitN(*(update.Text), " ", 2)
+ if len(parts) < 2 {
+ usage()
+ return
+ }
+ text := parts[1]
+ msgs := reg.FindStringSubmatch(text)
+ if len(msgs) <= 2 {
+ usage()
+ return
+ }
+
+ msgs[1] = strings.Replace(msgs[1], ",", "", -1)
+ msgs[1] = strings.Replace(msgs[1], " ", "-", -1)
+ msgs[2] = strings.Replace(msgs[2], ",", "", -1)
+ msgs[2] = strings.Replace(msgs[2], " ", "-", -1)
+ src := url.QueryEscape(msgs[1])
+ dst := url.QueryEscape(msgs[2])
+ url := viaggiurl + "&oName=" + src + "&dName=" + dst
+ resp, err := http.Get(url)
+ if err != nil {
+ oops(err)
+ return
+ }
+ defer resp.Body.Close()
+
+ var outjson Romejson
+ err = json.NewDecoder(resp.Body).Decode(&outjson)
+ if err != nil {
+ broker.SendTextMessage(update.Chat, "Hm, Rome2rio non ha trovato nulla, mi spiace :(\nForse non hai scritto bene uno dei due posti?", &update.MessageID)
+ return
+ }
+
+ var moreeco Romeroute
+ var lesstim Romeroute
+ if len(outjson.Routes) < 1 {
+ // Should never happen
+ log.Println("[viaggi] No routes found (??)")
+ broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID)
+ return
+ }
+
+ // Calculate cheapest and fastest
+ moreeco = outjson.Routes[0]
+ lesstim = outjson.Routes[0]
+ for _, v := range outjson.Routes {
+ if v.IndicativePrice.Price < moreeco.IndicativePrice.Price {
+ moreeco = v
+ }
+ if v.Duration < lesstim.Duration {
+ lesstim = v
+ }
+ }
+
+ broker.SendTextMessage(update.Chat,
+ "Viaggio da "+outjson.Places[0].Name+
+ " a "+outjson.Places[1].Name+""+
+ "\n\n"+
+ "Piu economico: "+moreeco.Name+" ("+parseData(moreeco)+")"+
+ "\n"+
+ "Piu veloce: "+lesstim.Name+" ("+parseData(lesstim)+")"+
+ "\n\n"+
+ "Maggiori informazioni",
+ &update.MessageID)
+ }
+}
+
+func parseData(route Romeroute) string {
+ // Get time
+ minutes := int(route.Duration)
+ hours := minutes / 60
+ minutes -= hours * 60
+ days := hours / 24
+ hours -= days * 24
+ timestamp := ""
+ if days > 0 {
+ timestamp += strconv.Itoa(days) + "d "
+ }
+ if hours > 0 {
+ timestamp += strconv.Itoa(hours) + "h "
+ }
+ if minutes > 0 {
+ timestamp += strconv.Itoa(minutes) + "m"
+ }
+
+ return strconv.Itoa(int(route.IndicativePrice.Price)) + " " + route.IndicativePrice.Currency + " - " + strconv.Itoa(int(route.Distance)) + " Km - " + timestamp
+}
+
+type Romeplace struct {
+ Name string
+}
+
+type Romeprice struct {
+ Price float64
+ Currency string
+}
+
+type Romeroute struct {
+ Name string
+ Distance float64
+ Duration float64
+ IndicativePrice Romeprice
+}
+
+type Romejson struct {
+ Places []Romeplace
+ Routes []Romeroute
+}
diff --git a/tg/api.go b/tg/api.go
index 567c1ff..a7e0cb5 100644
--- a/tg/api.go
+++ b/tg/api.go
@@ -1,5 +1,6 @@
package tg
+// APIUser represents the "User" JSON structure
type APIUser struct {
UserID int `json:"id"`
FirstName string `json:"first_name"`
@@ -7,15 +8,24 @@ type APIUser struct {
Username string `json:"username,omitempty"`
}
+// ChatType defines the type of chat
type ChatType string
const (
- ChatTypePrivate ChatType = "private"
- ChatTypeGroup ChatType = "group"
+ // 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 ChatType = "channel"
+
+ // ChatTypeChannel is a channel (Read-only)
+ ChatTypeChannel ChatType = "channel"
)
+// APIChat represents the "Chat" JSON structure
type APIChat struct {
ChatID int `json:"id"`
Type ChatType `json:"type"`
@@ -25,6 +35,7 @@ type APIChat struct {
LastName *string `json:"last_name,omitempty"`
}
+// APIMessage represents the "Message" JSON structure
type APIMessage struct {
MessageID int `json:"message_id"`
User APIUser `json:"from"`
@@ -43,7 +54,7 @@ type APIMessage struct {
Caption *string `json:"caption,omitempty"`
Contact *APIContact `json:"contact,omitempty"`
Location *APILocation `json:"location,omitempty"`
- NewUser *APIUser `json:"new_chat_partecipant",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"`
@@ -53,6 +64,7 @@ type APIMessage struct {
GroupFromSuper *int `json:"migrate_from_chat_id,omitempty"`
}
+// APIPhotoSize represents the "PhotoSize" JSON structure
type APIPhotoSize struct {
FileID string `json:"file_id"`
Width int `json:"width"`
@@ -60,6 +72,7 @@ type APIPhotoSize struct {
FileSize *int `json:"file_size,omitempty"`
}
+// APIAudio represents the "Audio" JSON structure
type APIAudio struct {
FileID string `json:"file_id"`
Duration int `json:"duration"`
@@ -69,6 +82,7 @@ type APIAudio struct {
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"`
@@ -77,6 +91,7 @@ type APIDocument struct {
FileSize *int `json:"file_size,omitempty"`
}
+// APISticker represents the "Sticker" JSON structure
type APISticker struct {
FileID string `json:"file_id"`
Width int `json:"width"`
@@ -85,6 +100,7 @@ type APISticker struct {
FileSize *int `json:"file_size,omitempty"`
}
+// APIVideo represents the "Video" JSON structure
type APIVideo struct {
FileID string `json:"file_id"`
Width int `json:"width"`
@@ -95,6 +111,7 @@ type APIVideo struct {
FileSize *int `json:"file_size,omitempty"`
}
+// APIVoice represents the "Voice" JSON structure
type APIVoice struct {
FileID string `json:"file_id"`
Duration int `json:"duration"`
@@ -102,6 +119,7 @@ type APIVoice struct {
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"`
@@ -109,16 +127,26 @@ type APIContact struct {
UserID *int `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 int `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"`
diff --git a/tg/broker.go b/tg/broker.go
index 4fc2059..a39aefa 100644
--- a/tg/broker.go
+++ b/tg/broker.go
@@ -1,16 +1,22 @@
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
+ 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 {
@@ -19,26 +25,112 @@ func ConnectToBroker(brokerAddr string) (*Broker, error) {
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 *int) {
- cmd := ClientCommand{
+ 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, caption *string, original *int) {
+ sendCmd(ClientCommand{
+ Type: CmdSendPhoto,
+ PhotoData: &ClientPhotoData{
+ ChatID: chat.ChatID,
+ Bytes: base64.StdEncoding.EncodeToString(data),
+ Caption: caption,
+ ReplyID: original,
+ },
+ })
+}
+
+// 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)
+ // Make file request
+ 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
}
- // Encode command and send to broker
+ // 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("[SendTextMessage] JSON Encode error: %s\n", err.Error())
+ 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
+ }
+ }
+}
diff --git a/tg/client.go b/tg/client.go
index a8ebdc0..1fab367 100644
--- a/tg/client.go
+++ b/tg/client.go
@@ -7,8 +7,14 @@ import (
"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 {
@@ -23,7 +29,7 @@ func CreateBrokerClient(brokerAddr string, updateFn UpdateHandler) error {
break
}
- var update APIUpdate
+ var update BrokerUpdate
err = json.Unmarshal(bytes, &update)
if err != nil {
log.Printf("[tg - CreateBrokerClient] ERROR reading JSON: %s\r\n", err.Error())
@@ -31,8 +37,13 @@ func CreateBrokerClient(brokerAddr string, updateFn UpdateHandler) error {
continue
}
- // Dispatch to UpdateHandler
- updateFn(broker, update.Message)
+ 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)
+ }
}
return io.EOF
}
diff --git a/tg/client_test.go b/tg/client_test.go
new file mode 100644
index 0000000..5cd4dd9
--- /dev/null
+++ b/tg/client_test.go
@@ -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)
+ }
+ }
+ })
+}
diff --git a/tg/command.go b/tg/command.go
index f12b343..7ec9161 100644
--- a/tg/command.go
+++ b/tg/command.go
@@ -1,18 +1,63 @@
package tg
-type ClientCommandType uint
+// BrokerUpdateType distinguishes update types coming from the broker
+type BrokerUpdateType string
const (
- CmdSendTextMessage ClientCommandType = 1
+ // BMessage is a message update (mostly webhook updates)
+ BMessage BrokerUpdateType = "message"
+
+ // BFile is a file retrieval response update
+ BFile BrokerUpdateType = "file"
)
+// BrokerUpdate is what is sent by the broker as update
+type BrokerUpdate struct {
+ Type BrokerUpdateType
+ Callback *int
+ Message *APIMessage
+ Bytes *string
+}
+
+// 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"
+
+ // CmdGetFile requests the broker to get a file from Telegram
+ CmdGetFile ClientCommandType = "getFile"
+)
+
+// ClientTextMessageData is the required data for a CmdSendTextMessage request
type ClientTextMessageData struct {
ChatID int
Text string
ReplyID *int
}
+// ClientPhotoData is the required data for a CmdSendPhoto request
+type ClientPhotoData struct {
+ ChatID int
+ Bytes string
+ Caption *string
+ ReplyID *int
+}
+
+// FileRequestData is the required data for a CmdGetFile request
+type FileRequestData struct {
+ FileID int
+}
+
+// ClientCommand is a request sent by clients to the broker
type ClientCommand struct {
Type ClientCommandType
TextMessageData *ClientTextMessageData
+ PhotoData *ClientPhotoData
+ FileRequestData *FileRequestData
+ Callback *int
}