Merge branch 'master' of github.com:hamcha/clessy
This commit is contained in:
commit
41cf682fbb
13 changed files with 572 additions and 16 deletions
2
Makefile
2
Makefile
|
@ -2,6 +2,8 @@ all: clessy-broker clessy-mods clessy-stats
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
go get github.com/boltdb/bolt/...
|
go get github.com/boltdb/bolt/...
|
||||||
|
go get github.com/golang/freetype
|
||||||
|
go get github.com/llgcode/draw2d
|
||||||
|
|
||||||
install-tg:
|
install-tg:
|
||||||
go install github.com/hamcha/clessy/tg
|
go install github.com/hamcha/clessy/tg
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The Config data (parsed from JSON)
|
||||||
type Config struct {
|
type Config struct {
|
||||||
BindServer string /* Address:Port to bind for Telegram */
|
BindServer string /* Address:Port to bind for Telegram */
|
||||||
BindClients string /* Address:Port to bind for clients */
|
BindClients string /* Address:Port to bind for clients */
|
||||||
|
|
|
@ -11,18 +11,22 @@ import (
|
||||||
"github.com/hamcha/clessy/tg"
|
"github.com/hamcha/clessy/tg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// APIEndpoint is Telegram's current Bot API base url endpoint
|
||||||
const APIEndpoint = "https://api.telegram.org/"
|
const APIEndpoint = "https://api.telegram.org/"
|
||||||
|
|
||||||
|
// Telegram is the API client for the Telegram Bot API
|
||||||
type Telegram struct {
|
type Telegram struct {
|
||||||
Token string
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mkAPI creates a Telegram instance from a Bot API token
|
||||||
func mkAPI(token string) *Telegram {
|
func mkAPI(token string) *Telegram {
|
||||||
tg := new(Telegram)
|
tg := new(Telegram)
|
||||||
tg.Token = token
|
tg.Token = token
|
||||||
return tg
|
return tg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWebhook sets the webhook address so that Telegram knows where to send updates
|
||||||
func (t Telegram) SetWebhook(webhook string) {
|
func (t Telegram) SetWebhook(webhook string) {
|
||||||
resp, err := http.PostForm(t.apiURL("setWebhook"), url.Values{"url": {webhook}})
|
resp, err := http.PostForm(t.apiURL("setWebhook"), url.Values{"url": {webhook}})
|
||||||
if !checkerr("SetWebhook", err) {
|
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) {
|
func (t Telegram) SendTextMessage(data tg.ClientTextMessageData) {
|
||||||
postdata := url.Values{
|
postdata := url.Values{
|
||||||
"chat_id": {strconv.Itoa(data.ChatID)},
|
"chat_id": {strconv.Itoa(data.ChatID)},
|
||||||
|
|
|
@ -19,7 +19,11 @@ func webhook(rw http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(update)
|
data, err := json.Marshal(tg.BrokerUpdate{
|
||||||
|
Type: tg.BMessage,
|
||||||
|
Message: &(update.Message),
|
||||||
|
Callback: nil,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[webhook] Cannot re-encode json (??) : " + err.Error())
|
log.Println("[webhook] Cannot re-encode json (??) : " + err.Error())
|
||||||
return
|
return
|
||||||
|
|
16
mods/main.go
16
mods/main.go
|
@ -7,8 +7,14 @@ import (
|
||||||
"github.com/hamcha/clessy/tg"
|
"github.com/hamcha/clessy/tg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func initmods() {
|
||||||
|
initviaggi()
|
||||||
|
initmeme()
|
||||||
|
}
|
||||||
|
|
||||||
func dispatch(broker *tg.Broker, update tg.APIMessage) {
|
func dispatch(broker *tg.Broker, update tg.APIMessage) {
|
||||||
metafora(broker, update)
|
metafora(broker, update)
|
||||||
|
viaggi(broker, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCommand(update tg.APIMessage, cmdname string) bool {
|
func isCommand(update tg.APIMessage, cmdname string) bool {
|
||||||
|
@ -21,14 +27,24 @@ func isCommand(update tg.APIMessage, cmdname string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
var botname *string
|
var botname *string
|
||||||
|
var impact *string
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
brokerAddr := flag.String("broker", "localhost:7314", "Broker address:port")
|
brokerAddr := flag.String("broker", "localhost:7314", "Broker address:port")
|
||||||
botname = flag.String("botname", "maudbot", "Bot name for /targetet@commands")
|
botname = flag.String("botname", "maudbot", "Bot name for /targetet@commands")
|
||||||
|
impact = flag.String("impact", "impact.ttf", "Path to impact.ttf (Impact font)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
initmods()
|
||||||
|
|
||||||
err := tg.CreateBrokerClient(*brokerAddr, dispatch)
|
err := tg.CreateBrokerClient(*brokerAddr, dispatch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assert(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
196
mods/memegen.go
Normal file
196
mods/memegen.go
Normal file
|
@ -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, "<b>Formato</b>: /meme TESTO IN ALTO<b>;</b>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, "<b>ERRORE!</b> @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, "<b>ERROR</b>: 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, "<b>ERRORE!</b> @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
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"github.com/hamcha/clessy/tg"
|
"github.com/hamcha/clessy/tg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var actions []string = []string{
|
var actions = []string{
|
||||||
"Puppami", "Degustami", "Lucidami", "Manipolami", "Disidratami", "Irritami", "Martorizzami",
|
"Puppami", "Degustami", "Lucidami", "Manipolami", "Disidratami", "Irritami", "Martorizzami",
|
||||||
"Lustrami", "Osannami", "Sorseggiami", "Assaporami", "Apostrofami", "Spremimi", "Dimenami",
|
"Lustrami", "Osannami", "Sorseggiami", "Assaporami", "Apostrofami", "Spremimi", "Dimenami",
|
||||||
"Agitami", "Stimolami", "Suonami", "Strimpellami", "Stuzzicami", "Spintonami", "Sguinzagliami",
|
"Agitami", "Stimolami", "Suonami", "Strimpellami", "Stuzzicami", "Spintonami", "Sguinzagliami",
|
||||||
|
@ -14,7 +14,7 @@ var actions []string = []string{
|
||||||
"Accordami", "Debuggami",
|
"Accordami", "Debuggami",
|
||||||
}
|
}
|
||||||
|
|
||||||
var objects []string = []string{
|
var objects = []string{
|
||||||
"il birillo", "il bastone", "l'ombrello", "il malloppo", "il manico", "il manganello",
|
"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 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",
|
"il fiorino", "lo scettro", "il campanile", "la proboscide", "il pino", "il maritozzo", "il perno",
|
||||||
|
|
140
mods/viaggi.go
Normal file
140
mods/viaggi.go
Normal file
|
@ -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 <i><PARTENZA></i> -> <i><DESTINAZIONE></i>", &update.MessageID)
|
||||||
|
}
|
||||||
|
oops := func(err error) {
|
||||||
|
log.Println("[viaggi] GET error:" + err.Error())
|
||||||
|
broker.SendTextMessage(update.Chat, "<b>ERRORE!</b> @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, "<b>ERRORE!</b> @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 <b>"+outjson.Places[0].Name+
|
||||||
|
"</b> a <b>"+outjson.Places[1].Name+"</b>"+
|
||||||
|
"\n\n"+
|
||||||
|
"Piu economico: <b>"+moreeco.Name+"</b> ("+parseData(moreeco)+")"+
|
||||||
|
"\n"+
|
||||||
|
"Piu veloce: <b>"+lesstim.Name+"</b> ("+parseData(lesstim)+")"+
|
||||||
|
"\n\n"+
|
||||||
|
"<a href=\"http://www.rome2rio.com/it/s/"+src+"/"+dst+"\">Maggiori informazioni</a>",
|
||||||
|
&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
|
||||||
|
}
|
36
tg/api.go
36
tg/api.go
|
@ -1,5 +1,6 @@
|
||||||
package tg
|
package tg
|
||||||
|
|
||||||
|
// APIUser represents the "User" JSON structure
|
||||||
type APIUser struct {
|
type APIUser struct {
|
||||||
UserID int `json:"id"`
|
UserID int `json:"id"`
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
|
@ -7,15 +8,24 @@ type APIUser struct {
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChatType defines the type of chat
|
||||||
type ChatType string
|
type ChatType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChatTypePrivate ChatType = "private"
|
// ChatTypePrivate is a private chat (between user and bot)
|
||||||
ChatTypeGroup ChatType = "group"
|
ChatTypePrivate ChatType = "private"
|
||||||
|
|
||||||
|
// ChatTypeGroup is a group chat (<100 members)
|
||||||
|
ChatTypeGroup ChatType = "group"
|
||||||
|
|
||||||
|
// ChatTypeSupergroup is a supergroup chat (>=100 members)
|
||||||
ChatTypeSupergroup ChatType = "supergroup"
|
ChatTypeSupergroup ChatType = "supergroup"
|
||||||
ChatTypeChannel ChatType = "channel"
|
|
||||||
|
// ChatTypeChannel is a channel (Read-only)
|
||||||
|
ChatTypeChannel ChatType = "channel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// APIChat represents the "Chat" JSON structure
|
||||||
type APIChat struct {
|
type APIChat struct {
|
||||||
ChatID int `json:"id"`
|
ChatID int `json:"id"`
|
||||||
Type ChatType `json:"type"`
|
Type ChatType `json:"type"`
|
||||||
|
@ -25,6 +35,7 @@ type APIChat struct {
|
||||||
LastName *string `json:"last_name,omitempty"`
|
LastName *string `json:"last_name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIMessage represents the "Message" JSON structure
|
||||||
type APIMessage struct {
|
type APIMessage struct {
|
||||||
MessageID int `json:"message_id"`
|
MessageID int `json:"message_id"`
|
||||||
User APIUser `json:"from"`
|
User APIUser `json:"from"`
|
||||||
|
@ -43,7 +54,7 @@ type APIMessage struct {
|
||||||
Caption *string `json:"caption,omitempty"`
|
Caption *string `json:"caption,omitempty"`
|
||||||
Contact *APIContact `json:"contact,omitempty"`
|
Contact *APIContact `json:"contact,omitempty"`
|
||||||
Location *APILocation `json:"location,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"`
|
LeftUser *APIUser `json:"left_chat_partecipant,omitempty"`
|
||||||
PhotoDeleted *bool `json:"delete_chat_photo,omitempty"`
|
PhotoDeleted *bool `json:"delete_chat_photo,omitempty"`
|
||||||
GroupCreated *bool `json:"group_chat_created,omitempty"`
|
GroupCreated *bool `json:"group_chat_created,omitempty"`
|
||||||
|
@ -53,6 +64,7 @@ type APIMessage struct {
|
||||||
GroupFromSuper *int `json:"migrate_from_chat_id,omitempty"`
|
GroupFromSuper *int `json:"migrate_from_chat_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIPhotoSize represents the "PhotoSize" JSON structure
|
||||||
type APIPhotoSize struct {
|
type APIPhotoSize struct {
|
||||||
FileID string `json:"file_id"`
|
FileID string `json:"file_id"`
|
||||||
Width int `json:"width"`
|
Width int `json:"width"`
|
||||||
|
@ -60,6 +72,7 @@ type APIPhotoSize struct {
|
||||||
FileSize *int `json:"file_size,omitempty"`
|
FileSize *int `json:"file_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIAudio represents the "Audio" JSON structure
|
||||||
type APIAudio struct {
|
type APIAudio struct {
|
||||||
FileID string `json:"file_id"`
|
FileID string `json:"file_id"`
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
|
@ -69,6 +82,7 @@ type APIAudio struct {
|
||||||
FileSize *int `json:"file_size,omitempty"`
|
FileSize *int `json:"file_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIDocument represents the "Document" JSON structure
|
||||||
type APIDocument struct {
|
type APIDocument struct {
|
||||||
FileID string `json:"file_id"`
|
FileID string `json:"file_id"`
|
||||||
Thumbnail *APIPhotoSize `json:"thumb,omitempty"`
|
Thumbnail *APIPhotoSize `json:"thumb,omitempty"`
|
||||||
|
@ -77,6 +91,7 @@ type APIDocument struct {
|
||||||
FileSize *int `json:"file_size,omitempty"`
|
FileSize *int `json:"file_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APISticker represents the "Sticker" JSON structure
|
||||||
type APISticker struct {
|
type APISticker struct {
|
||||||
FileID string `json:"file_id"`
|
FileID string `json:"file_id"`
|
||||||
Width int `json:"width"`
|
Width int `json:"width"`
|
||||||
|
@ -85,6 +100,7 @@ type APISticker struct {
|
||||||
FileSize *int `json:"file_size,omitempty"`
|
FileSize *int `json:"file_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIVideo represents the "Video" JSON structure
|
||||||
type APIVideo struct {
|
type APIVideo struct {
|
||||||
FileID string `json:"file_id"`
|
FileID string `json:"file_id"`
|
||||||
Width int `json:"width"`
|
Width int `json:"width"`
|
||||||
|
@ -95,6 +111,7 @@ type APIVideo struct {
|
||||||
FileSize *int `json:"file_size,omitempty"`
|
FileSize *int `json:"file_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIVoice represents the "Voice" JSON structure
|
||||||
type APIVoice struct {
|
type APIVoice struct {
|
||||||
FileID string `json:"file_id"`
|
FileID string `json:"file_id"`
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
|
@ -102,6 +119,7 @@ type APIVoice struct {
|
||||||
FileSize *int `json:"file_size,omitempty"`
|
FileSize *int `json:"file_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIContact represents the "Contact" JSON structure
|
||||||
type APIContact struct {
|
type APIContact struct {
|
||||||
PhoneNumber string `json:"phone_number"`
|
PhoneNumber string `json:"phone_number"`
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
|
@ -109,16 +127,26 @@ type APIContact struct {
|
||||||
UserID *int `json:"user_id,omitempty"`
|
UserID *int `json:"user_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APILocation represents the "Location" JSON structure
|
||||||
type APILocation struct {
|
type APILocation struct {
|
||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
Latitude float64 `json:"latitude"`
|
Latitude float64 `json:"latitude"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIUpdate represents the "Update" JSON structure
|
||||||
type APIUpdate struct {
|
type APIUpdate struct {
|
||||||
UpdateID int `json:"update_id"`
|
UpdateID int `json:"update_id"`
|
||||||
Message APIMessage `json:"message"`
|
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 {
|
type APIResponse struct {
|
||||||
Ok bool `json:"ok"`
|
Ok bool `json:"ok"`
|
||||||
ErrCode *int `json:"error_code,omitempty"`
|
ErrCode *int `json:"error_code,omitempty"`
|
||||||
|
|
100
tg/broker.go
100
tg/broker.go
|
@ -1,16 +1,22 @@
|
||||||
package tg
|
package tg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Broker is a broker connection handler with callback management functions
|
||||||
type Broker struct {
|
type Broker struct {
|
||||||
Socket net.Conn
|
Socket net.Conn
|
||||||
|
Callbacks []BrokerCallback
|
||||||
|
|
||||||
|
cbFree int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConnectToBroker creates a Broker connection
|
||||||
func ConnectToBroker(brokerAddr string) (*Broker, error) {
|
func ConnectToBroker(brokerAddr string) (*Broker, error) {
|
||||||
sock, err := net.Dial("tcp", brokerAddr)
|
sock, err := net.Dial("tcp", brokerAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -19,26 +25,112 @@ func ConnectToBroker(brokerAddr string) (*Broker, error) {
|
||||||
|
|
||||||
broker := new(Broker)
|
broker := new(Broker)
|
||||||
broker.Socket = sock
|
broker.Socket = sock
|
||||||
|
broker.Callbacks = make([]BrokerCallback, 0)
|
||||||
|
broker.cbFree = 0
|
||||||
return broker, nil
|
return broker, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes a broker connection
|
||||||
func (b *Broker) Close() {
|
func (b *Broker) Close() {
|
||||||
b.Socket.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) {
|
func (b *Broker) SendTextMessage(chat *APIChat, text string, original *int) {
|
||||||
cmd := ClientCommand{
|
sendCmd(ClientCommand{
|
||||||
Type: CmdSendTextMessage,
|
Type: CmdSendTextMessage,
|
||||||
TextMessageData: &ClientTextMessageData{
|
TextMessageData: &ClientTextMessageData{
|
||||||
Text: text,
|
Text: text,
|
||||||
ChatID: chat.ChatID,
|
ChatID: chat.ChatID,
|
||||||
ReplyID: original,
|
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)
|
data, err := json.Marshal(cmd)
|
||||||
if err != nil {
|
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))
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
17
tg/client.go
17
tg/client.go
|
@ -7,8 +7,14 @@ import (
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateHandler is an update handler for webhook updates
|
||||||
type UpdateHandler func(broker *Broker, message APIMessage)
|
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 {
|
func CreateBrokerClient(brokerAddr string, updateFn UpdateHandler) error {
|
||||||
broker, err := ConnectToBroker(brokerAddr)
|
broker, err := ConnectToBroker(brokerAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -23,7 +29,7 @@ func CreateBrokerClient(brokerAddr string, updateFn UpdateHandler) error {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
var update APIUpdate
|
var update BrokerUpdate
|
||||||
err = json.Unmarshal(bytes, &update)
|
err = json.Unmarshal(bytes, &update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[tg - CreateBrokerClient] ERROR reading JSON: %s\r\n", err.Error())
|
log.Printf("[tg - CreateBrokerClient] ERROR reading JSON: %s\r\n", err.Error())
|
||||||
|
@ -31,8 +37,13 @@ func CreateBrokerClient(brokerAddr string, updateFn UpdateHandler) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch to UpdateHandler
|
if update.Callback == nil {
|
||||||
updateFn(broker, update.Message)
|
// 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
|
return io.EOF
|
||||||
}
|
}
|
||||||
|
|
16
tg/client_test.go
Normal file
16
tg/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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,18 +1,63 @@
|
||||||
package tg
|
package tg
|
||||||
|
|
||||||
type ClientCommandType uint
|
// BrokerUpdateType distinguishes update types coming from the broker
|
||||||
|
type BrokerUpdateType string
|
||||||
|
|
||||||
const (
|
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 {
|
type ClientTextMessageData struct {
|
||||||
ChatID int
|
ChatID int
|
||||||
Text string
|
Text string
|
||||||
ReplyID *int
|
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 ClientCommand struct {
|
||||||
Type ClientCommandType
|
Type ClientCommandType
|
||||||
TextMessageData *ClientTextMessageData
|
TextMessageData *ClientTextMessageData
|
||||||
|
PhotoData *ClientPhotoData
|
||||||
|
FileRequestData *FileRequestData
|
||||||
|
Callback *int
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue