diff --git a/Dockerfile b/Dockerfile
index 603ab48..e55f158 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,6 +17,7 @@ FROM scratch
WORKDIR /app
COPY --from=build /frigate-notify /app/frigate-notify
+COPY /templates /app/templates
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
diff --git a/config/config.go b/config/config.go
index 0f92b28..824d5cd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,12 +1,15 @@
package config
import (
+ "encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
+ "github.com/0x2142/frigate-notify/models"
+ "github.com/0x2142/frigate-notify/util"
"github.com/kkyr/fig"
)
@@ -17,11 +20,13 @@ type Config struct {
}
type Frigate struct {
- Server string `fig:"server" validate:"required"`
- Insecure bool `fig:"ignoressl" default:false`
- WebAPI WebAPI `fig:"webapi"`
- MQTT MQTT `fig:"mqtt"`
- Cameras Cameras `fig:"cameras"`
+ Server string `fig:"server" validate:"required"`
+ Insecure bool `fig:"ignoressl" default:false`
+ PublicURL string `fig:"public_url" default:""`
+ Headers []map[string]string `fig:"headers"`
+ WebAPI WebAPI `fig:"webapi"`
+ MQTT MQTT `fig:"mqtt"`
+ Cameras Cameras `fig:"cameras"`
}
type WebAPI struct {
@@ -53,6 +58,7 @@ type Alerts struct {
SMTP SMTP `fig:"smtp"`
Telegram Telegram `fig:"telegram"`
Pushover Pushover `fig:"pushover"`
+ Nfty Nfty `fig:"nfty"`
}
type General struct {
@@ -110,6 +116,13 @@ type Pushover struct {
TTL int `fig:"ttl" default:0`
}
+type Nfty struct {
+ Enabled bool `fig:"enabled" default:false`
+ Server string `fig:"server" default:""`
+ Topic string `fig:"topic" default:""`
+ Insecure bool `fig:"ignoressl" default:false`
+}
+
type Monitor struct {
Enabled bool `fig:"enabled" default:false`
URL string `fig:"url" default:""`
@@ -137,11 +150,10 @@ func LoadConfig(configFile string) {
if err != nil {
log.Fatal("Failed to load config file! Error: ", err)
}
+ log.Print("Config file loaded.")
// Send config file to validation before completing
validateConfig()
-
- log.Print("Config file loaded.")
}
// validateConfig checks config file structure & loads info into associated packages
@@ -153,6 +165,11 @@ func validateConfig() {
configErrors = append(configErrors, "Please configure only one polling method: Frigate Web API or MQTT")
}
+ // Set default web API interval if not specified
+ if ConfigData.Frigate.WebAPI.Enabled && ConfigData.Frigate.WebAPI.Interval == 0 {
+ ConfigData.Frigate.WebAPI.Interval = 30
+ }
+
// Warn on test mode being enabled
if ConfigData.Frigate.WebAPI.Enabled && ConfigData.Frigate.WebAPI.TestMode {
log.Print("~~~~~~~~~~~~~~~~~~~")
@@ -168,6 +185,27 @@ func validateConfig() {
ConfigData.Frigate.Server = fmt.Sprintf("http://%s", ConfigData.Frigate.Server)
}
+ // Test connectivity to Frigate
+ log.Print("Checking connection to Frigate server...")
+ statsAPI := fmt.Sprintf("%s/api/stats", ConfigData.Frigate.Server)
+ response, err := util.HTTPGet(statsAPI, ConfigData.Frigate.Insecure)
+ if err != nil {
+ log.Fatalf("Cannot reach Frigate server at %v, error: %v", ConfigData.Frigate.Server, err)
+ }
+ var stats models.FrigateStats
+ json.Unmarshal([]byte(response), &stats)
+ log.Printf("Successfully connected to %v", ConfigData.Frigate.Server)
+ if stats.Service.Version != "" {
+ log.Printf("Frigate server is running version %v", stats.Service.Version)
+ }
+
+ // Check Public / External URL if set
+ if ConfigData.Frigate.PublicURL != "" {
+ if !strings.Contains(ConfigData.Frigate.PublicURL, "http://") && !strings.Contains(ConfigData.Frigate.PublicURL, "https://") {
+ configErrors = append(configErrors, "Public URL must include http:// or https://")
+ }
+ }
+
// Check for camera exclusions
if len(ConfigData.Frigate.Cameras.Exclude) > 0 {
log.Println("Cameras to exclude from alerting:")
@@ -301,6 +339,16 @@ func validateConfig() {
configErrors = append(configErrors, "Pushover TTL cannot be negative!")
}
}
+ if ConfigData.Alerts.Nfty.Enabled {
+ log.Print("Nfty alerting enabled.")
+ if ConfigData.Alerts.Nfty.Server == "" {
+ configErrors = append(configErrors, "No Nfty server specified!")
+ }
+ if ConfigData.Alerts.Nfty.Topic == "" {
+ configErrors = append(configErrors, "No Nfty topic specified!")
+ }
+
+ }
// Validate monitoring config
if ConfigData.Monitor.Enabled {
diff --git a/docs/changelog.md b/docs/changelog.md
index ac46ca5..39f8558 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,16 @@
# Changelog
+## [v0.2.8](https://github.com/0x2142/frigate-notify/releases/tag/v0.2.8) - May 15 2024
+
+- 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
+- Add new `public_url` config item for Frigate
+ - This will be used in notification links & should be configured if Frigate is accessible via the internet
+- Add startup check to verify Frigate API is accessible
+- Rework event notifications to be built from templates
+- Fix default interval for querying evens via web API
+- Fix issue where label score is 0% via web API event query
+
## [v0.2.7](https://github.com/0x2142/frigate-notify/releases/tag/v0.2.7) - May 06 2024
- Allow changing default MQTT topic prefix via config
diff --git a/docs/config.md b/docs/config.md
index f136f9b..9441eb8 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -9,14 +9,28 @@ Configuration snippets will be provided throughout this page. Feel free to copy
### Server
- **server** (Required)
- - IP or hostname of the Frigate NVR
+ - IP, hostname, or URL of the Frigate NVR
+ - If IP or hostname specified, app will prepend `http://`
+ - If Frigate is not behind a reverse proxy, append port number if necessary
- **ignoressl** (Optional - Default: `false`)
- - Set to `true` to allow self-signed certificates
+ - Set to `true` to allow self-signed certificates for `server`
+- **public_url** (Optional)
+ - Should be set if Frigate is available via an external, public URL
+ - This value is used for the links used in notifications
+ - Format should be full URL (example: `https://nvr.your.public.domain.tld`)
+- **headers** (Optional)
+ - Send additional HTTP headers to Frigate
+ - Useful for things like authentication
+ - Header format: `Header: Value`
+ - Example: `Authorization: Basic abcd1234`
```yaml title="Config File Snippet"
frigate:
server: nvr.your.domain.tld
ignoressl: true
+ public_url: https://nvr.your.public.domain.tld
+ headers:
+ - Authorization: Basic abcd1234
```
### WebAPI
@@ -27,7 +41,7 @@ frigate:
- **enabled** (Optional - Default: `false`)
- If set to `true`, Frigate events are collected by polling the web API
- **interval** (Optional - Default: `30`)
- - How frequently to check the Frigate web API for new events, in seconds
+ - How frequently to check the Frigate web API for new events, in seconds
```yaml title="Config File Snippet"
frigate:
@@ -324,6 +338,28 @@ alerts:
ttl:
```
+### Nfty
+
+- **enabled** (Optional - Default: `false`)
+ - Set to `true` to enable alerting via Nfty
+- **server** (Required)
+ - Full URL of the desired Nfty server
+ - Required if this alerting method is enabled
+- **topic** (Required)
+ - Destination topic that will receive alert notifications
+ - Required if this alerting method is enabled
+- **ignoressl** (Optional - Default: `false`)
+ - Set to `true` to allow self-signed certificates
+
+```yaml title="Config File Snippet"
+alerts:
+ nfty:
+ enabled: true
+ server: https://nfty.your.domain.tld
+ topic: frigate
+ ignoressl: true
+```
+
## Monitor
If enabled, this application will check in with tools like [HealthChecks](https://github.com/healthchecks/healthchecks) or [Uptime Kuma](https://github.com/louislam/uptime-kuma) on a regular interval for health / status monitoring.
@@ -347,10 +383,8 @@ monitor:
ignoressl:
```
-
---
-
## Sample Config { data-search-exclude }
A full config file template has been provided below:
@@ -358,7 +392,9 @@ A full config file template has been provided below:
```yaml
frigate:
server:
- ignoressl:
+ ignoressl:
+ public_url:
+ headers:
webapi:
enabled:
@@ -431,9 +467,15 @@ alerts:
expire:
ttl:
+ nfty:
+ enabled: false
+ server:
+ topic:
+ ignoressl:
+
monitor:
enabled: false
url:
interval:
ignoressl:
-```
\ No newline at end of file
+```
diff --git a/events/api.go b/events/api.go
index 2475066..41be195 100644
--- a/events/api.go
+++ b/events/api.go
@@ -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"
@@ -35,12 +36,13 @@ func CheckForEvents() {
log.Println("Checking for new events...")
// Query events
- response, err := util.HTTPGet(url, config.ConfigData.Frigate.Insecure)
+ response, err := util.HTTPGet(url, config.ConfigData.Frigate.Insecure, config.ConfigData.Frigate.Headers...)
if err != nil {
log.Printf("Cannot get events from %s", url)
+ log.Printf("Error received: %s", err)
}
- var events []Event
+ var events []models.Event
json.Unmarshal([]byte(response), &events)
@@ -77,17 +79,15 @@ 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)
}
}
// GetSnapshot downloads a snapshot from Frigate
func GetSnapshot(snapshotURL, eventID string) io.Reader {
- response, err := util.HTTPGet(snapshotURL, config.ConfigData.Frigate.Insecure)
+ response, err := util.HTTPGet(snapshotURL, config.ConfigData.Frigate.Insecure, config.ConfigData.Frigate.Headers...)
if err != nil {
log.Println("Could not access snaphot. Error: ", err)
}
diff --git a/events/events.go b/events/events.go
index fdd0e83..d4dd276 100644
--- a/events/events.go
+++ b/events/events.go
@@ -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
diff --git a/events/mqtt.go b/events/mqtt.go
index 73dcfc1..8e0d7ca 100644
--- a/events/mqtt.go
+++ b/events/mqtt.go
@@ -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
@@ -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" {
@@ -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)
}
}
diff --git a/example-config.yml b/example-config.yml
index 1624322..d346f89 100644
--- a/example-config.yml
+++ b/example-config.yml
@@ -9,6 +9,13 @@ frigate:
server:
# Set to true if using SSL & a self-signed certificate
ignoressl: false
+ # Public / internet-facing Frigate URL, if different from above server address
+ public_url:
+
+ # List of HTTP headers to send to Frigate, in format Header: Value
+ headers:
+ # Example:
+ # - Authorization: Basic abcd1234
webapi:
# Set to true to enable event collection via the web API
@@ -127,6 +134,16 @@ alerts:
# Optional message lifetime
ttl:
+ # Nfty Config
+ nfty:
+ # Set to true to enable alerting via
+ enabled: false
+ # URL of Nfty server
+ server:
+ # Nfty topic for notifications
+ topic:
+ # Set to true if using SSL & a self-signed certificate
+ ignoressl:
## App Monitoring
# Sends HTTP GET to provided URL for aliveness checks
diff --git a/go.mod b/go.mod
index 4b12184..bee8686 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,6 @@ require (
github.com/disgoorg/disgo v0.17.2
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
- github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
github.com/gregdel/pushover v1.3.0
github.com/kkyr/fig v0.4.0
github.com/wneessen/go-mail v0.4.1
diff --git a/go.sum b/go.sum
index 2eb5963..46826c8 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,6 @@ github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQ
github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
-github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
-github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gregdel/pushover v1.3.0 h1:CewbxqsThoN/1imgwkDKFkRkltaQMoyBV0K9IquQLtw=
diff --git a/main.go b/main.go
index 7abc1f9..dce78f1 100644
--- a/main.go
+++ b/main.go
@@ -13,7 +13,7 @@ import (
"github.com/0x2142/frigate-notify/util"
)
-var APP_VER = "v0.2.7"
+var APP_VER = "v0.2.8"
func main() {
log.Println("Frigate Notify -", APP_VER)
diff --git a/models/event.go b/models/event.go
new file mode 100644
index 0000000..e79315c
--- /dev/null
+++ b/models/event.go
@@ -0,0 +1,54 @@
+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"`
+ Data struct {
+ Attributes []interface{} `json:"attributes"`
+ Box []float64 `json:"box"`
+ Region []float64 `json:"region"`
+ Score float64 `json:"score"`
+ TopScore float64 `json:"top_score"`
+ Type string `json:"type"`
+ } `json:"data"`
+ 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
+ ZoneList string
+ LocalURL string
+ PublicURL string
+}
diff --git a/models/stats.go b/models/stats.go
new file mode 100644
index 0000000..a87aad1
--- /dev/null
+++ b/models/stats.go
@@ -0,0 +1,10 @@
+package models
+
+type FrigateStats struct {
+ Service struct {
+ LastUpdated int `json:"last_updated"`
+ LatestVersion string `json:"latest_version"`
+ Uptime int `json:"uptime"`
+ Version string `json:"version"`
+ } `json:"service"`
+}
diff --git a/notifier/alerts.go b/notifier/alerts.go
index e73d8cb..834e103 100644
--- a/notifier/alerts.go
+++ b/notifier/alerts.go
@@ -2,31 +2,83 @@ package notifier
import (
"bytes"
+ "fmt"
"io"
+ "strings"
+ "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(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
+ event.Extra.PublicURL = config.ConfigData.Frigate.PublicURL
+
+ // MQTT uses CurrentZones, Web API uses Zones
+ // Combine into one object to use regardless of connection method
+ event.Zones = append(event.Zones, event.CurrentZones...)
+ // Join zones into plain comma-separated string
+ event.Extra.ZoneList = strings.Join(event.Zones, ", ")
+
+ // 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)
+ }
+
+ // For Web API query, top-level top_score value is no longer used
+ // So need to replace it with data.top_score value
+ if event.TopScore == 0 {
+ event.TopScore = event.Data.TopScore
+ }
+ // 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()
+
}
diff --git a/notifier/discord.go b/notifier/discord.go
index ae2dde3..1c00413 100644
--- a/notifier/discord.go
+++ b/notifier/discord.go
@@ -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 {
diff --git a/notifier/gotify.go b/notifier/gotify.go
index 9534c82..ab078f2 100644
--- a/notifier/gotify.go
+++ b/notifier/gotify.go
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/0x2142/frigate-notify/config"
+ "github.com/0x2142/frigate-notify/models"
"github.com/0x2142/frigate-notify/util"
)
@@ -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 {
@@ -49,13 +53,14 @@ func SendGotifyPush(message, snapshotURL string, eventid string) {
data, err := json.Marshal(payload)
if err != nil {
- log.Println("Event ID %v - Unable to build Gotify payload: ", eventid, err)
+ log.Printf("Event ID %v - Unable to build Gotify payload: %v", eventid, err)
return
}
gotifyURL := fmt.Sprintf("%s/message?token=%s&", config.ConfigData.Alerts.Gotify.Server, config.ConfigData.Alerts.Gotify.Token)
- response, err := util.HTTPPost(gotifyURL, config.ConfigData.Alerts.Gotify.Insecure, data)
+ header := map[string]string{"Content-Type": "application/json"}
+ response, err := util.HTTPPost(gotifyURL, config.ConfigData.Alerts.Gotify.Insecure, data, header)
if err != nil {
log.Print("Failed to send Gotify notification: ", err)
return
diff --git a/notifier/nfty.go b/notifier/nfty.go
new file mode 100644
index 0000000..d64c329
--- /dev/null
+++ b/notifier/nfty.go
@@ -0,0 +1,49 @@
+package notifier
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "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(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
+ var headers []map[string]string
+ headers = append(headers, map[string]string{"Content-Type": "text/markdown"})
+ headers = append(headers, map[string]string{"X-Title": config.ConfigData.Alerts.General.Title})
+
+ // Set action link to the recorded clip
+ clip := fmt.Sprintf("%s/api/events/%s/clip.mp4", config.ConfigData.Frigate.Server, eventid)
+ headers = append(headers, map[string]string{"X-Actions": "view, View Clip, " + clip + ", clear=true"})
+
+ var attachment []byte
+ if snapshot != nil {
+ headers = append(headers, map[string]string{"X-Filename": "snapshot.jpg"})
+ attachment, _ = io.ReadAll(snapshot)
+ } else {
+ message += "\n\nNo snapshot saved."
+ }
+
+ // Escape newlines in message
+ message = strings.ReplaceAll(message, "\n", "\\n")
+ headers = append(headers, map[string]string{"X-Message": message})
+
+ _, err := util.HTTPPost(NftyURL, config.ConfigData.Alerts.Nfty.Insecure, attachment, headers...)
+ if err != nil {
+ log.Print("Failed to send Nfty notification: ", err)
+ return
+ }
+
+ log.Printf("Event ID %v - Nfty alert sent", eventid)
+}
diff --git a/notifier/pushover.go b/notifier/pushover.go
index bd367d8..21b98e9 100644
--- a/notifier/pushover.go
+++ b/notifier/pushover.go
@@ -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, "
", "")
+
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,
@@ -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
}
}
diff --git a/notifier/smtp.go b/notifier/smtp.go
index 70ecb9e..42db235 100644
--- a/notifier/smtp.go
+++ b/notifier/smtp.go
@@ -7,12 +7,15 @@ import (
"time"
"github.com/0x2142/frigate-notify/config"
- "github.com/gomarkdown/markdown"
+ "github.com/0x2142/frigate-notify/models"
"github.com/wneessen/go-mail"
)
// SendSMTP forwards alert data via email
-func SendSMTP(message string, snapshot io.Reader, eventid string) {
+func SendSMTP(event models.Event, snapshot io.Reader, eventid string) {
+ // Build notification
+ message := renderMessage("html", event)
+
// Set up email alert
m := mail.NewMsg()
m.From(config.ConfigData.Alerts.SMTP.User)
@@ -24,9 +27,9 @@ func SendSMTP(message string, snapshot io.Reader, eventid string) {
} else {
message += "\n\nNo snapshot saved."
}
+
// Convert message body to HTML
- htmlMessage := markdown.ToHTML([]byte(message), nil, nil)
- m.SetBodyString(mail.TypeTextHTML, string(htmlMessage))
+ m.SetBodyString(mail.TypeTextHTML, message)
time.Sleep(5 * time.Second)
@@ -44,12 +47,12 @@ func SendSMTP(message string, snapshot io.Reader, eventid string) {
}
if err != nil {
- log.Print("Event ID %v - Failed to connect to SMTP Server: ", eventid, err)
+ log.Printf("Event ID %v - Failed to connect to SMTP Server: %v", eventid, err)
}
// Send message
if err := c.DialAndSend(m); err != nil {
- log.Print("Event ID %v - Failed to send SMTP message: ", eventid, err)
+ log.Printf("Event ID %v - Failed to send SMTP message: %v", eventid, err)
return
}
log.Printf("Event ID %v - SMTP alert sent", eventid)
diff --git a/notifier/telegram.go b/notifier/telegram.go
index d836d4e..8e7dc27 100644
--- a/notifier/telegram.go
+++ b/notifier/telegram.go
@@ -6,40 +6,38 @@ import (
"strings"
"github.com/0x2142/frigate-notify/config"
+ "github.com/0x2142/frigate-notify/models"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
- "github.com/gomarkdown/markdown"
)
// SendTelegramMessage sends alert through Telegram to individual users
-func SendTelegramMessage(message string, snapshot io.Reader, eventid string) {
+func SendTelegramMessage(event models.Event, snapshot io.Reader, eventid string) {
+ // Build notification
+ message := renderMessage("html", event)
+ message = strings.ReplaceAll(message, "
", "")
+
bot, err := tgbotapi.NewBotAPI(config.ConfigData.Alerts.Telegram.Token)
if err != nil {
- log.Print("Event ID %v - Failed to connect to Telegram:", eventid, err)
+ log.Printf("Event ID %v - Failed to connect to Telegram: %v", eventid, err)
return
}
- // Convert message to HTML & remove tags not permitted by Telegram
- htmlMessage := string(markdown.ToHTML([]byte(message), nil, nil))
- htmlMessage = strings.Replace(htmlMessage, "
", "", -1) - htmlMessage = strings.Replace(htmlMessage, "
", "", -1) - htmlMessage = strings.Replace(htmlMessage, "