package main import ( // External werr "git.gibonuddevalla.se/go/wrappederror" // Standard "encoding/json" "fmt" "regexp" "strconv" "strings" "time" ) func init() { fmt.Printf("") } type Schedule struct { ID int UserID int `json:"user_id" db:"user_id"` Node Node ScheduleUUID string `db:"schedule_uuid"` Time time.Time RemindMinutes int `db:"remind_minutes"` Description string Acknowledged bool } func scheduleHandler() { // {{{ // Wait for the approximate minute. wait := 60000 - time.Now().Sub(time.Now().Truncate(time.Minute)).Milliseconds() logger.Info("schedule", "wait", wait/1000) time.Sleep(time.Millisecond * time.Duration(wait)) tick := time.NewTicker(time.Minute) for { schedules := ExpiredSchedules() for _, event := range schedules { notificationManager.Send( event.UserID, event.ScheduleUUID, []byte( fmt.Sprintf( "%s\n%s", event.Time.Format("2006-01-02 15:04"), event.Description, ), ), ) } <-tick.C } } // }}} func ScanForSchedules(timezone string, content string) (schedules []Schedule) { // {{{ schedules = []Schedule{} rxp := regexp.MustCompile(`\{\s*([0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}(?::[0-9]{2})?)\s*\,\s*(?:(\d+)\s*(h|min)\s*,)?\s*([^\]]+?)\s*\}`) foundSchedules := rxp.FindAllStringSubmatch(content, -1) for _, data := range foundSchedules { // Missing seconds if strings.Count(data[1], ":") == 1 { data[1] = data[1] + ":00" } logger.Info("time", "parse", data[1]) location, err := time.LoadLocation(timezone) if err != nil { err = werr.Wrap(err).WithCode("002-00010") return } timestamp, err := time.ParseInLocation("2006-01-02 15:04:05", data[1], location) if err != nil { continue } // Reminder var remindMinutes int if data[2] != "" && data[3] != "" { value, _ := strconv.Atoi(data[2]) unit := strings.ToLower(data[3]) switch unit { case "min": remindMinutes = value case "h": remindMinutes = value * 60 } } schedule := Schedule{ Time: timestamp, RemindMinutes: remindMinutes, Description: data[4], } schedules = append(schedules, schedule) } return } // }}} func RetrieveSchedules(userID int, nodeID int) (schedules []Schedule, err error) { // {{{ schedules = []Schedule{} res := service.Db.Conn.QueryRow(` WITH schedule_events AS ( SELECT s.id, s.user_id, json_build_object('id', s.node_id) AS node, s.schedule_uuid, (time - MAKE_INTERVAL(mins => s.remind_minutes)) AT TIME ZONE u.timezone AS time, s.description, s.acknowledged FROM schedule s INNER JOIN _webservice.user u ON s.user_id = u.id WHERE user_id=$1 AND CASE WHEN $2 > 0 THEN node_id = $2 ELSE true END ) SELECT COALESCE(jsonb_agg(s.*), '[]'::jsonb) FROM schedule_events s `, userID, nodeID, ) var data []byte err = res.Scan(&data) if err != nil { err = werr.Wrap(err).WithCode("002-000E") return } err = json.Unmarshal(data, &schedules) return } // }}} func (a Schedule) IsEqual(b Schedule) bool { // {{{ return a.UserID == b.UserID && a.Node.ID == b.Node.ID && a.Time.Equal(b.Time) && a.RemindMinutes == b.RemindMinutes && a.Description == b.Description } // }}} func (s *Schedule) Insert(queryable Queryable) error { // {{{ res := queryable.QueryRow(` INSERT INTO schedule(user_id, node_id, time, remind_minutes, description) VALUES($1, $2, $3, $4, $5) RETURNING id `, s.UserID, s.Node.ID, s.Time.Format("2006-01-02 15:04:05"), s.RemindMinutes, s.Description, ) err := res.Scan(&s.ID) if err != nil { err = werr.Wrap(err).WithCode("002-000D") } return err } // }}} func (s *Schedule) Delete(queryable Queryable) error { // {{{ _, err := queryable.Exec(` DELETE FROM schedule WHERE user_id = $1 AND id = $2 `, s.UserID, s.ID, ) return err } // }}} func ExpiredSchedules() (schedules []Schedule) { // {{{ schedules = []Schedule{} res, err := service.Db.Conn.Queryx(` SELECT s.id, s.user_id, s.node_id, s.schedule_uuid, (s.time - MAKE_INTERVAL(mins => s.remind_minutes)) AT TIME ZONE u.timezone AS time, s.description FROM schedule s INNER JOIN _webservice.user u ON s.user_id = u.id WHERE (time - MAKE_INTERVAL(mins => remind_minutes)) AT TIME ZONE u.timezone < NOW() AND NOT acknowledged ORDER BY time ASC `) if err != nil { err = werr.Wrap(err).WithCode("002-0009") return } defer res.Close() for res.Next() { s := Schedule{} if err = res.Scan(&s.ID, &s.UserID, &s.Node.ID, &s.ScheduleUUID, &s.Time, &s.Description); err != nil { werr.Wrap(err).WithCode("002-000a") continue } schedules = append(schedules, s) } return } // }}} func FutureSchedules(userID int, nodeID int, start time.Time, end time.Time) (schedules []Schedule, err error) { // {{{ schedules = []Schedule{} var foo string row := service.Db.Conn.QueryRow(`SELECT TO_CHAR($1::date AT TIME ZONE 'UTC', 'yyyy-mm-dd HH24:MI')`, start) err = row.Scan(&foo) if err != nil { return } logger.Info("FOO", "date", foo) res := service.Db.Conn.QueryRow(` WITH schedule_events AS ( SELECT s.id, s.user_id, jsonb_build_object( 'id', n.id, 'name', n.name, 'updated', n.updated ) AS node, s.schedule_uuid, time AT TIME ZONE u.timezone AS time, s.description, s.acknowledged, s.remind_minutes AS RemindMinutes FROM schedule s INNER JOIN _webservice.user u ON s.user_id = u.id INNER JOIN node n ON s.node_id = n.id WHERE s.user_id = $1 AND ( CASE WHEN $2 > 0 THEN n.id = $2 ELSE true END ) AND ( CASE WHEN TO_CHAR($3::date, 'yyyy-mm-dd HH24:MI') = '0001-01-01 00:00' THEN TRUE ELSE (s.time AT TIME ZONE u.timezone) >= $3 END ) AND ( CASE WHEN TO_CHAR($4::date, 'yyyy-mm-dd HH24:MI') = '0001-01-01 00:00' THEN TRUE ELSE (s.time AT TIME ZONE u.timezone) <= $4 END ) AND time >= NOW() AND NOT acknowledged ) SELECT COALESCE(jsonb_agg(s.*), '[]'::jsonb) FROM schedule_events s `, userID, nodeID, start, end, ) var j []byte err = res.Scan(&j) if err != nil { err = werr.Wrap(err).WithCode("002-000B").WithData(userID) return } err = json.Unmarshal(j, &schedules) if err != nil { err = werr.Wrap(err).WithCode("002-0010").WithData(string(j)).Log() return } return } // }}}