Skip to content

Commit

Permalink
Merge pull request #59 from 0x2142/notif-templates
Browse files Browse the repository at this point in the history
Notif templates
  • Loading branch information
0x2142 authored May 15, 2024
2 parents 681b3c5 + 9ef85ec commit fc982dc
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 115 deletions.
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Add support for notifications via [Nfty](https://frigate-notify.0x2142.com/config/#nfty)
- Add ability to send additional HTTP [headers](https://frigate-notify.0x2142.com/config/#frigate) to Frigate
- Rework event notifications to be built from templates

## [v0.2.7](https://github.com/0x2142/frigate-notify/releases/tag/v0.2.7) - May 06 2024

Expand Down
7 changes: 3 additions & 4 deletions events/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/models"
"github.com/0x2142/frigate-notify/notifier"
"github.com/0x2142/frigate-notify/util"
"golang.org/x/exp/slices"
Expand Down Expand Up @@ -41,7 +42,7 @@ func CheckForEvents() {
log.Printf("Error received: %s", err)
}

var events []Event
var events []models.Event

json.Unmarshal([]byte(response), &events)

Expand Down Expand Up @@ -78,10 +79,8 @@ func CheckForEvents() {
snapshot = GetSnapshot(snapshotURL, event.ID)
}

message := buildMessage(eventTime, event)

// Send alert with snapshot
notifier.SendAlert(message, snapshotURL, snapshot, event.ID)
notifier.SendAlert(event, snapshotURL, snapshot, event.ID)
}

}
Expand Down
57 changes: 0 additions & 57 deletions events/events.go
Original file line number Diff line number Diff line change
@@ -1,70 +1,13 @@
package frigate

import (
"fmt"
"log"
"slices"
"strings"
"time"

"github.com/0x2142/frigate-notify/config"
)

// Event stores Frigate alert attributes
type Event struct {
Area interface{} `json:"area"`
Box interface{} `json:"box"`
Camera string `json:"camera"`
EndTime interface{} `json:"end_time"`
FalsePositive interface{} `json:"false_positive"`
HasClip bool `json:"has_clip"`
HasSnapshot bool `json:"has_snapshot"`
ID string `json:"id"`
Label string `json:"label"`
PlusID interface{} `json:"plus_id"`
Ratio interface{} `json:"ratio"`
Region interface{} `json:"region"`
RetainIndefinitely bool `json:"retain_indefinitely"`
StartTime float64 `json:"start_time"`
SubLabel interface{} `json:"sub_label"`
Thumbnail string `json:"thumbnail"`
TopScore float64 `json:"top_score"`
Zones []string `json:"zones"`
CurrentZones []string `json:"current_zones"`
EnteredZones []string `json:"entered_zones"`
}

// buildMessage constructs message payload for all alerting methods
func buildMessage(time time.Time, event Event) string {
// If certain time format is provided, re-format date / time string
timestr := time.String()
if config.ConfigData.Alerts.General.TimeFormat != "" {
timestr = time.Format(config.ConfigData.Alerts.General.TimeFormat)
}
// Build alert message payload, include two spaces at end to force markdown newline
message := fmt.Sprintf("Detection at %v ", timestr)
message += fmt.Sprintf("\nCamera: %s ", event.Camera)
// Attach detection label & caculate score percentage
message += fmt.Sprintf("\nLabel: %v (%v%%) ", event.Label, int((event.TopScore * 100)))
// If zones configured / detected, include details
var zones []string
zones = append(zones, event.Zones...)
zones = append(zones, event.CurrentZones...)
if len(zones) >= 1 {
message += fmt.Sprintf("\nZone(s): %v ", strings.Join(zones, ", "))
}
// Append link to camera
message += "\n\nLinks: "
message += fmt.Sprintf("[Camera](%s/cameras/%s)", config.ConfigData.Frigate.Server, event.Camera)
// If event has a recorded clip, include a link to that as well
if event.HasClip {
message += " | "
message += fmt.Sprintf("[Event Clip](%s/api/events/%s/clip.mp4) ", config.ConfigData.Frigate.Server, event.ID)
}

return message
}

