package main import ( "bufio" "errors" "fmt" "log" "os" "strconv" "time" "github.com/google/uuid" "gopkg.in/yaml.v2" ) type ( DB interface { HistoryOf(string) map[string][]History Next(string, string) time.Time Question(string) Question LastAnswer(string, string) Answer Answer(string) Answer PushAnswer(string, string, string, bool) error } Question struct { Q string Tags []string } Answer struct { Q string A string TS int64 Author string } History struct { A string TS int64 Pass bool } Duration time.Duration ) func main() { if err := Main(); err != nil { panic(err) } } func Main() error { db, err := NewDB() if err != nil { return err } user := os.Getenv("USER") for q, history := range db.HistoryOf(user) { log.Printf("%s/%s/%+v", user, q, history) if time.Until(db.Next(user, q)) > 0 { continue } question := db.Question(q) fmt.Printf("> Q: %s\n", question.Q) fmt.Printf("> %+v\n\t", question.Tags) response := readline() if lastAnswer := db.Answer(db.LastAnswer(user, q).A); lastAnswer.A != "" { fmt.Printf("> Last time, you responded:\n\t%s\n", lastAnswer.A) } fmt.Printf("> Did you pass this time? [Yn] ") pass := readline() == "n" if err := db.PushAnswer(user, q, response, pass); err != nil { return err } fmt.Println() } if b, _ := yaml.Marshal(db); len(b) > 0 { log.Printf("%s\n", b) } return nil } func readline() string { reader := bufio.NewReader(os.Stdin) text, _ := reader.ReadString('\n') return text } func NewDB() (DB, error) { var db db if b, err := os.ReadFile(os.Getenv("DB")); err != nil { return nil, err } else if err := yaml.Unmarshal(b, &db); err != nil { return nil, err } return db, nil } func (db db) HistoryOf(user string) map[string][]History { return db.Users[user].History } func (db db) Question(q string) Question { return db.Knowledge.Questions[q] } func (db db) Answer(a string) Answer { return db.Knowledge.Answers[a] } func (db db) LastAnswer(user, q string) Answer { for _, v := range db.Knowledge.Answers { if v.Q == q && v.Author == user { return v } } return Answer{} } func (db db) PushAnswer(user, q, a string, pass bool) error { uuid := uuid.New().String() db.Knowledge.Answers[uuid] = Answer{ Q: q, A: a, TS: time.Now().UnixNano(), Author: user, } db.Users[user].History[q] = append(db.Users[user].History[q], History{ A: uuid, Pass: pass, }) return nil } func (db db) Next(user, q string) time.Time { history := db.Users[user].History[q] progress := 0 for i := range history { if history[i].Pass { progress += 1 } else { progress -= 1 } } if progress < 0 { progress = 0 } else if progress > len(db.Cadence) { return time.Now().Add(time.Hour * 24 * 365 * 10) } return db.LastTS(user, q).Add(time.Duration(db.Cadence[progress])) } func (db db) LastTS(user, q string) time.Time { max := int64(0) for _, v := range db.Users[user].History[q] { if v.TS > max { max = v.TS } } return time.Unix(0, max) } func (d Duration) MarshalYAML() (interface{}, error) { return time.Duration(d).String(), nil } func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } count := "" var ttl time.Duration for i := range s { if s[i] < '0' || s[i] > '9' { n, err := strconv.Atoi(count) if err != nil { return err } count += s[i : i+1] switch s[i] { case 'w': count = fmt.Sprintf("%dh", n*24*7) case 'd': count = fmt.Sprintf("%dh", n*24) } d, err := time.ParseDuration(count) if err != nil { return err } ttl += d count = "" } else { count += s[i : i+1] } } if count != "" { return errors.New(count) } return nil }