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 }