Added callback management and godoc comments

This commit is contained in:
Hamcha 2016-02-19 11:20:13 +00:00
parent 4ce8113afc
commit 883dcc4483
10 changed files with 200 additions and 14 deletions

View file

@ -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 */

View file

@ -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)},

View file

@ -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

View file

@ -1,14 +1,24 @@
package main package main
import ( import (
"fmt"
"strings" "strings"
"github.com/hamcha/clessy/tg" "github.com/hamcha/clessy/tg"
) )
func memegen(broker *tg.Broker, update tg.APIMessage) { 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") { 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)
} }
} }
} }

View file

@ -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",

View file

@ -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 is a private chat (between user and bot)
ChatTypePrivate ChatType = "private" ChatTypePrivate ChatType = "private"
// ChatTypeGroup is a group chat (<100 members)
ChatTypeGroup ChatType = "group" ChatTypeGroup ChatType = "group"
// ChatTypeSupergroup is a supergroup chat (>=100 members)
ChatTypeSupergroup ChatType = "supergroup" ChatTypeSupergroup ChatType = "supergroup"
// ChatTypeChannel is a channel (Read-only)
ChatTypeChannel ChatType = "channel" 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"`

View file

@ -7,10 +7,15 @@ import (
"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,13 +24,18 @@ 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 specific 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{ cmd := ClientCommand{
Type: CmdSendTextMessage, Type: CmdSendTextMessage,
@ -42,3 +52,68 @@ func (b *Broker) SendTextMessage(chat *APIChat, text string, original *int) {
} }
fmt.Fprintln(b.Socket, string(data)) 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
}
}
}

View file

@ -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
View file

@ -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)
}
}
})
}

View file

@ -1,18 +1,54 @@
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 []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 { type ClientTextMessageData struct {
ChatID int ChatID int
Text string Text string
ReplyID *int 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
FileRequestData *FileRequestData
Callback *int
} }