package health import ( "fmt" "html" "log" "strconv" "strings" "sync" "time" "github.com/eternal-flame-AD/yoake/internal/auth" "github.com/eternal-flame-AD/yoake/internal/comm" "github.com/eternal-flame-AD/yoake/internal/comm/model" "github.com/eternal-flame-AD/yoake/internal/comm/telegram" "github.com/eternal-flame-AD/yoake/internal/db" "github.com/eternal-flame-AD/yoake/internal/util" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/labstack/echo/v4" ) func telegramHandler(database db.DB) telegram.CommandHandler { return func(bot *telegram.Bot, role telegram.Role, msg *tgbotapi.Message) error { if role != telegram.RoleOwner { bot.SendHTML(msg.Chat.ID, "EPERM: You are not authorized to use this command.") return nil } switch msg.Command() { case "medtake": argStr := strings.TrimSpace(msg.CommandArguments()) if len(argStr) == 0 || argStr == "help" { bot.SendHTML(msg.Chat.ID, "Usage: /medtake [dose] [YYYY-MM-DDZHH:mm:ss]") return nil } args := strings.Split(argStr, " ") meds, err := DBMedListGet(database) if err != nil { return err } for _, med := range meds { if med.KeyName() == strings.ToLower(args[0]) { dose := med.Dosage if len(args) > 1 { dose, err = strconv.Atoi(args[1]) if err != nil { return err } } timestamp := time.Now() if len(args) > 2 { timestamp, err = time.Parse("2006-01-02Z15:04:05", args[2]) if err != nil { return err } } var log ComplianceLog log.MedKeyname = med.KeyName() log.Actual.Time = timestamp log.Actual.Dose = dose if err := DBMedComplianceLogSetOne(database, med, &log); err != nil { return err } return bot.SendHTML(msg.Chat.ID, "Success.\n\nDose: %d/%d\nTime Actual: %s\nTime Expected: %s\n\nOffset: %.2f", log.Actual.Dose, log.Expected.Dose, log.Actual.Time.Format("2006-01-02 15:04:05"), log.Expected.Time.Format("2006-01-02 15:04:05"), log.DoseOffset) } } return bot.SendHTML(msg.Chat.ID, "keyname %s not found", args[0]) case "medinfo": meds, err := DBMedListGet(database) if err != nil { return err } logs, err := DBMedComplianceLogGet(database, util.DateRangeAround(time.Now(), 1)) if err != nil { return err } argStr := strings.TrimSpace(msg.CommandArguments()) if argStr == "help" { bot.SendHTML(msg.Chat.ID, "Usage: /medinfo [[keyname...]|all]") return nil } all := true keynames := strings.Split(msg.CommandArguments(), " ") var replies []strings.Builder if argStr == "" || argStr == "all" { all = argStr == "all" replies = make([]strings.Builder, len(meds)) } else { replies = make([]strings.Builder, len(keynames)) } for i, med := range meds { index := -1 if argStr != "" && argStr != "all" { for j, keyname := range keynames { if med.KeyName() == keyname { index = j break } } if index == -1 { continue } } else { index = i } name, dir := med.ShortHand() fmt.Fprintf(&replies[index], "%s %s\n", html.EscapeString(name), html.EscapeString(dir)) fmt.Fprintf(&replies[index], "Dosage: %d\n", med.Dosage) nextDose := logs.ProjectNextDose(med) fmt.Fprintf(&replies[index], "Next Dose: %d%s at %s\n", nextDose.Expected.Dose, html.EscapeString(med.DosageUnit), nextDose.Expected.Time.Format("2006-01-02 15:04:05")) stateStr := "unknown" if util.Contain(med.Flags, DirectionFlagPRN) && nextDose.DoseOffset >= 0 { stateStr = "available" } else if nextDose.DoseOffset > 0 { stateStr = "DUE" } else if util.Contain(med.Flags, DirectionFlagAdLib) { stateStr = "available" } else if nextDose.DoseOffset < 0 { stateStr = "scheduled" } fmt.Fprintf(&replies[index], "Offset: %.2f (%s)\n", nextDose.DoseOffset, stateStr) if !all && nextDose.DoseOffset < 0 { replies[index].Reset() } } var out strings.Builder for _, reply := range replies { if reply.Len() > 0 { out.WriteString(reply.String()) out.WriteString("\n") } } return bot.SendHTML(msg.Chat.ID, out.String()) } return nil } } func Register(g *echo.Group, database db.DB, comm *comm.Communicator) { megsG := g.Group("/meds") { shortHands := megsG.Group("/shorthand") { shortHands.GET("/parse", RESTParseShorthand()) shortHands.POST("/parse", RESTParseShorthand()) shortHands.POST("/format", RESTFormatShorthand()) } writeMutex := new(sync.Mutex) directions := megsG.Group("/directions", auth.RequireMiddleware(auth.RoleAdmin)) { directions.GET("", RESTMedGetDirections(database)) directions.POST("", RESTMedPostDirections(database, writeMutex)) directions.DELETE("/:name", RESTMedDeleteDirections(database, writeMutex)) } compliance := megsG.Group("/compliance", auth.RequireMiddleware(auth.RoleAdmin)) { complianceByMed := compliance.Group("/med/:med") { complianceByMed.GET("/log", RESTComplianceLogGet(database)) complianceByMed.GET("/project", RESTComplianceLogProjectMed(database)) } compliance.GET("/log", RESTComplianceLogGet(database)) compliance.POST("/log", RESTComplianceLogPost(database, writeMutex)) compliance.POST("/recalc", RESTRecalcMedComplianceLog(database, writeMutex)) } } if tgBot := comm.GetMethod("telegram"); tgBot != nil { bot := tgBot.(*telegram.Bot) handler := telegramHandler(database) if err := bot.RegisterCommand("medtake", "take 1 med", handler); err != nil { log.Printf("failed to register telegram command: %v", err) } if err := bot.RegisterCommand("medinfo", "current med info", handler); err != nil { log.Printf("failed to register telegram command: %v", err) } } go func() { ticker := time.NewTicker(5 * time.Minute) notified := make(map[string]time.Time) for { func() { txn := database.NewTransaction(false) defer txn.Discard() existingNotified := make(map[string]time.Time) err := db.GetJSON(txn, []byte("health_meds_compliance_notified_meds"), &existingNotified) if err != nil && !db.IsNotFound(err) { log.Println("Error getting notified meds: ", err) return } for k, v := range existingNotified { o := notified[k] if o.Before(v) { notified[k] = v } } txn.Discard() txn = database.NewTransaction(true) err = db.SetJSON(txn, []byte("health_meds_compliance_notified_meds"), notified) if err != nil { log.Println("Error setting notified meds: ", err) return } else if err := txn.Commit(); err != nil { log.Println("Error committing notified meds: ", err) return } }() meds, err := DBMedListGet(database) if err != nil { log.Println("Error getting med list:", err) continue } logs, err := DBMedComplianceLogGet(database, util.DateRangeAround(time.Now(), 1)) if err != nil { log.Println("Error getting med compliance log:", err) continue } var notifications []CommCtx hasNew := false for _, med := range meds { nextDose := logs.ProjectNextDose(med) if nextDose.Expected.Time.Before(time.Now()) { if lastNotified, ok := notified[med.KeyName()]; !ok || lastNotified.Before(nextDose.Expected.Time) || lastNotified.Add(4*time.Hour).Before(time.Now()) { { if !util.Contain(med.Flags, DirectionFlagPRN) { hasNew = true } } notifications = append(notifications, CommCtx{ Med: med, Dose: nextDose, }) } } } if hasNew { if err := comm.SendGenericMessage("gotify", &model.GenericMessage{ Subject: "Medications Due", Body: commTemplate, MIME: "text/markdown+html/template", Context: notifications, }, true); err != nil { log.Println("Error sending med compliance notification:", err) } for _, n := range notifications { notified[n.Med.KeyName()] = time.Now() } } <-ticker.C } }() }