From 33197c50101a58c993e62b80e92cdf7c4bcd3973 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Mon, 15 Feb 2016 15:28:41 +0000 Subject: [PATCH 01/10] mods: Added /viaggi (from flotrshi-ports) --- mods/main.go | 7 +++ mods/viaggi.go | 136 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 mods/viaggi.go diff --git a/mods/main.go b/mods/main.go index d9fec01..f5589d6 100644 --- a/mods/main.go +++ b/mods/main.go @@ -7,8 +7,13 @@ import ( "github.com/hamcha/clessy/tg" ) +func initmods() { + initviaggi() +} + func dispatch(broker *tg.Broker, update tg.APIMessage) { metafora(broker, update) + viaggi(broker, update) } func isCommand(update tg.APIMessage, cmdname string) bool { @@ -27,6 +32,8 @@ func main() { botname = flag.String("botname", "maudbot", "Bot name for /targetet@commands") flag.Parse() + initmods() + err := tg.CreateBrokerClient(*brokerAddr, dispatch) if err != nil { panic(err) diff --git a/mods/viaggi.go b/mods/viaggi.go new file mode 100644 index 0000000..9f849a6 --- /dev/null +++ b/mods/viaggi.go @@ -0,0 +1,136 @@ +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 + } + + 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 { + oops(err) + 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 +} From f0d265f3746b0c498036cf9684e6a44bc5916274 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Mon, 15 Feb 2016 15:48:14 +0000 Subject: [PATCH 02/10] mods: some fixes to viaggi --- mods/viaggi.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mods/viaggi.go b/mods/viaggi.go index 9f849a6..93f1b13 100644 --- a/mods/viaggi.go +++ b/mods/viaggi.go @@ -42,6 +42,10 @@ func viaggi(broker *tg.Broker, update tg.APIMessage) { 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 @@ -55,7 +59,7 @@ func viaggi(broker *tg.Broker, update tg.APIMessage) { var outjson Romejson err = json.NewDecoder(resp.Body).Decode(&outjson) if err != nil { - oops(err) + broker.SendTextMessage(update.Chat, "Hm, Rome2rio non ha trovato nulla, mi spiace :(\nForse non hai scritto bene uno dei due posti?", &update.MessageID) return } From 4ce8113afc7c67ee20eb16d50cb73152eec147f6 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Mon, 15 Feb 2016 16:52:25 +0000 Subject: [PATCH 03/10] mods: start work on memegen --- Makefile | 1 + mods/memegen.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 mods/memegen.go diff --git a/Makefile b/Makefile index 5984105..e671976 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ all: clessy-broker clessy-mods clessy-stats deps: go get github.com/boltdb/bolt/... + go get github.com/golang/freetype install-tg: go install github.com/hamcha/clessy/tg diff --git a/mods/memegen.go b/mods/memegen.go new file mode 100644 index 0000000..cd5dfd6 --- /dev/null +++ b/mods/memegen.go @@ -0,0 +1,14 @@ +package main + +import ( + "strings" + + "github.com/hamcha/clessy/tg" +) + +func memegen(broker *tg.Broker, update tg.APIMessage) { + if update.Caption != nil { + if strings.HasPrefix(*(update.Caption), "/meme") { + } + } +} From 883dcc44832a9abadb4864377e8dfaa6fc1215f0 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 19 Feb 2016 11:20:13 +0000 Subject: [PATCH 04/10] Added callback management and godoc comments --- broker/main.go | 1 + broker/telegram.go | 5 +++ broker/webhook.go | 6 +++- mods/memegen.go | 12 +++++++- mods/metafora.go | 4 +-- tg/api.go | 36 +++++++++++++++++++--- tg/broker.go | 77 +++++++++++++++++++++++++++++++++++++++++++++- tg/client.go | 17 ++++++++-- tg/client_test.go | 16 ++++++++++ tg/command.go | 40 ++++++++++++++++++++++-- 10 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 tg/client_test.go 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/memegen.go b/mods/memegen.go index cd5dfd6..ea70c6c 100644 --- a/mods/memegen.go +++ b/mods/memegen.go @@ -1,14 +1,24 @@ package main import ( + "fmt" "strings" "github.com/hamcha/clessy/tg" ) func memegen(broker *tg.Broker, update tg.APIMessage) { - if update.Caption != nil { + if update.Photo != nil && update.Caption != nil { if strings.HasPrefix(*(update.Caption), "/meme") { + maxsz := 0 + photo := tg.APIPhotoSize{} + for _, curphoto := range update.Photo { + if curphoto.Width > maxsz { + maxsz = curphoto.Width + photo = curphoto + } + } + fmt.Println(photo) } } } 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/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..9ebd1d4 100644 --- a/tg/broker.go +++ b/tg/broker.go @@ -7,10 +7,15 @@ import ( "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,13 +24,18 @@ 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 specific chat +// A reply_to message ID can be specified as optional parameter func (b *Broker) SendTextMessage(chat *APIChat, text string, original *int) { cmd := ClientCommand{ Type: CmdSendTextMessage, @@ -42,3 +52,68 @@ func (b *Broker) SendTextMessage(chat *APIChat, text string, original *int) { } fmt.Fprintln(b.Socket, string(data)) } + +// 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 + } + // 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) 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..95fe387 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..4c4595e --- /dev/null +++ b/tg/client_test.go @@ -0,0 +1,16 @@ +package tg + +// This function 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 ExampleHelloClient() { + 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..0c6a12c 100644 --- a/tg/command.go +++ b/tg/command.go @@ -1,18 +1,54 @@ 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 []byte +} + +// 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 } +// 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 + FileRequestData *FileRequestData + Callback *int } From 370ce1c1d8fd31961df13467a767abb4c1e3c5a6 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 19 Feb 2016 11:25:38 +0000 Subject: [PATCH 05/10] tg: Minor corrections to the godoc text --- tg/broker.go | 20 ++++++++++---------- tg/client.go | 4 ++-- tg/client_test.go | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tg/broker.go b/tg/broker.go index 9ebd1d4..90792fb 100644 --- a/tg/broker.go +++ b/tg/broker.go @@ -34,8 +34,8 @@ func (b *Broker) Close() { b.Socket.Close() } -// SendTextMessage sends a HTML-styles text message to a specific chat -// A reply_to message ID can be specified as optional parameter +// SendTextMessage sends a HTML-styles text message to a specific chat. +// A reply_to message ID can be specified as optional parameter. func (b *Broker) SendTextMessage(chat *APIChat, text string, original *int) { cmd := ClientCommand{ Type: CmdSendTextMessage, @@ -53,16 +53,16 @@ func (b *Broker) SendTextMessage(chat *APIChat, text string, original *int) { fmt.Fprintln(b.Socket, string(data)) } -// GetFile sends a file retrieval request to the Broker -// This function is asynchronous as data will be delivered to the given callback +// 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 +// 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 @@ -84,8 +84,8 @@ func (b *Broker) RegisterCallback(fn BrokerCallback) int { return id } -// RemoveCallback removes a callback from the callback list by ID -// This function should never be called by clients +// 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 { @@ -94,8 +94,8 @@ func (b *Broker) RemoveCallback(id int) { b.resizeCbArray() } -// SpliceCallback retrieves a callback by ID and removes it from the list -// This function should never be called by clients +// 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] diff --git a/tg/client.go b/tg/client.go index 95fe387..1fab367 100644 --- a/tg/client.go +++ b/tg/client.go @@ -13,8 +13,8 @@ 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 +// 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 { diff --git a/tg/client_test.go b/tg/client_test.go index 4c4595e..6f7d39d 100644 --- a/tg/client_test.go +++ b/tg/client_test.go @@ -1,4 +1,4 @@ -package tg +package tg_test // This function 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) From 353f227706b0fae476c5d5974d6e64b96fdbbb51 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 19 Feb 2016 11:27:06 +0000 Subject: [PATCH 06/10] tg: Renamed example, maybe this time godoc will be happy --- tg/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tg/client_test.go b/tg/client_test.go index 6f7d39d..f35d5c2 100644 --- a/tg/client_test.go +++ b/tg/client_test.go @@ -2,7 +2,7 @@ package tg_test // This function 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 ExampleHelloClient() { +func ExampleCreateBrokerClient() { CreateBrokerClient("localhost:7314", func(broker *Broker, message APIMessage) { // Check if it's a text message if message.Text != nil { From 2152dd935edac0a6c04c620e508ae41d1bedc1b1 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 19 Feb 2016 11:28:17 +0000 Subject: [PATCH 07/10] tg: Wording, young padawan --- tg/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tg/client_test.go b/tg/client_test.go index f35d5c2..5cd4dd9 100644 --- a/tg/client_test.go +++ b/tg/client_test.go @@ -1,6 +1,6 @@ package tg_test -// This function creates a basic client that connects to a broker and checks for message containing greetings. +// 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) { From 32612670a3b8430e2f35bfac05080b49434e3678 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 19 Feb 2016 16:35:43 +0000 Subject: [PATCH 08/10] mods: Add all the meme generation code --- Makefile | 1 + mods/main.go | 9 +++ mods/memegen.go | 163 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 170 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e671976..f5cd0da 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ 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/mods/main.go b/mods/main.go index f5589d6..3cf7a5c 100644 --- a/mods/main.go +++ b/mods/main.go @@ -9,6 +9,7 @@ import ( func initmods() { initviaggi() + initmeme() } func dispatch(broker *tg.Broker, update tg.APIMessage) { @@ -26,10 +27,12 @@ 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() @@ -39,3 +42,9 @@ func main() { panic(err) } } + +func assert(err error) { + if err != nil { + panic(err) + } +} diff --git a/mods/memegen.go b/mods/memegen.go index ea70c6c..487d269 100644 --- a/mods/memegen.go +++ b/mods/memegen.go @@ -1,15 +1,51 @@ package main import ( - "fmt" + "bytes" + "image" + _ "image/gif" + "image/jpeg" + _ "image/png" + "io/ioutil" + "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 { - if strings.HasPrefix(*(update.Caption), "/meme") { + 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 { @@ -18,7 +54,128 @@ func memegen(broker *tg.Broker, update tg.APIMessage) { photo = curphoto } } - fmt.Println(photo) + broker.GetFile(photo.FileID, func(broker *tg.Broker, data tg.BrokerUpdate) { + img, _, err := image.Decode(bytes.NewReader(data.Bytes)) + if err != nil { + 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})) + }) } } } + +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 +} From 1f113c54bdb0087a21e1a28a96e08d5598dccaea Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 19 Feb 2016 16:54:25 +0000 Subject: [PATCH 09/10] Changed []byte structs to Base64 encoding --- mods/memegen.go | 21 ++++++++++++++++++--- tg/broker.go | 21 +++++++++++++++++++++ tg/command.go | 11 ++++++++++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/mods/memegen.go b/mods/memegen.go index 487d269..5f1831c 100644 --- a/mods/memegen.go +++ b/mods/memegen.go @@ -2,11 +2,13 @@ package main import ( "bytes" + "encoding/base64" "image" _ "image/gif" "image/jpeg" _ "image/png" "io/ioutil" + "log" "os" "strings" @@ -39,7 +41,7 @@ func memegen(broker *tg.Broker, update tg.APIMessage) { 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)) + broker.SendTextMessage(update.Chat, "Formato: /meme TESTO IN ALTO;TESTO IN BASSO", &update.MessageID) return } @@ -55,9 +57,17 @@ func memegen(broker *tg.Broker, update tg.APIMessage) { } } broker.GetFile(photo.FileID, func(broker *tg.Broker, data tg.BrokerUpdate) { - img, _, err := image.Decode(bytes.NewReader(data.Bytes)) + pbytes, err := base64.StdEncoding.DecodeString(*data.Bytes) if err != nil { - broker.SendTextMessage(update.Chat, "ERROR: Non riesco a leggere l'immagine", &(update.MessageID)) + 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 } @@ -145,6 +155,11 @@ func memegen(broker *tg.Broker, update tg.APIMessage) { 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 + } }) } } diff --git a/tg/broker.go b/tg/broker.go index 90792fb..ddeb86f 100644 --- a/tg/broker.go +++ b/tg/broker.go @@ -1,6 +1,7 @@ package tg import ( + "encoding/base64" "encoding/json" "fmt" "log" @@ -53,6 +54,26 @@ func (b *Broker) SendTextMessage(chat *APIChat, text string, original *int) { fmt.Fprintln(b.Socket, string(data)) } +// SendTextMessage sends a HTML-styles text message to a specific chat. +// A reply_to message ID can be specified as optional parameter. +func (b *Broker) SendPhoto(chat *APIChat, data []byte, caption *string, original *int) { + cmd := ClientCommand{ + Type: CmdSendPhoto, + PhotoData: &ClientPhotoData{ + ChatID: chat.ChatID, + Bytes: base64.StdEncoding.EncodeToString(data), + Caption: caption, + ReplyID: original, + }, + } + // Encode command and send to broker + data, err := json.Marshal(cmd) + if err != nil { + log.Printf("[SendPhoto] JSON Encode error: %s\n", err.Error()) + } + fmt.Fprintln(b.Socket, string(data)) +} + // 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 { diff --git a/tg/command.go b/tg/command.go index 0c6a12c..7ec9161 100644 --- a/tg/command.go +++ b/tg/command.go @@ -16,7 +16,7 @@ type BrokerUpdate struct { Type BrokerUpdateType Callback *int Message *APIMessage - Bytes []byte + Bytes *string } // ClientCommandType distinguishes requests sent by clients to the broker @@ -40,6 +40,14 @@ type ClientTextMessageData struct { 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 @@ -49,6 +57,7 @@ type FileRequestData struct { type ClientCommand struct { Type ClientCommandType TextMessageData *ClientTextMessageData + PhotoData *ClientPhotoData FileRequestData *FileRequestData Callback *int } From b393317e7e97f26b429b95c7f3f201656ec76266 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 19 Feb 2016 16:59:36 +0000 Subject: [PATCH 10/10] [tg] Refactor and little godoc fix --- tg/broker.go | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tg/broker.go b/tg/broker.go index ddeb86f..a39aefa 100644 --- a/tg/broker.go +++ b/tg/broker.go @@ -35,29 +35,23 @@ func (b *Broker) Close() { b.Socket.Close() } -// SendTextMessage sends a HTML-styles text message to a specific chat. +// 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, }, - } - // Encode command and send to broker - data, err := json.Marshal(cmd) - if err != nil { - log.Printf("[SendTextMessage] JSON Encode error: %s\n", err.Error()) - } - fmt.Fprintln(b.Socket, string(data)) + }) } -// SendTextMessage sends a HTML-styles text message to a specific chat. +// 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) { - cmd := ClientCommand{ + sendCmd(ClientCommand{ Type: CmdSendPhoto, PhotoData: &ClientPhotoData{ ChatID: chat.ChatID, @@ -65,13 +59,7 @@ func (b *Broker) SendPhoto(chat *APIChat, data []byte, caption *string, original Caption: caption, ReplyID: original, }, - } - // Encode command and send to broker - data, err := json.Marshal(cmd) - if err != nil { - log.Printf("[SendPhoto] JSON Encode error: %s\n", err.Error()) - } - fmt.Fprintln(b.Socket, string(data)) + }) } // GetFile sends a file retrieval request to the Broker. @@ -122,6 +110,14 @@ func (b *Broker) SpliceCallback(id int) BrokerCallback { 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