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")
+	}
+}