diff --git a/core/config.go b/core/config.go index 7f25d93..3167888 100644 --- a/core/config.go +++ b/core/config.go @@ -16,12 +16,6 @@ type Config struct { Matrix MatrixConfig } -type MatrixConfig struct { - UserId string - AccessToken string - RoomId string -} - func LoadConfigFromTOML(configPath string) (*Config, error) { file, err := os.Open(configPath) if err != nil { diff --git a/core/database.go b/core/database.go index 7c0307a..3e0502c 100644 --- a/core/database.go +++ b/core/database.go @@ -14,13 +14,13 @@ import ( "github.com/oklog/ulid/v2" - "git.exozy.me/exozyme/status/scanner" + . "git.exozy.me/exozyme/status/scanner" ) type Row struct { time time.Time kind EventType - data scanner.ServiceStatus + data ServiceStatus } type EventType int @@ -48,7 +48,8 @@ type Database interface { QueryAllAfter(cutoff time.Time) ([]Row, error) // store service status records with timestamp - StoreServiceStatusBatch(entries scanner.ServiceStatusBatch, when time.Time) error + // returns non-duplicate rows + StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) // store self up/down event with timestamp // @@ -59,10 +60,10 @@ type Database interface { type Span struct { start time.Time stop time.Time - status scanner.ServiceStatus + status ServiceStatus } -func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status scanner.ServiceStatus) { +func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status ServiceStatus) { var stop_time time.Time if stop_i < 0 { stop_time = time.Now() @@ -78,7 +79,7 @@ func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status scan func CalculateServiceStatusSpans(rows []Row, service_url string) []Span { res := []Span{} start_i := -1 - var status scanner.ServiceStatus + var status ServiceStatus for i, v := range rows { switch v.kind { @@ -210,7 +211,9 @@ func (db *dumbDatabase) QueryAllAfter(cutoff time.Time) ([]Row, error) { return db.rows[start:], nil } -func (db *dumbDatabase) StoreServiceStatusBatch(entries scanner.ServiceStatusBatch, when time.Time) error { +func (db *dumbDatabase) StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) { + non_duplicates := []ServiceStatus{} + db.mutex.Lock() defer db.mutex.Unlock() @@ -235,7 +238,7 @@ func (db *dumbDatabase) StoreServiceStatusBatch(entries scanner.ServiceStatusBat db.rows = append(db.rows, new_row) } } - return nil + return non_duplicates, nil } const max_rows = 10000 @@ -247,7 +250,7 @@ func (db *dumbDatabase) StoreSelfStatus(status EventType, when time.Time) error if status != EventSelfUp && status != EventSelfDown { log.Panicf("(StoreSelfStatus) invalid value: %v", status) } - var placeholder scanner.ServiceStatus + var placeholder ServiceStatus new_row := Row{when, EventType(status), placeholder} db.rows = append(db.rows, new_row) if len(db.rows) > max_rows { diff --git a/core/matrix.go b/core/matrix.go new file mode 100644 index 0000000..2c236e9 --- /dev/null +++ b/core/matrix.go @@ -0,0 +1,61 @@ +package core + +import ( + "fmt" + "strings" + + m "maunium.net/go/mautrix" + . "maunium.net/go/mautrix/id" +) + +type MatrixConfig struct { + UserId string + // The AccessToken can be got by logging in with any Matrix client. + // In element.io, find it under Settings -> About & Help -> Advanced. + AccessToken string + RoomId string +} + +type MatrixClient struct { + inner m.Client + config MatrixConfig + // Resolved/opaque Room Id + room_id RoomID +} + +func NewMatrixClient(config MatrixConfig) (*MatrixClient, error) { + if config.AccessToken == "" { + return nil, fmt.Errorf("no access token") + } + + user_id := UserID(config.UserId) + client, err := m.NewClient(user_id.Homeserver(), user_id, config.AccessToken) + if err != nil { + return nil, err + } + + var opaque_room_id RoomID + if strings.HasPrefix(config.RoomId, "!") { + opaque_room_id = RoomID(config.RoomId) + } else { + resp, err := client.ResolveAlias(RoomAlias(config.RoomId)) + if err != nil { + return nil, err + } + opaque_room_id = resp.RoomID + } + + return &MatrixClient{ + *client, + config, + opaque_room_id, + }, nil +} + +func (client *MatrixClient) SendNotice(message string) (*m.RespSendEvent, error) { + return client.inner.SendNotice(client.room_id, message) +} + +func (client *MatrixClient) RedactEvent(eventId EventID, reason string) (*m.RespSendEvent, error) { + return client.inner.RedactEvent(client.room_id, eventId, m.ReqRedact{Reason: reason}) +} diff --git a/main.go b/main.go index 36151ff..10d9d7d 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,8 @@ package main import ( "context" "embed" + "errors" + "fmt" "io/fs" "log" "net" @@ -59,9 +61,18 @@ func rootPage(w http.ResponseWriter, r *http.Request) { // get service status' timeout := time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() data := scanner.CheckServiceBatch(ctx, config.Service) - dbm.StoreServiceStatusBatch(data, time.Now()) - cancel() + non_duplicates, err := dbm.StoreServiceStatusBatch(data, time.Now()) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + err = notify(non_duplicates) + if err != nil { + http.Error(w, err.Error(), 500) + return + } // don't uncomment this in production. verbose // log.Printf("%v", data) @@ -69,20 +80,15 @@ func rootPage(w http.ResponseWriter, r *http.Request) { parsedTemplate, err := parseAndRenderTemplate("templates/index.html", data) if err != nil { http.Error(w, err.Error(), 500) + return } w.Write([]byte(parsedTemplate)) } -func removeFile(path string) { - err := os.Remove(path) - if err != nil && !os.IsNotExist(err) { - log.Panic(err) - } -} - var config *core.Config var dbm core.Database +var client *core.MatrixClient func cleanup() { log.Println("Cleaning up") @@ -102,6 +108,11 @@ func main() { log.Panic(err) } + client, err = core.NewMatrixClient(config.Matrix) + if err != nil { + log.Panic(err) + } + http.HandleFunc("/", rootPage) // serve public folder from embedfs @@ -151,7 +162,7 @@ func main() { } // log startup and register cleanup (on SIGTERM or panic) - ch_sigterm := make(chan os.Signal) + ch_sigterm := make(chan os.Signal, 1) os_signal.Notify(ch_sigterm, syscall.SIGTERM, syscall.SIGINT) log.Print("Starting up") @@ -173,7 +184,11 @@ func main() { go func() { for { - routineCheck(5 * time.Minute) + err := routineCheck(5 * time.Minute) + if err != nil { + log.Printf("Error in routineCheck: %v", err) + // idea: maybe should delay here + } } }() @@ -185,12 +200,37 @@ func main() { } } -func routineCheck(timeout time.Duration) { +func routineCheck(timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - + data := scanner.CheckServiceBatch(ctx, config.Service) - dbm.StoreServiceStatusBatch(data, time.Now()) + non_duplicates, err := dbm.StoreServiceStatusBatch(data, time.Now()) + if err != nil { + return err + } + err = notify(non_duplicates) + if err != nil { + return err + } // log.Printf("Routine check: %v", data) <-ctx.Done() + + return nil +} + +func notify(updates []scanner.ServiceStatus) error { + final_err := error(nil) + for _, v := range updates { + statusString := "???" + if v.Ok { + statusString = "up" + } else { + statusString = "down" + + } + _, err := client.SendNotice(fmt.Sprintf("%s is %s!", v.Name, statusString)) + final_err = errors.Join(final_err, err) + } + return final_err } diff --git a/util.go b/util.go new file mode 100644 index 0000000..2a50f5a --- /dev/null +++ b/util.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + "os" +) + +func removeFile(path string) { + err := os.Remove(path) + if err != nil && !os.IsNotExist(err) { + log.Panic(err) + } +}