// isAllowedZone verifies whether a zone should be allowed to generate a notification
func isAllowedZone(id string, zones []string) bool {
// By default, send events without a zone unless specified otherwise
Expand Down
18 changes: 3 additions & 15 deletions events/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,12 @@ import (
"time"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/models"
"github.com/0x2142/frigate-notify/notifier"
mqtt "github.com/eclipse/paho.mqtt.golang"
"golang.org/x/exp/slices"
)

// MQTTEvent stores incoming MQTT payloads from Frigate
type MQTTEvent struct {
Before struct {
Event
} `json:"before,omitempty"`
After struct {
Event
} `json:"after,omitempty"`
Type string `json:"type"`
}

// SubscribeMQTT establishes subscription to MQTT server & listens for messages
func SubscribeMQTT() {
// MQTT client configuration
Expand Down Expand Up @@ -61,7 +51,7 @@ func SubscribeMQTT() {
// processEvent handles incoming MQTT messages & pulls out relevant info for alerting
func processEvent(client mqtt.Client, msg mqtt.Message) {
// Parse incoming MQTT message
var event MQTTEvent
var event models.MQTTEvent
json.Unmarshal(msg.Payload(), &event)

if event.Type == "new" || event.Type == "update" {
Expand Down Expand Up @@ -109,10 +99,8 @@ func processEvent(client mqtt.Client, msg mqtt.Message) {
snapshot = GetSnapshot(snapshotURL, event.After.ID)
}

message := buildMessage(eventTime, event.After.Event)

// Send alert with snapshot
notifier.SendAlert(message, snapshotURL, snapshot, event.After.ID)
notifier.SendAlert(event.After.Event, snapshotURL, snapshot, event.After.ID)
}
}

Expand Down
44 changes: 44 additions & 0 deletions models/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package models

// MQTTEvent stores incoming MQTT payloads from Frigate
type MQTTEvent struct {
Before struct {
Event
} `json:"before,omitempty"`
After struct {
Event
} `json:"after,omitempty"`
Type string `json:"type"`
}

// Event stores Frigate alert attributes
type Event struct {
Area interface{} `json:"area"`
Box interface{} `json:"box"`
Camera string `json:"camera"`
EndTime interface{} `json:"end_time"`
FalsePositive interface{} `json:"false_positive"`
HasClip bool `json:"has_clip"`
HasSnapshot bool `json:"has_snapshot"`
ID string `json:"id"`
Label string `json:"label"`
PlusID interface{} `json:"plus_id"`
Ratio interface{} `json:"ratio"`
Region interface{} `json:"region"`
RetainIndefinitely bool `json:"retain_indefinitely"`
StartTime float64 `json:"start_time"`
SubLabel interface{} `json:"sub_label"`
Thumbnail string `json:"thumbnail"`
TopScore float64 `json:"top_score"`
Zones []string `json:"zones"`
CurrentZones []string `json:"current_zones"`
EnteredZones []string `json:"entered_zones"`
Extra ExtraFields
}

// Additional custom fields
type ExtraFields struct {
FormattedTime string
TopScorePercent string
LocalURL string
}
49 changes: 42 additions & 7 deletions notifier/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,69 @@ package notifier

import (
"bytes"
"fmt"
"io"
"text/template"
"time"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/models"
)

// SendAlert forwards alert information to all enabled alerting methods
func SendAlert(message, snapshotURL string, snapshot io.Reader, eventid string) {
func SendAlert(event models.Event, snapshotURL string, snapshot io.Reader, eventid string) {
// Create copy of snapshot for each alerting method
var snap []byte
if snapshot != nil {
snap, _ = io.ReadAll(snapshot)
}
if config.ConfigData.Alerts.Discord.Enabled {
SendDiscordMessage(message, bytes.NewReader(snap), eventid)
SendDiscordMessage(event, bytes.NewReader(snap), eventid)
}
if config.ConfigData.Alerts.Gotify.Enabled {
SendGotifyPush(message, snapshotURL, eventid)
SendGotifyPush(event, snapshotURL, eventid)
}
if config.ConfigData.Alerts.SMTP.Enabled {
SendSMTP(message, bytes.NewReader(snap), eventid)
SendSMTP(event, bytes.NewReader(snap), eventid)
}
if config.ConfigData.Alerts.Telegram.Enabled {
SendTelegramMessage(message, bytes.NewReader(snap), eventid)
SendTelegramMessage(event, bytes.NewReader(snap), eventid)
}
if config.ConfigData.Alerts.Pushover.Enabled {
SendPushoverMessage(message, bytes.NewReader(snap), eventid)
SendPushoverMessage(event, bytes.NewReader(snap), eventid)
}
if config.ConfigData.Alerts.Nfty.Enabled {
SendNftyPush(message, bytes.NewReader(snap), eventid)
SendNftyPush(event, bytes.NewReader(snap), eventid)
}
}

// Build notification based on template
func renderMessage(sourceTemplate string, event models.Event) string {
// Assign Frigate URL to extra event fields
event.Extra.LocalURL = config.ConfigData.Frigate.Server

// If certain time format is provided, re-format date / time string
eventTime := time.Unix(int64(event.StartTime), 0)
event.Extra.FormattedTime = eventTime.String()
if config.ConfigData.Alerts.General.TimeFormat != "" {
event.Extra.FormattedTime = eventTime.Format(config.ConfigData.Alerts.General.TimeFormat)
}
// Calc TopScore percentage
event.Extra.TopScorePercent = fmt.Sprintf("%v%%", int((event.TopScore * 100)))

// Render template
var tmpl *template.Template
if sourceTemplate == "markdown" || sourceTemplate == "plaintext" || sourceTemplate == "html" {
var templateFile = "./templates/" + sourceTemplate + ".template"
tmpl = template.Must(template.ParseFiles(templateFile))
}

var renderedTemplate bytes.Buffer
err := tmpl.Execute(&renderedTemplate, event)
if err != nil {
panic(err)
}

return renderedTemplate.String()

}
6 changes: 5 additions & 1 deletion notifier/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import (
"log"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/models"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/webhook"
)

// SendDiscordMessage pushes alert message to Discord via webhook
func SendDiscordMessage(message string, snapshot io.Reader, eventid string) {
func SendDiscordMessage(event models.Event, snapshot io.Reader, eventid string) {
var err error

// Build notification
message := renderMessage("markdown", event)

// Connect to Discord
client, err := webhook.NewWithURL(config.ConfigData.Alerts.Discord.Webhook)
if err != nil {
Expand Down
6 changes: 5 additions & 1 deletion notifier/gotify.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/models"
"github.com/0x2142/frigate-notify/util"
)

Expand All @@ -33,7 +34,10 @@ type gotifyPayload struct {
}

// SendGotifyPush forwards alert messages to Gotify push notification server
func SendGotifyPush(message, snapshotURL string, eventid string) {
func SendGotifyPush(event models.Event, snapshotURL string, eventid string) {
// Build notification
message := renderMessage("markdown", event)

if snapshotURL != "" {
message += fmt.Sprintf("\n\n![](%s)", snapshotURL)
} else {
Expand Down
6 changes: 5 additions & 1 deletion notifier/nfty.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import (
"strings"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/models"
"github.com/0x2142/frigate-notify/util"
)

// SendNftyPush forwards alert messages to Nfty server
func SendNftyPush(message string, snapshot io.Reader, eventid string) {
func SendNftyPush(event models.Event, snapshot io.Reader, eventid string) {
// Build notification
message := renderMessage("plaintext", event)

NftyURL := fmt.Sprintf("%s/%s", config.ConfigData.Alerts.Nfty.Server, config.ConfigData.Alerts.Nfty.Topic)

// Set headers
Expand Down
18 changes: 9 additions & 9 deletions notifier/pushover.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ import (
"time"

"github.com/0x2142/frigate-notify/config"
"github.com/gomarkdown/markdown"
"github.com/0x2142/frigate-notify/models"
"github.com/gregdel/pushover"
)

// SendPushoverMessage sends alert message through Pushover service
func SendPushoverMessage(message string, snapshot io.Reader, eventid string) {
func SendPushoverMessage(event models.Event, snapshot io.Reader, eventid string) {
// Build notification
message := renderMessage("html", event)
message = strings.ReplaceAll(message, "<br />", "")

push := pushover.New(config.ConfigData.Alerts.Pushover.Token)
recipient := pushover.NewRecipient(config.ConfigData.Alerts.Pushover.Userkey)

// Convert message to HTML & strip newline characters
htmlMessage := string(markdown.ToHTML([]byte(message), nil, nil))
htmlMessage = strings.Replace(htmlMessage, "\n", "", -1)

// Create new message
notif := &pushover.Message{
Message: htmlMessage,
Message: message,
Title: config.ConfigData.Alerts.General.Title,
Priority: config.ConfigData.Alerts.Pushover.Priority,
HTML: true,
Expand All @@ -47,12 +47,12 @@ func SendPushoverMessage(message string, snapshot io.Reader, eventid string) {
if snapshot != nil {
notif.AddAttachment(snapshot)
if _, err := push.SendMessage(notif, recipient); err != nil {
log.Print("Event ID %v - Error sending Pushover notification:", eventid, err)
log.Printf("Event ID %v - Error sending Pushover notification: %v", eventid, err)
return
}
} else {
if _, err := push.SendMessage(notif, recipient); err != nil {
log.Print("Event ID %v - Error sending Pushover notification:", eventid, err)
log.Printf("Event ID %v - Error sending Pushover notification: %v", eventid, err)
return
}
}
Expand Down
Loading

0 comments on commit fc982dc

Please sign in to comment.