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
}