package health import ( "encoding/json" "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, update tgbotapi.Update) error { msg := update.Message if callback := update.CallbackQuery; callback != nil { msg = callback.Message.ReplyToMessage switch msg.Command() { case "medtake": fields := strings.Fields(callback.Data) if fields[0] == "" { return nil } switch fields[0] { case "medtake_confirm": if msg := callback.Message; msg != nil { if _, err := bot.Client().Send(tgbotapi.NewEditMessageReplyMarkup(msg.Chat.ID, msg.MessageID, tgbotapi.InlineKeyboardMarkup{ InlineKeyboard: [][]tgbotapi.InlineKeyboardButton{}, })); err != nil { return err } } return nil case "medtake_undo": uuid := fields[1] delDose := ComplianceLog{ UUID: uuid, Actual: ComplianceDoseInfo{ Time: time.Now(), Dose: -1, }, } if err := DBMedComplianceLogSetOne(database, Direction{}, &delDose); err != nil { return err } if err := bot.SendHTML(msg.Chat.ID, "Last action has been undone."); err != nil { return err } if _, err := bot.Client().Request(tgbotapi.NewDeleteMessage(msg.Chat.ID, callback.Message.MessageID)); err != nil { return err } return nil } } return nil } else if msg != nil { 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" { return bot.SendHTML(msg.Chat.ID, html.EscapeString("Usage: /medtake [dose] [YYYY-MM-DDZHH:mm:ss]")) } 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 } logJSON, err := json.MarshalIndent(log, "", " ") if err != nil { return err } message := tgbotapi.NewMessage(msg.Chat.ID, fmt.Sprintf("Medication %s taken.
%s
", med.Name, logJSON)) message.ChatID = msg.Chat.ID message.ParseMode = tgbotapi.ModeHTML message.ReplyToMessageID = msg.MessageID message.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup([]tgbotapi.InlineKeyboardButton{ tgbotapi.NewInlineKeyboardButtonData("Undo", fmt.Sprintf("medtake_undo %s", log.UUID)), tgbotapi.NewInlineKeyboardButtonData("Confirm", "medtake_confirm"), }) _, err = bot.Client().Send(message) return err } } 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)) nextDose := logs.ProjectNextDose(med) 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], "Status: %s\n", stateStr) fmt.Fprintf(&replies[index], "Offset: %+.2f period, expected %.2f hrs (%s)\n", nextDose.DoseOffset, float64(time.Until(nextDose.Expected.Time))/float64(time.Hour), nextDose.Expected.Time.Format("2006-01-02 15:04:05")) 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 } 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 } }() }