changeset 45:7a18eb2dd075

Implement reminder handler
author Lewin Bormann <lbo@spheniscida.de>
date Sat, 10 Dec 2016 17:33:15 +0100
parents 88245ca6ed0e
children 52ec77607624
files handler_remind.go handlers.go main.go remind.go sql/remind.go
diffstat 5 files changed, 152 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/handler_remind.go	Sat Dec 10 17:33:15 2016 +0100
@@ -0,0 +1,115 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"bitbucket.org/dermesser/goe_bot/sql"
+)
+
+var (
+	hhmmRE       = regexp.MustCompile(`(\d{1,2}):(\d\d)`)
+	durRE        = regexp.MustCompile(`\+?(\d+)([smhd])`)
+	dateRE       = regexp.MustCompile(`(\d{4})-(\d\d)-(\d\d) (\d{1,2}):(\d\d)`)
+	dateOfWeekRE = regexp.MustCompile(`(Mo|Di|Mi|Do|Fr|Sa|So) (\d{1,2}):(\d\d)`)
+)
+
+func parseReminderString(s string) time.Time {
+	now := time.Now().In(time.FixedZone("local", *flagLocalOffset))
+	log.Println(s)
+
+	if m := hhmmRE.FindStringSubmatch(s); len(m) >= 3 {
+		log.Println(m)
+		h, err1 := strconv.ParseInt(m[1], 10, 32)
+		m, err2 := strconv.ParseInt(m[2], 10, 32)
+
+		if err1 == nil && err2 == nil {
+			nowH, nowM, nowS := now.Clock()
+			timeOfDay := time.Duration(nowH)*time.Hour + time.Duration(nowM)*time.Minute + time.Duration(nowS)*time.Second
+			midnight := now.Add(-timeOfDay)
+			// duration since midnight of alert time
+			alert := time.Duration(h)*time.Hour + time.Duration(m)*time.Minute
+
+			if alert > timeOfDay { // later today
+				return midnight.Add(alert)
+			} else { // that time tomorrow
+				return midnight.Add(24*time.Hour + alert)
+			}
+		} else {
+			return time.Unix(0, 0)
+		}
+	} else if m := durRE.FindStringSubmatch(s); len(m) >= 3 {
+		log.Println(m)
+		dur, err := strconv.ParseInt(m[1], 10, 32)
+
+		if err != nil {
+			return time.Unix(0, 0)
+		}
+
+		if m[2] == "s" {
+			return now.Add(time.Duration(dur) * time.Second)
+		} else if m[2] == "m" {
+			return now.Add(time.Duration(dur) * time.Minute)
+		} else if m[2] == "h" {
+			return now.Add(time.Duration(dur) * time.Hour)
+		} else if m[2] == "d" {
+			return now.Add(time.Duration(dur) * 24 * time.Hour)
+		} else {
+			return time.Unix(0, 0)
+		}
+	}
+
+	// rest is not implemented yet
+
+	return time.Unix(0, 0)
+}
+
+// Handler for /remind messages
+// Allowed formats: hh:mm (today), +XXs, +XXm, +XXh, +XXd, "yyyy-mm-dd hh:mm",
+// {Mo,Di,Mi,Do,Fr,Sa,So} hh:mm,
+func reminderHandler(ctx context.Context, msg message) (replyContent, error) {
+	// TODO: This doesn't work with multi-part formats like dateRE or dateOfWeekRE!
+	// first part is time
+	parts := strings.SplitN(msg.Text, " ", 2)
+
+	if len(parts) < 2 {
+		log.Println("Not enough parts in message:", msg.Text)
+		return replyContent{text: "_Hilfe (z.B.): /remind 25m Irgendwas_"}, errors.New("bad message format")
+	}
+
+	alertTime := parseReminderString(parts[0])
+
+	if alertTime.IsZero() {
+		return replyContent{text: "Tut mir leid, ich verstehe das Format nicht. Bitte benutze +XX{s,m,h,d} oder hh:mm"},
+			errors.New("didn't understand time format")
+	}
+
+	db, err := backend.Reminders()
+
+	if err != nil {
+		log.Println("Couldn't get Reminders object:", err)
+		return replyContent{text: "_Ich konnte mich nicht mit der Datenbank verbinden :(_"}, errors.New("couldn't get Reminders")
+	}
+
+	r := reminder{
+		Text:    parts[1],
+		Owner:   msg.From.First_Name + " " + msg.From.Last_Name,
+		Due:     alertTime,
+		ChatID:  msg.Chat.ID,
+		ReplyTo: msg.Message_ID}
+
+	id, err := db.InsertReminder(sql.Reminder(r))
+
+	if err != nil {
+		log.Println("Couldn't insert reminder:", err)
+		return replyContent{text: "_Ich konnte leider keine Erinnerung setzen._"}, errors.New("couldn't set reminder")
+	}
+
+	return replyContent{text: fmt.Sprintf("*✓* Erinnerung #%d in %v", id, alertTime.Sub(time.Now()))}, nil
+}
--- a/handlers.go	Sat Dec 10 17:22:54 2016 +0100
+++ b/handlers.go	Sat Dec 10 17:33:15 2016 +0100
@@ -32,7 +32,7 @@
 	handlers = map[string]handler{
 		echoCmd:    {echoHandler, "Anfrage zurücksenden"},
 		fortuneCmd: {fortuneHandler, "Glückskeks"},
-		remindCmd:  {missingHandler, "Wecker"},
+		remindCmd:  {reminderHandler, "Wecker"},
 		statusCmd:  {statusHandler, "Status anfragen"},
 		todoCmd:    {todoHandler, "Aufgabenliste"},
 	}
