Mercurial > lbo > hg > goe_bot
changeset 27:4a84e6d53800
Add SQL backend infrastructure and first TODO handler
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Sat, 10 Dec 2016 12:07:29 +0100 |
parents | d584798666b9 |
children | be37dbe1c05d |
files | handler_todo.go main.go queries.sql sql/storage.go sql/todo.go |
diffstat | 5 files changed, 178 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/handler_todo.go Sat Dec 10 12:07:11 2016 +0100 +++ b/handler_todo.go Sat Dec 10 12:07:29 2016 +0100 @@ -2,11 +2,34 @@ import ( "context" + "errors" + "fmt" "log" _ "bitbucket.org/dermesser/goe_bot/sql" ) +func todoHandler(ctx context.Context, msg message) (replyContent, error) { + if backend == nil { + log.Println("todoHandler: Not connected to DB!") + return replyContent{text: "_Interner Fehler: Nicht mit Datenbank verbunden_"}, errors.New("not connected to DB") + } + + todo, err := backend.Todo(msg.Chat.ID) + + if err != nil { + return replyContent{text: "_Zugriff fehlgeschlagen:_ " + err.Error()}, err + } + + id, err := todo.AddTodo(msg.Text, msg.From.First_Name+" "+msg.From.Last_Name) + + if err != nil { + return replyContent{text: "_Erstellung fehlgeschlagen:_ " + err.Error()}, err + } + + return replyContent{text: fmt.Sprintf("Aufgabe #%d erstellt", id)}, nil +} + func todoButtonTest(ctx context.Context, msg message) (replyContent, error) { buttonRows := [][]inlineKeyboardButton{}
--- a/main.go Sat Dec 10 12:07:11 2016 +0100 +++ b/main.go Sat Dec 10 12:07:29 2016 +0100 @@ -4,9 +4,12 @@ "flag" "log" "net/http" + + "bitbucket.org/dermesser/goe_bot/sql" ) var ( + // FLAGS flagToken = flag.String("token", "", "The telegram API token") flagWebhookToken = flag.String("hook_token", "supersecrettoken", "A shared secret authenticating the Telegram API to the bot") @@ -19,8 +22,28 @@ flagUsePull = flag.Bool("pull", false, "Actively pull updates (instead of using the webhook)") flagMyURL = flag.String("myurl", "", "Webhook URL to register") flagListenAddr = flag.String("listen", "[::1]:4000", "Listen address for HTTP server.") + + flagDatabase = flag.String("dbname", "goe_bot", "Database name") + flagDBUser = flag.String("dbuser", "goe_bot", "Database user") + flagDBPass = flag.String("dbpass", "", "Database password (please prefer peer authentication!)") + flagDBPort = flag.Uint("dbport", 5432, "Database port") + flagDBHost = flag.String("dbhost", "/var/run/postgresql/", "Socket path or address for database") + + // SHARED VARIABLES + backend *sql.Storage ) +func setUpDatabase() error { + c := sql.MakeConnectParams(*flagDatabase, *flagDBUser, *flagDBPass, *flagDBHost, *flagDBPort) + db, err := sql.NewStorage(c) + + if err == nil { + backend = db + } + + return err +} + func main() { log.SetFlags(log.Lmicroseconds) flag.Parse() @@ -38,6 +61,10 @@ return } + if err := setUpDatabase(); err != nil { + return + } + mux := http.NewServeMux() mux.HandleFunc("/debug", debugHandler) mux.HandleFunc("/hook", updatesHandler)
--- a/queries.sql Sat Dec 10 12:07:11 2016 +0100 +++ b/queries.sql Sat Dec 10 12:07:29 2016 +0100 @@ -17,5 +17,9 @@ LIMIT 1; UPDATE quotes SET quoted = quoted + 1 WHERE id = $0; -- $0 comes from previous query +-- Add todo item (w/o lists) +INSERT INTO todos (text, owner, chat_id) VALUES ($0, $1, $2) RETURNING id; -- Select open todo items SELECT id, text, owner FROM todo WHERE NOT done AND chat_id = $0 AND list = '' ORDER BY ts; +-- Mark item as done +UPDATE todo SET done = true WHERE id = $0;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sql/storage.go Sat Dec 10 12:07:29 2016 +0100 @@ -0,0 +1,40 @@ +package sql + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/lib/pq" +) + +// main abstraction over the Postgres backend. Provides high-level operations to handlers. +type Storage struct { + db *sql.DB + // prepared statements cache. Keyed by the actual query, which are constants in the + // action-specific files + prepared map[string]*sql.Stmt +} + +func MakeConnectParams(db, user, pass, host string, port uint) string { + return fmt.Sprintf("dbname=%s user=%s password='%s' host=%s port=%d", db, user, pass, host, port) +} + +func NewStorage(connString string) (*Storage, error) { + log.Println("Opening database...") + + db, err := sql.Open("postgres", connString) + + if err != nil { + log.Println("Couldn't establish database connection:", err) + return nil, err + } + + log.Println("Successfully connected to database") + + return &Storage{db: db, prepared: make(map[string]*sql.Stmt)}, nil +} + +func (s *Storage) Todo(chatID uint64) (Todo, error) { + return newTodo(s, chatID) +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sql/todo.go Sat Dec 10 12:07:29 2016 +0100 @@ -0,0 +1,84 @@ +// This file contains logic for retrieving and manipulating todo lists. +package sql + +import ( + "errors" + "log" +) + +const ( + // parameters: 1 = text, 2 = owner, 3 = chat ID + // returns: todo ID INTEGER + insertTodo = `INSERT INTO todo (ts, text, owner, chat_id) VALUES (now(), $1, $2, $3) RETURNING id` + // parameters: 1 = chat ID, 2 = list ID (default '') + // returns: id INTEGER, text TEXT, owner TEXT + selectOpenTodos = `SELECT id, text, owner FROM todo WHERE NOT done AND chat_id = $1 AND list = $2 ORDER BY ts ASC` + // parameters: 1 = todo ID + // returns: n/a + markTodoDone = `UPDATE todo SET done = true WHERE id = $1` +) + +// Data access object for todo lists +type Todo struct { + chatID uint64 + db *Storage +} + +func newTodo(s *Storage, chat uint64) (Todo, error) { + t := Todo{chatID: chat, db: s} + + return t, t.prewarm() +} + +// prepare statements. MUST be called before any further use (usually by the Storage object +// that creates the todo object) +func (td Todo) prewarm() error { + for _, q := range []string{ + insertTodo, + selectOpenTodos, + markTodoDone, + } { + if _, ok := td.db.prepared[q]; !ok { + stmt, err := td.db.db.Prepare(q) + + if err == nil { + td.db.prepared[q] = stmt + } else { + log.Println("Couldn't prepare:", err) + return err + } + } + } + return nil +} + +func (td Todo) AddTodo(text, owner string) (uint, error) { + if stmt, ok := td.db.prepared[insertTodo]; ok { + rows, err := stmt.Query(text, owner, td.chatID) + + if err != nil { + log.Println("Couldn't insert todo:", err) + return 0, err + } + + defer rows.Close() + + if rows.Next() { + var id uint + err = rows.Scan(&id) + + if err != nil { + log.Println("todo: Couldn't fetch new ID:", err) + return 0, err + } + + return id, nil + } else { + log.Println("todo: No row returned!") + return 0, errors.New("no row returned") + } + } else { + log.Println("Couldn't find prepared statement for", insertTodo) + return 0, errors.New("prepared statement not found") + } +}