mirror of
https://github.com/lone-cloud/prism
synced 2026-06-03 08:43:10 -07:00
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
This commit is contained in:
parent
7fdc62e91b
commit
7fd57101a3
24 changed files with 280 additions and 153 deletions
49
README.md
49
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)
|
||||
|
||||
</div>
|
||||
|
||||
<!-- markdownlint-enable MD033 -->
|
||||
|
||||
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 <http://localhost:8080>. 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 <http://localhost:8080>. 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 <http://localhost:8080>, authenticate with your API_KEY, and scan the QR c
|
|||
|
||||
**Settings → Linked Devices → Link New Device**
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
16
go.mod
16
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
|
||||
)
|
||||
|
|
|
|||
52
go.sum
52
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=
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<<uint(attempt))
|
||||
d.logger.Warn("Failed to send notification, retrying", "app", appName, "attempt", attempt+1, "error", err, "retryIn", delay)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Error("Failed to send notification after retries", "app", appName, "attempts", maxRetries, "error", lastErr)
|
||||
return lastErr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package notification
|
||||
|
||||
type Action struct {
|
||||
ID string `json:"id"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Method string `json:"method"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Method string `json:"method"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
|
|
@ -68,8 +68,8 @@ type SignalSubscription struct {
|
|||
}
|
||||
|
||||
type Mapping struct {
|
||||
AppName string
|
||||
Channel Channel
|
||||
Signal *SignalSubscription
|
||||
WebPush *WebPushSubscription
|
||||
AppName string
|
||||
Channel Channel
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package server
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
|
@ -21,7 +20,7 @@ func (s *Server) handleGetMappings(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(mappings); err != nil {
|
||||
slog.Error("Failed to encode response", "error", err)
|
||||
s.logger.Error("Failed to encode response", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,8 +108,7 @@ func (s *Server) handleUpdateChannel(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if err := s.store.UpdateChannel(appName, req.Channel); err != nil {
|
||||
s.logger.Error("Failed to update channel", "error", err)
|
||||
http.Error(w, "Failed to update channel", http.StatusInternalServerError)
|
||||
util.LogAndError(w, s.logger, "Failed to update channel", http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -123,14 +121,13 @@ func (s *Server) handleUpdateChannel(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *Server) handleGetStats(w http.ResponseWriter, r *http.Request) {
|
||||
mappings, err := s.store.GetAllMappings()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get mappings", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
util.LogAndError(w, s.logger, "Failed to get mappings", http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
uptime := time.Since(s.startTime)
|
||||
|
||||
stats := map[string]interface{}{
|
||||
stats := map[string]any{
|
||||
"uptime": util.FormatUptime(uptime),
|
||||
"uptimeSeconds": int(uptime.Seconds()),
|
||||
"mappingsCount": len(mappings),
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ func (s *Server) handleFragmentApps(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
var buf bytes.Buffer
|
||||
if err := s.fragmentTmpl.ExecuteTemplate(&buf, "app-list.html", s.buildAppListData(mappings)); err != nil {
|
||||
s.logger.Error("Failed to execute template", "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
util.LogAndError(w, s.logger, "Failed to execute template", http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
53
service/server/handlers_health.go
Normal file
53
service/server/handlers_health.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"prism/service/util"
|
||||
)
|
||||
|
||||
type healthResponse struct {
|
||||
Uptime string `json:"uptime"`
|
||||
Signal *integrationHealth `json:"signal,omitempty"`
|
||||
Proton *integrationHealth `json:"proton,omitempty"`
|
||||
Telegram *integrationHealth `json:"telegram,omitempty"`
|
||||
}
|
||||
|
||||
type integrationHealth struct {
|
||||
Linked bool `json:"linked"`
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
uptime := time.Since(s.startTime)
|
||||
|
||||
resp := healthResponse{
|
||||
Uptime: util.FormatUptime(uptime),
|
||||
}
|
||||
|
||||
if s.integrations.Signal != nil && s.integrations.Signal.IsEnabled() {
|
||||
signalClient := s.integrations.Signal.GetHandlers().GetClient()
|
||||
account, _ := signalClient.GetLinkedAccount()
|
||||
resp.Signal = &integrationHealth{
|
||||
Linked: account != nil,
|
||||
}
|
||||
}
|
||||
|
||||
if s.cfg.IsProtonEnabled() {
|
||||
resp.Proton = &integrationHealth{
|
||||
Linked: true,
|
||||
}
|
||||
}
|
||||
|
||||
if s.cfg.IsTelegramEnabled() {
|
||||
resp.Telegram = &integrationHealth{
|
||||
Linked: true,
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
s.logger.Error("Failed to encode health response", "error", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,18 +9,19 @@ import (
|
|||
"time"
|
||||
|
||||
"prism/service/notification"
|
||||
"prism/service/util"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func (s *Server) handleNtfyPublish(w http.ResponseWriter, r *http.Request) {
|
||||
topic := chi.URLParam(r, "endpoint")
|
||||
if topic == "" {
|
||||
topic = chi.URLParam(r, "topic")
|
||||
appName := chi.URLParam(r, "appName")
|
||||
if appName == "" {
|
||||
appName = chi.URLParam(r, "endpoint")
|
||||
}
|
||||
topic, _ = url.QueryUnescape(topic)
|
||||
if topic == "" || strings.Contains(topic, "/") {
|
||||
http.Error(w, "Invalid topic", http.StatusBadRequest)
|
||||
appName, _ = url.QueryUnescape(appName)
|
||||
if appName == "" || strings.Contains(appName, "/") {
|
||||
http.Error(w, "Invalid app name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -30,24 +31,25 @@ func (s *Server) handleNtfyPublish(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
message := string(body)
|
||||
if message == "" {
|
||||
http.Error(w, "Message required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
title := r.Header.Get("X-Title")
|
||||
if title == "" {
|
||||
title = r.Header.Get("Title")
|
||||
}
|
||||
if title == "" {
|
||||
title = r.Header.Get("t")
|
||||
}
|
||||
|
||||
var message, title string
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "application/x-www-form-urlencoded") {
|
||||
if values, err := url.ParseQuery(message); err == nil {
|
||||
if m := values.Get("message"); m != "" {
|
||||
message = m
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
var payload struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err == nil {
|
||||
message = payload.Message
|
||||
title = payload.Title
|
||||
} else {
|
||||
message = string(body)
|
||||
}
|
||||
} else if strings.Contains(contentType, "application/x-www-form-urlencoded") {
|
||||
if values, err := url.ParseQuery(string(body)); err == nil {
|
||||
message = values.Get("message")
|
||||
if message == "" {
|
||||
message = string(body)
|
||||
}
|
||||
if title == "" {
|
||||
if t := values.Get("title"); t != "" {
|
||||
|
|
@ -56,10 +58,29 @@ func (s *Server) handleNtfyPublish(w http.ResponseWriter, r *http.Request) {
|
|||
title = t
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message = string(body)
|
||||
}
|
||||
} else {
|
||||
message = string(body)
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = r.Header.Get("X-Title")
|
||||
if title == "" {
|
||||
title = r.Header.Get("Title")
|
||||
}
|
||||
if title == "" {
|
||||
title = r.Header.Get("t")
|
||||
}
|
||||
}
|
||||
|
||||
if title == topic {
|
||||
if message == "" {
|
||||
http.Error(w, "Message required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if title == appName {
|
||||
title = ""
|
||||
}
|
||||
|
||||
|
|
@ -68,20 +89,20 @@ func (s *Server) handleNtfyPublish(w http.ResponseWriter, r *http.Request) {
|
|||
Message: message,
|
||||
}
|
||||
|
||||
if err := s.dispatcher.Send(topic, notif); err != nil {
|
||||
s.logger.Error("Failed to send notification", "app", topic, "error", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
if err := s.dispatcher.Send(appName, notif); err != nil {
|
||||
util.LogAndError(w, s.logger, "Failed to send notification", http.StatusInternalServerError, err, "app", appName)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Debug("Sent ntfy message", "topic", topic, "preview", truncate(message, 50))
|
||||
s.logger.Debug("Sent ntfy message", "app", appName, "preview", truncate(message, 50))
|
||||
|
||||
now := time.Now()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := map[string]interface{}{
|
||||
"id": time.Now().UnixNano(),
|
||||
"time": time.Now().Unix(),
|
||||
response := map[string]any{
|
||||
"id": now.UnixNano(),
|
||||
"time": now.Unix(),
|
||||
"event": "message",
|
||||
"topic": topic,
|
||||
"topic": appName,
|
||||
"message": message,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
|
|
|
|||
|
|
@ -131,3 +131,12 @@ func cleanupVisitors() {
|
|||
visitorsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func maxBodySizeMiddleware(maxBytes int64) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import (
|
|||
var fragmentTemplates embed.FS
|
||||
|
||||
type Server struct {
|
||||
startTime time.Time
|
||||
publicAssets embed.FS
|
||||
cfg *config.Config
|
||||
store *notification.Store
|
||||
dispatcher *notification.Dispatcher
|
||||
|
|
@ -35,11 +37,9 @@ type Server struct {
|
|||
logger *slog.Logger
|
||||
router *chi.Mux
|
||||
httpServer *http.Server
|
||||
startTime time.Time
|
||||
version string
|
||||
indexTmpl *template.Template
|
||||
fragmentTmpl *template.Template
|
||||
publicAssets embed.FS
|
||||
version string
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, publicAssets embed.FS) (*Server, error) {
|
||||
|
|
@ -110,6 +110,7 @@ func (s *Server) setupRoutes() {
|
|||
r.Use(middleware.Compress(5))
|
||||
r.Use(middleware.StripSlashes)
|
||||
r.Use(rateLimitMiddleware(s.cfg.RateLimit))
|
||||
r.Use(maxBodySizeMiddleware(1 << 20))
|
||||
|
||||
r.Get("/", s.handleIndex)
|
||||
|
||||
|
|
@ -140,7 +141,9 @@ func (s *Server) setupRoutes() {
|
|||
r.Get("/stats", s.handleGetStats)
|
||||
})
|
||||
|
||||
r.With(authMiddleware(s.cfg.APIKey)).Post("/{topic}", s.handleNtfyPublish)
|
||||
r.With(authMiddleware(s.cfg.APIKey)).Get("/api/health", s.handleHealth)
|
||||
|
||||
r.With(authMiddleware(s.cfg.APIKey)).Post("/{appName}", s.handleNtfyPublish)
|
||||
|
||||
s.router = r
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func LogAndError(w http.ResponseWriter, logger *slog.Logger, message string, code int, err error) {
|
||||
func LogAndError(w http.ResponseWriter, logger *slog.Logger, message string, code int, err error, attrs ...any) {
|
||||
if err != nil {
|
||||
logger.Error(message, "error", err)
|
||||
logAttrs := append([]any{"error", err}, attrs...)
|
||||
logger.Error(message, logAttrs...)
|
||||
} else {
|
||||
logger.Error(message)
|
||||
logger.Error(message, attrs...)
|
||||
}
|
||||
http.Error(w, message, code)
|
||||
}
|
||||
|
||||
func JSONError(w http.ResponseWriter, message string, code int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ func NewTemplateRenderer(tmpl *template.Template) *TemplateRenderer {
|
|||
return &TemplateRenderer{tmpl: tmpl}
|
||||
}
|
||||
|
||||
func (tr *TemplateRenderer) Render(name string, data interface{}) (string, error) {
|
||||
func (tr *TemplateRenderer) Render(name string, data any) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := tr.tmpl.ExecuteTemplate(&buf, name, data); err != nil {
|
||||
return "", err
|
||||
|
|
@ -21,7 +21,7 @@ func (tr *TemplateRenderer) Render(name string, data interface{}) (string, error
|
|||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (tr *TemplateRenderer) RenderHTML(name string, data interface{}) (template.HTML, error) {
|
||||
func (tr *TemplateRenderer) RenderHTML(name string, data any) (template.HTML, error) {
|
||||
content, err := tr.Render(name, data)
|
||||
return template.HTML(content), err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue