package speak import ( "log/slog" "os" "os/exec" "strings" "sync" "time" "gopkg.awl.red/cicada/config" ) var log = slog.New(slog.NewJSONHandler(os.Stdout, nil)) var muSpeak sync.Mutex var daysth = map[int]string{ 1: "first", 2: "second", 3: "third", 4: "fourth", 5: "fifth", 6: "sixth", 7: "seventh", 8: "eighth", 9: "ninth", 10: "tenth", 11: "eleventh", 12: "twelfth", 13: "thirteenth", 14: "fourteenth", 15: "fifteenth", 16: "sixteenth", 17: "seventeenth", 18: "eighteenth", 19: "nineteenth", 20: "twentieth", 21: "twenty-first", 22: "twenty-secondth", 23: "twenty-third", 24: "twenty-fourth", 25: "twenty-fifth", 26: "twenty-sixth", 27: "twenty-seventh", 28: "twenty-eighth", 29: "twenty-ninth", 30: "thirtieth", 31: "thirty-first", } func th(t time.Time) string { dayWord, ok := daysth[t.Day()] if !ok { return "_2" } return dayWord } func timeAsSpeak(t time.Time, omitTimezone bool) string { var timeFormat string st := th(t) if omitTimezone { timeFormat = "January " + st + " 3 04 PM" } else { timeFormat = "January _2 15 04 MST" } return t.Format(timeFormat) } func Speak(str string, cfg config.Config) error { if cfg.SkipVoicing { return nil } speakerAndArgs := strings.Split(cfg.Speaker, " ") args := []string{} if len(speakerAndArgs) > 1 { args = speakerAndArgs[1:] } args = append(args, str) cmdSpeaker := exec.Command(speakerAndArgs[0], args...) var err error muSpeak.Lock() if cfg.SpeakerPipe == "" { err = cmdSpeaker.Run() } else { pipeCmdAndArgs := strings.Split(cfg.SpeakerPipe, " ") args := []string{} if len(pipeCmdAndArgs) > 1 { args = pipeCmdAndArgs[1:] } cmdSpeakerPipe := exec.Command(pipeCmdAndArgs[0], args...) cmdSpeakerPipe.Stdin, err = cmdSpeaker.StdoutPipe() if err != nil { log.Error("could not set Speaker as pipe to SpeakerPipe", "err", err) } errSpeaker := cmdSpeaker.Start() if errSpeaker != nil { log.Error("err start Speaker", "err", errSpeaker) } errSpeakerPipe := cmdSpeakerPipe.Start() if errSpeakerPipe != nil { log.Error("err start SpeakerPipe", "err", errSpeakerPipe) } errSpeakerWait := cmdSpeaker.Wait() errSpeakerPipeWait := cmdSpeakerPipe.Wait() if errSpeakerWait != nil { log.Error("err wait Speaker", "err", errSpeakerWait) } if errSpeakerPipeWait != nil { log.Error("err wait SpeakerPipe2", "err", errSpeakerPipeWait) } } muSpeak.Unlock() if err != nil { log.Error("unable to speak", "err", err) return err } return nil } type Alert struct { Id int Service string Component string Error string Message string Severity string LastVoiced time.Time StatusTime time.Time } func (sa *Alert) Speak(cfg config.Config) error { var msg strings.Builder if sa.Service != "" { msg.WriteString(" service: ") msg.WriteString(sa.Service) } if sa.Component != "" { msg.WriteString(" component: ") msg.WriteString(sa.Component) } if sa.Error != "" { msg.WriteString(" error: ") msg.WriteString(sa.Error) } if sa.Message != "" { msg.WriteString(" message: ") msg.WriteString(sa.Message) } if !sa.StatusTime.IsZero() { msg.WriteString(". Since ") saLocal := sa.StatusTime setLocal := false if cfg.SpeakerTimezone != "" { loc, err := time.LoadLocation(cfg.SpeakerTimezone) if err == nil { saLocal = sa.StatusTime.In(loc) setLocal = true } } msg.WriteString(timeAsSpeak(saLocal, setLocal)) } err := Speak(msg.String(), cfg) return err } type Scan func(...any) error func (sa *Alert) FetchId(scanner Scan, id int, cfg config.Config) error { var lastVoiced *time.Time err := scanner(id, &sa.Id, &sa.Service, &sa.Component, &sa.Error, &sa.Message, &sa.Severity, &lastVoiced, &sa.StatusTime, ) if err != nil { return err } if lastVoiced != nil { sa.LastVoiced = *lastVoiced } sa.LastVoiced = sa.LastVoiced.UTC() sa.StatusTime = sa.StatusTime.UTC() return nil }