--- a/main.go	Sat Dec 10 17:22:54 2016 +0100
+++ b/main.go	Sat Dec 10 17:33:15 2016 +0100
@@ -29,6 +29,8 @@
 	flagDBPort   = flag.Uint("dbport", 5432, "Database port")
 	flagDBHost   = flag.String("dbhost", "/var/run/postgresql/", "Socket path or address for database")
 
+	flagLocalOffset = flag.Int("tz_off", 3600, "Seconds offset from UTC")
+
 	// SHARED VARIABLES
 	backend *sql.Storage
 )
@@ -67,6 +69,9 @@
 		return
 	}
 
+	// Start background reminder thread
+	go (&reminders{}).reminderChecker()
+
 	mux := http.NewServeMux()
 	mux.HandleFunc("/debug", debugHandler)
 	mux.HandleFunc("/hook", updatesHandler)
--- a/remind.go	Sat Dec 10 17:22:54 2016 +0100
+++ b/remind.go	Sat Dec 10 17:33:15 2016 +0100
@@ -22,8 +22,12 @@
 		return rm.removeFromDB()
 	}
 
-	msg := sendMessage{Chat_ID: rm.ChatID, Parse_Mode: "Markdown",
-		Reply_To_Message_Id: rm.ReplyTo, Text: rm.Text}
+	msg := sendMessage{
+		Chat_ID:             rm.ChatID,
+		Parse_Mode:          "Markdown",
+		Reply_To_Message_Id: rm.ReplyTo,
+		Text:                "*BEEP BEEP BEEP*",
+	}
 
 	rm.Attempts++
 	err := sendChatMessage(msg)
@@ -65,7 +69,6 @@
 	inner:
 		for {
 			time.Sleep(2 * time.Second)
-			now := time.Now()
 
 			events, err := db.SelectDueReminders()
 
@@ -76,10 +79,6 @@
 			}
 
 			for _, rm := range events {
-				if !now.After(rm.Due) {
-					continue
-				}
-
 				err := reminder(rm).fire()
 
 				if err != nil {
--- a/sql/remind.go	Sat Dec 10 17:22:54 2016 +0100
+++ b/sql/remind.go	Sat Dec 10 17:33:15 2016 +0100
@@ -5,14 +5,16 @@
 	"errors"
 	"log"
 	"time"
+
+	"github.com/lib/pq"
 )
 
 // prepared statements
 const (
-	// Parameters: 1 = due time, 2 = text, 3 = chat ID, 4 = command message ID
+	// Parameters: 1 = due time, 2 = description, 3 = owner, 4 = chat ID, 5 = command message ID
 	// Returns: id INTEGER
-	insertReminder = `INSERT INTO reminders (created, due, description, chat_id, orig_msg_id)
-    VALUES (now(), $1, $2, $3, $4) RETURNING id`
+	insertReminder = `INSERT INTO reminders (created, due, description, owner, chat_id, orig_msg_id)
+    VALUES (now(), $1, $2, $3, $4, $5) RETURNING id`
 	// Parameters: n/a
 	// Returns: id INTEGER, due TIMESTAMP, owner TEXT, description TEXT, chat_id INTEGER, orig_msg_id INTEGER
 	selectDueReminders = `SELECT id, due, owner, description, chat_id, orig_msg_id FROM reminders WHERE NOT done AND now() > due ORDER BY due ASC`
@@ -64,6 +66,26 @@
 	}
 }
 
+func (r Reminders) InsertReminder(rm Reminder) (uint, error) {
+	if stmt, ok := r.db.prepared[insertReminder]; ok {
+		row := stmt.QueryRow(pq.FormatTimestamp(rm.Due), rm.Text, rm.Owner, rm.ChatID, rm.ReplyTo)
+
+		var id uint
+
+		err := row.Scan(&id)
+
+		if err != nil {
+			log.Println("Couldn't insert reminder:", err)
+			return 0, err
+		}
+
+		return id, nil
+	} else {
+		log.Println("Couldn't find prepared statement for", insertReminder)
+		return 0, errors.New("couldn't find prepared statement")
+	}
+}
+
 func (r Reminders) SelectDueReminders() ([]Reminder, error) {
 	if stmt, ok := r.db.prepared[selectDueReminders]; ok {
 		rows, err := stmt.Query()