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 167d35d..0000000 Binary files a/assets/screenshots/1.webp and /dev/null differ diff --git a/assets/screenshots/2.webp b/assets/screenshots/2.webp deleted file mode 100644 index b3f26a1..0000000 Binary files a/assets/screenshots/2.webp and /dev/null differ diff --git a/assets/screenshots/3.webp b/assets/screenshots/3.webp deleted file mode 100644 index 6603802..0000000 Binary files a/assets/screenshots/3.webp and /dev/null differ diff --git a/assets/screenshots/4.webp b/assets/screenshots/4.webp deleted file mode 100644 index bcdbefc..0000000 Binary files a/assets/screenshots/4.webp and /dev/null differ 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<