From 7fd57101a3e3d5622212c936c772e221e822ba15 Mon Sep 17 00:00:00 2001 From: lone-cloud Date: Sat, 7 Feb 2026 01:54:06 -0800 Subject: [PATCH] improve README, code cleaning, use the other sqlite lib for less RAM usage, use any instead of interface, add 3 retries with exponential backoff for undelivered notifications --- README.md | 49 +++++--------- assets/screenshots/1.webp | Bin 3940 -> 0 bytes assets/screenshots/2.webp | Bin 19970 -> 0 bytes assets/screenshots/3.webp | Bin 5142 -> 0 bytes assets/screenshots/4.webp | Bin 6304 -> 0 bytes go.mod | 16 ++++- go.sum | 52 ++++++++++++--- service/config/config.go | 30 ++++----- service/integration/proton/email.go | 2 +- service/integration/proton/handlers.go | 16 ++--- service/integration/signal/client.go | 18 ++--- service/integration/signal/link.go | 2 +- service/integration/webpush/handlers.go | 14 ++-- service/notification/dispatcher.go | 29 +++++++- service/notification/notification.go | 12 ++-- service/notification/store.go | 3 +- service/server/handlers_admin.go | 11 ++- service/server/handlers_fragment.go | 3 +- service/server/handlers_health.go | 53 +++++++++++++++ service/server/handlers_ntfy.go | 85 +++++++++++++++--------- service/server/middleware.go | 9 +++ service/server/server.go | 11 +-- service/util/http.go | 14 +++- service/util/template.go | 4 +- 24 files changed, 280 insertions(+), 153 deletions(-) delete mode 100644 assets/screenshots/1.webp delete mode 100644 assets/screenshots/2.webp delete mode 100644 assets/screenshots/3.webp delete mode 100644 assets/screenshots/4.webp create mode 100644 service/server/handlers_health.go diff --git a/README.md b/README.md index 35985bb..b1621d2 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ # Prism -**Self-hosted notification gateway using WebPush and optional Signal for transport** +**Self-hosted notification gateway** -[Setup](#setup) • [Real-World Examples](#real-world-examples) • [Architecture](#architecture) +[Setup](#setup) • [Real-World Examples](#real-world-examples) -Prism is a self-hosted notification gateway that receives HTTP requests and routes them through WebPush apps or optionally through Signal groups. Route notifications through Signal to avoid exposing unique network fingerprints, or forward them to your own WebPush apps for custom handling. +Prism is a self-hosted notification gateway. Prism can receive messages and route them to Signal, Telegram or WebPush URLs. Messages can be sent via Webhooks or from an optional Proton Mail integration. ## Setup @@ -31,7 +31,7 @@ nano .env # Set API_KEY=your-secret-key-here docker compose up -d ``` -Prism is now running at . By default, all notifications use WebPush (encrypted push notifications or plain HTTP webhooks). Enable optional integrations below for Signal or Telegram delivery, or Proton Mail monitoring. +Prism is now running at . Enable optional integrations below for custom functionality. ## Integrations @@ -39,7 +39,8 @@ All integrations are optional. Enable only what you need. ### Signal -Send notifications through Signal groups instead of WebPush. +Send notifications through Signal groups. +Each registered app will route messages to private Signal groups matching the app name. **1. Enable Signal in `.env`:** @@ -47,7 +48,7 @@ Send notifications through Signal groups instead of WebPush. FEATURE_ENABLE_SIGNAL=true ``` -**2. Start Prism with Signal:** +**2. Start Prism with the Signal service:** ```bash docker compose --profile signal up -d @@ -59,13 +60,12 @@ Visit , authenticate with your API_KEY, and scan the QR c **Settings → Linked Devices → Link New Device** -![QR code linking screen](assets/screenshots/2.webp) - Once linked, new apps will default to Signal delivery. ### Telegram -Send notifications through Telegram instead of WebPush. +Send notifications through Telegram instead. +Unlike the Signal integration, Telegram relies on creating a Telegram bot as it will serve as the notifications messenger. **1. Create a Telegram bot:** @@ -103,10 +103,12 @@ All notifications will now be sent to your Telegram chat. Unlike Signal, Telegra ### Proton Mail Receive notifications when new Proton Mail emails arrive. +Unlike other integrations, this one will generate new messages to be delivered by one of the configured transports. +Note that using this integration requires a paid Proton Mail account to be able to use the Proton Mail Bridge that this integration relies on. -> **Note:** The default image (`shenxn/protonmail-bridge:build`) compiles from source and supports all architectures. For x86_64 only, you can use `shenxn/protonmail-bridge:latest` (smaller, faster). +> **Note:** The default image (`shenxn/protonmail-bridge:build`) used by Prism compiles from source and supports all architectures. For x86_64 only, you can use `shenxn/protonmail-bridge:latest` (smaller, faster). -**1. Initialize the bridge:** +**1. Initialize the Proton Mail Bridge:** ```bash docker compose run --rm protonmail-bridge init @@ -140,15 +142,9 @@ PROTON_IMAP_PASSWORD=password-from-info-command **5. Start Prism with Proton Mail:** ```bash -# Start only Prism + Proton Mail docker compose --profile proton up -d - -# Or with Signal + Proton Mail -docker compose --profile signal --profile proton up -d ``` -Prism will now forward Proton Mail notifications to your configured channel (Signal, Telegram, or WebPush). - ## Real-World Examples ### Proton Mail Notifications @@ -185,9 +181,9 @@ Reboot your Home Assistant system and you'll then be able to send Signal notific ### Send Notification -#### POST /{topic} +#### POST /{appName} -Send a notification to a registered app/topic. Compatible with ntfy format. +Send a notification. Compatible with ntfy format. ```bash curl -X POST http://localhost:8080/my-app \ @@ -253,18 +249,5 @@ The health of the system can be viewed in the same admin UI used for linking Sig For API-based monitoring, call `/api/health` which returns JSON: ```json -{"uptime":"3s","signal":{"daemon":"running","linked":true},"protonMail":"connected"} +{"uptime":"3s","signal":{"linked":true},"proton":{"linked":true},"telegram":{"linked":true}} ``` - -## Architecture - -Prism accepts notifications via HTTP POST requests and routes them based on your configured delivery method: - -- **WebPush** (default): Supports both encrypted WebPush (with VAPID signing and payload encryption) and plain HTTP webhooks - - **Encrypted WebPush**: Full WebPush protocol with end-to-end encryption - requires `appName`, `pushEndpoint`, `p256dh`, `auth`, and `vapidPrivateKey` - - **Plain webhooks**: Simple JSON POST to any HTTP endpoint - only requires `appName` and `pushEndpoint` -- **Signal groups** (optional): Uses [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) to create a Signal group for each app and send notifications as messages - -Each app can be independently configured to use either delivery method through the admin UI. - -For the optional Proton Mail integration, Prism requires a server that runs Proton's official [proton-bridge](https://github.com/ProtonMail/proton-bridge). Prism's docker compose process will run an image from [protonmail-bridge-docker](https://github.com/shenxn/protonmail-bridge-docker). Once authenticated, the communication between Prism and proton-bridge will be over IMAP. diff --git a/assets/screenshots/1.webp b/assets/screenshots/1.webp deleted file mode 100644 index 167d35dc0e5a8701d26daa9a94579814befb2fd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3940 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 diff --git a/assets/screenshots/2.webp b/assets/screenshots/2.webp deleted file mode 100644 index b3f26a11243083a55790664aff593771eb449d2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19970 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�_SrdK4f*a|Bt39b`` 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 diff --git a/assets/screenshots/3.webp b/assets/screenshots/3.webp deleted file mode 100644 index 6603802aaa5d3804730ca099962af7d63faa8d3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5142 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 lZ>!$Jeciq6O@3Q~^ diff --git a/assets/screenshots/4.webp b/assets/screenshots/4.webp deleted file mode 100644 index bcdbefc92432575664b916228f2efba6d2d47499..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6304 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 diff --git a/go.mod b/go.mod index d723a4c..2c3a024 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/go-chi/chi/v5 v5.2.4 github.com/joho/godotenv v1.5.1 github.com/mymmrac/telego v1.5.1 - github.com/ncruces/go-sqlite3 v0.30.5 golang.org/x/time v0.14.0 + modernc.org/sqlite v1.34.4 ) require ( @@ -18,14 +18,18 @@ require ( github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emersion/go-message v0.18.1 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/grbit/go-json v0.11.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/ncruces/julianday v1.0.0 // indirect - github.com/tetratelabs/wazero v1.11.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect @@ -33,4 +37,10 @@ require ( golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/sys v0.40.0 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 0a51bc5..f774ecc 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emersion/go-imap/v2 v2.0.0-beta.7 h1:lNznYWa5uhMrngnSYEklzCeye4DBq9TEJ+pr0K593+8= github.com/emersion/go-imap/v2 v2.0.0-beta.7/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= @@ -24,22 +26,30 @@ github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfup github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc= github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mymmrac/telego v1.5.1 h1:BnPPo158ABpHdS6xsTymLb8ut1gLwS927y87c+14mV8= github.com/mymmrac/telego v1.5.1/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y= -github.com/ncruces/go-sqlite3 v0.30.5 h1:6usmTQ6khriL8oWilkAZSJM/AIpAlVL2zFrlcpDldCE= -github.com/ncruces/go-sqlite3 v0.30.5/go.mod h1:0I0JFflTKzfs3Ogfv8erP7CCoV/Z8uxigVDNOR0AQ5E= -github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= -github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -50,8 +60,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -79,6 +87,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -94,6 +103,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -101,6 +111,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -126,8 +137,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -135,9 +144,36 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8= +modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/service/config/config.go b/service/config/config.go index 505fb92..3a549ad 100644 --- a/service/config/config.go +++ b/service/config/config.go @@ -7,26 +7,22 @@ import ( ) type Config struct { - Port int - APIKey string - VerboseLogging bool - RateLimit int - + TelegramChatID int64 + Port int + RateLimit int + APIKey string DeviceName string PrismEndpointPrefix string - EnableSignal bool SignalSocket string - - EnableProton bool - ProtonIMAPUsername string - ProtonIMAPPassword string - ProtonBridgeAddr string - - EnableTelegram bool - TelegramBotToken string - TelegramChatID int64 - - StoragePath string + ProtonIMAPUsername string + ProtonIMAPPassword string + ProtonBridgeAddr string + TelegramBotToken string + StoragePath string + VerboseLogging bool + EnableSignal bool + EnableProton bool + EnableTelegram bool } func Load() (*Config, error) { diff --git a/service/integration/proton/email.go b/service/integration/proton/email.go index e33cecb..c162e6d 100644 --- a/service/integration/proton/email.go +++ b/service/integration/proton/email.go @@ -82,7 +82,7 @@ func (m *Monitor) sendNotification() error { ID: "mark-read", Endpoint: "/api/proton-mail/mark-read", Method: "POST", - Data: map[string]interface{}{ + Data: map[string]any{ "uid": msgData.UID, }, }, diff --git a/service/integration/proton/handlers.go b/service/integration/proton/handlers.go index 49ff232..bc4a3fa 100644 --- a/service/integration/proton/handlers.go +++ b/service/integration/proton/handlers.go @@ -94,31 +94,23 @@ type markReadRequest struct { func (h *Handlers) HandleMarkRead(w http.ResponseWriter, r *http.Request) { var req markReadRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "invalid request body"}) + util.JSONError(w, "invalid request body", http.StatusBadRequest) return } if req.UID == 0 { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "uid (number) is required"}) + util.JSONError(w, "uid (number) is required", http.StatusBadRequest) return } if h.monitor == nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "Proton integration not enabled"}) + util.JSONError(w, "Proton integration not enabled", http.StatusBadRequest) return } if err := h.monitor.MarkAsRead(req.UID); err != nil { h.logger.Error("failed to mark email as read", "uid", req.UID, "error", err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "failed to mark as read"}) + util.JSONError(w, "failed to mark as read", http.StatusInternalServerError) return } diff --git a/service/integration/signal/client.go b/service/integration/signal/client.go index 01a1b6c..720805b 100644 --- a/service/integration/signal/client.go +++ b/service/integration/signal/client.go @@ -20,10 +20,10 @@ func NewClient(socketPath string) *Client { } type RPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID int32 `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID int32 `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` } type RPCResponse struct { @@ -38,7 +38,7 @@ type RPCError struct { Message string `json:"message"` } -func (c *Client) Call(method string, params map[string]interface{}) (json.RawMessage, error) { +func (c *Client) Call(method string, params map[string]any) (json.RawMessage, error) { conn, err := net.Dial("unix", c.SocketPath) if err != nil { return nil, fmt.Errorf("failed to connect: %w", err) @@ -68,9 +68,9 @@ func (c *Client) Call(method string, params map[string]interface{}) (json.RawMes return response.Result, nil } -func (c *Client) CallWithAccount(method string, params map[string]interface{}, account string) (json.RawMessage, error) { +func (c *Client) CallWithAccount(method string, params map[string]any, account string) (json.RawMessage, error) { if params == nil { - params = make(map[string]interface{}) + params = make(map[string]any) } params["account"] = account return c.Call(method, params) @@ -120,7 +120,7 @@ func (c *Client) CreateGroup(name string) (string, string, error) { return "", "", fmt.Errorf("no linked Signal account") } - params := map[string]interface{}{ + params := map[string]any{ "name": name, "member": []string{}, } @@ -157,7 +157,7 @@ func (c *Client) SendGroupMessage(groupID, message string) error { return fmt.Errorf("no linked account") } - params := map[string]interface{}{ + params := map[string]any{ "groupId": groupID, "message": message, "notifySelf": true, diff --git a/service/integration/signal/link.go b/service/integration/signal/link.go index 7454ead..46bf4ae 100644 --- a/service/integration/signal/link.go +++ b/service/integration/signal/link.go @@ -62,7 +62,7 @@ func (l *LinkDevice) GenerateQR() (string, error) { } func (l *LinkDevice) finishLink() { - params := map[string]interface{}{ + params := map[string]any{ "deviceLinkUri": l.deviceLinkUri, "deviceName": l.deviceName, } diff --git a/service/integration/webpush/handlers.go b/service/integration/webpush/handlers.go index 1b6646c..a6f44d7 100644 --- a/service/integration/webpush/handlers.go +++ b/service/integration/webpush/handlers.go @@ -40,16 +40,12 @@ func (h *Handlers) HandleRegister(w http.ResponseWriter, r *http.Request) { } if req.AppName == "" || req.PushEndpoint == "" { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "appName and pushEndpoint are required"}) + util.JSONError(w, "appName and pushEndpoint are required", http.StatusBadRequest) return } if _, err := url.Parse(req.PushEndpoint); err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "Invalid pushEndpoint URL"}) + util.JSONError(w, "Invalid pushEndpoint URL", http.StatusBadRequest) return } @@ -82,8 +78,7 @@ func (h *Handlers) HandleRegister(w http.ResponseWriter, r *http.Request) { } else { channel := notification.ChannelWebPush if err := h.store.Register(req.AppName, &channel, nil, webPush); err != nil { - h.logger.Error("Failed to register webpush endpoint", "error", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + util.LogAndError(w, h.logger, "Failed to register webpush endpoint", http.StatusInternalServerError, err) return } h.logger.Info("Registered new webpush endpoint", "app", req.AppName, "pushEndpoint", req.PushEndpoint) @@ -105,8 +100,7 @@ func (h *Handlers) HandleUnregister(w http.ResponseWriter, r *http.Request) { } if err := h.store.ClearWebPush(appName); err != nil { - h.logger.Error("Failed to clear webpush endpoint", "error", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + util.LogAndError(w, h.logger, "Failed to clear webpush endpoint", http.StatusInternalServerError, err) return } diff --git a/service/notification/dispatcher.go b/service/notification/dispatcher.go index c2650cd..32b41ed 100644 --- a/service/notification/dispatcher.go +++ b/service/notification/dispatcher.go @@ -3,6 +3,7 @@ package notification import ( "fmt" "log/slog" + "time" "prism/service/util" ) @@ -85,5 +86,31 @@ func (d *Dispatcher) Send(appName string, notif Notification) error { return fmt.Errorf("no sender for channel: %s", mapping.Channel) } - return sender.Send(mapping, notif) + return d.sendWithRetry(sender, mapping, notif, appName) +} + +func (d *Dispatcher) sendWithRetry(sender NotificationSender, mapping *Mapping, notif Notification, appName string) error { + maxRetries := 3 + baseDelay := 100 * time.Millisecond + + var lastErr error + for attempt := 0; attempt < maxRetries; attempt++ { + err := sender.Send(mapping, notif) + if err == nil { + if attempt > 0 { + d.logger.Info("Notification sent after retry", "app", appName, "attempt", attempt+1) + } + return nil + } + + lastErr = err + if attempt < maxRetries-1 { + delay := baseDelay * time.Duration(1<