Skip to content

Commit 98dfb95

Browse files
committed
Implement Telegram notification provider
1 parent 27b6a5d commit 98dfb95

File tree

13 files changed

+351
-38
lines changed

13 files changed

+351
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
2121
.env.test.local
2222
.env.production.local
2323
.env.local
24+
config.toml
2425

2526
# caches
2627
.eslintcache

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ A high-performance uptime monitoring system built with Bun and ClickHouse. Recei
44

55
## Features
66

7-
- **Pulse-Based Monitoring** Services send heartbeats; missing pulses trigger alerts
8-
- **Hierarchical Groups** Organize monitors with flexible health strategies (any-up, all-up, percentage)
9-
- **Custom Metrics** Track up to 3 numeric values per monitor (player count, connections, etc.)
10-
- **Multi-Channel Notifications** Discord, Email, and Ntfy support with per-monitor control
11-
- **Real-Time Status Pages** WebSocket-powered live updates
12-
- **Self-Healing** Automatic backfill when the monitor itself recovers from downtime
7+
- **Pulse-Based Monitoring** - Services send heartbeats; missing pulses trigger alerts
8+
- **Hierarchical Groups** - Organize monitors with flexible health strategies (any-up, all-up, percentage)
9+
- **Custom Metrics** - Track up to 3 numeric values per monitor (player count, connections, etc.)
10+
- **Multi-Channel Notifications** - Discord, Email, Ntfy and Telegram support with per-monitor control
11+
- **Real-Time Status Pages** - WebSocket-powered live updates
12+
- **Self-Healing** - Automatic backfill when the monitor itself recovers from downtime
1313

1414
## Quick Start
1515

@@ -82,7 +82,7 @@ curl http://localhost:3000/v1/status/:slug
8282
| [Pulses](docs/pulses.md) | How pulses work, timing, and best practices |
8383
| [Configuration Guide](docs/configuration.md) | Complete config.toml reference |
8484
| [API Reference](docs/api.md) | All endpoints and WebSocket events |
85-
| [Notifications](docs/notifications.md) | Setting up Discord, Email, Ntfy |
85+
| [Notifications](docs/notifications.md) | Setting up Discord, Email, Ntfy, Telegram |
8686
| [Groups & Strategies](docs/groups.md) | Organizing monitors hierarchically |
8787
| [Custom Metrics](docs/custom-metrics.md) | Tracking additional data points |
8888
| [PulseMonitor Integration](docs/pulsemonitor.md) | Automated monitoring from multiple regions |

config.toml renamed to config.example.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,14 @@ topic = "uptime-monitor"
276276
#token = "tk_your_token_here"
277277
# Optional: Username/password authentication
278278
#username = "your_username"
279-
#password = "your_password"
279+
#password = "your_password"
280+
281+
# Telegram configuration for critical alerts
282+
[notifications.channels.critical.telegram]
283+
enabled = false
284+
botToken = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
285+
chatId = "-1001234567890"
286+
# Optional: send to a specific forum topic
287+
# topicId = 123
288+
# Optional: send silently
289+
# disableNotification = false

docs/configuration.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,13 @@ pulseMonitors = ["US-WEST-1"]
138138

139139
| Field | Required | Default | Description |
140140
| ---------------------- | -------- | ------- | ------------------------------------------------------------ |
141-
| `id` | Yes | | Unique identifier |
142-
| `name` | Yes | | Display name |
143-
| `token` | Yes | | Secret token for pulse authentication |
144-
| `interval` | Yes | | Expected pulse interval in seconds (see [Pulses](pulses.md)) |
145-
| `maxRetries` | Yes | | Missed pulses before marking down (see [Pulses](pulses.md)) |
146-
| `resendNotification` | Yes | | Resend notification every N down checks (0 = never) |
147-
| `groupId` | No | | Parent group ID |
141+
| `id` | Yes | - | Unique identifier |
142+
| `name` | Yes | - | Display name |
143+
| `token` | Yes | - | Secret token for pulse authentication |
144+
| `interval` | Yes | - | Expected pulse interval in seconds (see [Pulses](pulses.md)) |
145+
| `maxRetries` | Yes | - | Missed pulses before marking down (see [Pulses](pulses.md)) |
146+
| `resendNotification` | Yes | - | Resend notification every N down checks (0 = never) |
147+
| `groupId` | No | - | Parent group ID |
148148
| `notificationChannels` | No | `[]` | Array of notification channel IDs |
149149
| `pulseMonitors` | No | `[]` | Array of PulseMonitor IDs for automated checking |
150150

@@ -217,21 +217,21 @@ notificationChannels = ["critical"]
217217

218218
| Field | Required | Default | Description |
219219
| ---------------------- | -------- | ------- | ------------------------------------------------ |
220-
| `id` | Yes | | Unique identifier |
221-
| `name` | Yes | | Display name |
222-
| `strategy` | Yes | | `"any-up"`, `"all-up"`, or `"percentage"` |
223-
| `degradedThreshold` | Yes | | Percentage threshold (0-100) for degraded status |
224-
| `interval` | Yes | | Used for uptime calculations |
220+
| `id` | Yes | - | Unique identifier |
221+
| `name` | Yes | - | Display name |
222+
| `strategy` | Yes | - | `"any-up"`, `"all-up"`, or `"percentage"` |
223+
| `degradedThreshold` | Yes | - | Percentage threshold (0-100) for degraded status |
224+
| `interval` | Yes | - | Used for uptime calculations |
225225
| `resendNotification` | No | `0` | Resend notification every N down checks |
226-
| `parentId` | No | | Parent group ID for nesting |
226+
| `parentId` | No | - | Parent group ID for nesting |
227227
| `notificationChannels` | No | `[]` | Array of notification channel IDs |
228228

229229
### Strategy Reference
230230

231231
| Strategy | UP | DEGRADED | DOWN |
232232
| ------------ | --------------- | -------------- | ----------------- |
233-
| `any-up` | ≥1 child up | | All children down |
234-
| `all-up` | All children up | | Any child down |
233+
| `any-up` | ≥1 child up | - | All children down |
234+
| `all-up` | All children up | - | Any child down |
235235
| `percentage` | 100% up | ≥threshold% up | <threshold% up |
236236

237237
## Status Pages
@@ -329,7 +329,7 @@ The reload token is shown in logs at startup if not explicitly configured.
329329

330330
The configuration is validated at startup. Common errors:
331331

332-
- **Duplicate IDs/tokens** All IDs and tokens must be unique
333-
- **Invalid references** Group/notification channel IDs must exist
334-
- **Circular references** Groups cannot reference themselves as parents
335-
- **Missing required fields** All required fields must be present
332+
- **Duplicate IDs/tokens** - All IDs and tokens must be unique
333+
- **Invalid references** - Group/notification channel IDs must exist
334+
- **Circular references** - Groups cannot reference themselves as parents
335+
- **Missing required fields** - All required fields must be present

docs/groups.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ Group uptime is computed from child uptimes using the same strategy:
167167

168168
## Group History
169169

170-
Groups don't store their own pulseshistory is computed from children in real-time.
170+
Groups don't store their own pulses-history is computed from children in real-time.
171171

172172
```bash
173173
# Get raw history (~24h)

docs/notifications.md

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ Uptime Monitor supports multiple notification providers. Notifications are organ
44

55
## How It Works
66

7-
1. **Define channels** Each channel has a unique ID and can have multiple providers (Discord, Email, Ntfy)
8-
2. **Assign channels to monitors/groups** Use the `notificationChannels` array
9-
3. **Receive alerts** When a monitor goes down, recovers, or stays down, notifications are sent to all providers in the assigned channels
7+
1. **Define channels** - Each channel has a unique ID and can have multiple providers (Discord, Email, Ntfy)
8+
2. **Assign channels to monitors/groups** - Use the `notificationChannels` array
9+
3. **Receive alerts** - When a monitor goes down, recovers, or stays down, notifications are sent to all providers in the assigned channels
1010

1111
## Notification Types
1212

@@ -175,6 +175,55 @@ password = "secret"
175175
ntfy subscribe my-uptime-alerts
176176
```
177177

178+
## Telegram
179+
180+
Send notifications to Telegram chats, groups, or channels via a bot.
181+
182+
### Configuration
183+
184+
```toml
185+
[notifications.channels.critical.telegram]
186+
enabled = true
187+
botToken = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
188+
chatId = "-1001234567890"
189+
# Optional: send to a specific forum topic
190+
# topicId = 123
191+
# Optional: send silently
192+
# disableNotification = false
193+
194+
```
195+
196+
| Field | Required | Description |
197+
| --------------------- | -------- | ----------------------------------------------- |
198+
| `enabled` | Yes | Enable Telegram notifications |
199+
| `botToken` | Yes | Bot API token from @BotFather |
200+
| `chatId` | Yes | Target chat/group/channel ID |
201+
| `topicId` | No | Forum topic ID (for groups with topics enabled) |
202+
| `disableNotification` | No | Send silently without notification sound |
203+
204+
### Setting Up a Telegram Bot
205+
206+
1. Message [@BotFather](https://t.me/BotFather) on Telegram
207+
2. Send `/newbot` and follow the prompts
208+
3. Copy the bot token provided
209+
4. Add the bot to your group/channel
210+
5. Get the chat ID:
211+
- For groups: add [@userinfobot](https://t.me/userinfobot) to the group, or use the Telegram Bot API `getUpdates` endpoint
212+
- For channels: forward a message from the channel to @userinfobot
213+
- For direct messages: message the bot and check `getUpdates`
214+
215+
### Forum Topics
216+
217+
If your group has topics (forum mode) enabled, you can send to a specific topic:
218+
219+
```toml
220+
[notifications.channels.critical.telegram]
221+
enabled = true
222+
botToken = "123456:ABC-DEF..."
223+
chatId = "-1001234567890"
224+
topicId = 42
225+
```
226+
178227
## Assigning Channels to Monitors
179228

180229
```toml

docs/pulses.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ interval = 30
3030
maxRetries = 0 # Mark down immediately on first missed pulse
3131
```
3232

33-
- `maxRetries = 0` Down on the first missed pulse
34-
- `maxRetries = 3` Tolerates 3 missed pulses before marking down (roughly `3 x interval from missingPulseDetector` seconds)
33+
- `maxRetries = 0` - Down on the first missed pulse
34+
- `maxRetries = 3` - Tolerates 3 missed pulses before marking down (roughly `3 x interval from missingPulseDetector` seconds)
3535

3636
## Sending Pulses
3737

@@ -40,7 +40,7 @@ Pulses are sent via HTTP GET or WebSocket.
4040
### HTTP
4141

4242
```bash
43-
# Simple pulse records with current timestamp
43+
# Simple pulse - records with current timestamp
4444
curl http://localhost:3000/v1/push/:token
4545
```
4646

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "uptimemonitor-server",
33
"module": "src/index.ts",
4-
"version": "0.2.16",
4+
"version": "0.2.17",
55
"type": "module",
66
"private": true,
77
"scripts": {

src/config.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import type {
1717
import type { NodeClickHouseClientConfigOptions } from "@clickhouse/client/dist/config";
1818
import { Logger } from "./logger";
1919
import { type IpExtractionPreset } from "@rabbit-company/web-middleware/ip-extract";
20-
import { password } from "bun";
2120

2221
export const defaultReloadToken = generateSecureToken();
2322

@@ -1074,6 +1073,50 @@ function validateNtfyConfig(config: unknown, channelId: string): any {
10741073
return result;
10751074
}
10761075

1076+
function validateTelegramConfig(config: unknown, channelId: string): any {
1077+
const errors: string[] = [];
1078+
const cfg = config as Record<string, unknown>;
1079+
1080+
if (!isBoolean(cfg.enabled)) {
1081+
errors.push(`notifications.channels.${channelId}.telegram.enabled must be a boolean`);
1082+
}
1083+
1084+
if (!cfg.enabled) {
1085+
return { enabled: false };
1086+
}
1087+
1088+
if (!isString(cfg.botToken) || cfg.botToken.trim().length === 0) {
1089+
errors.push(`notifications.channels.${channelId}.telegram.botToken must be a non-empty string`);
1090+
}
1091+
1092+
if (!isString(cfg.chatId) || cfg.chatId.trim().length === 0) {
1093+
errors.push(`notifications.channels.${channelId}.telegram.chatId must be a non-empty string`);
1094+
}
1095+
1096+
if (cfg.topicId !== undefined && !isNumber(cfg.topicId)) {
1097+
errors.push(`notifications.channels.${channelId}.telegram.topicId must be a number if provided`);
1098+
}
1099+
1100+
if (cfg.disableNotification !== undefined && !isBoolean(cfg.disableNotification)) {
1101+
errors.push(`notifications.channels.${channelId}.telegram.disableNotification must be a boolean if provided`);
1102+
}
1103+
1104+
if (errors.length > 0) {
1105+
throw new ConfigValidationError(errors);
1106+
}
1107+
1108+
const result: any = {
1109+
enabled: cfg.enabled,
1110+
botToken: cfg.botToken,
1111+
chatId: cfg.chatId,
1112+
};
1113+
1114+
if (cfg.topicId !== undefined) result.topicId = cfg.topicId;
1115+
if (cfg.disableNotification !== undefined) result.disableNotification = cfg.disableNotification;
1116+
1117+
return result;
1118+
}
1119+
10771120
function validateNotificationChannel(channel: unknown, channelId: string): NotificationChannel {
10781121
const errors: string[] = [];
10791122

@@ -1143,6 +1186,16 @@ function validateNotificationChannel(channel: unknown, channelId: string): Notif
11431186
}
11441187
}
11451188

1189+
if (channel.telegram) {
1190+
try {
1191+
result.telegram = validateTelegramConfig(channel.telegram, channelId);
1192+
} catch (error) {
1193+
if (error instanceof ConfigValidationError) {
1194+
throw error;
1195+
}
1196+
}
1197+
}
1198+
11461199
return result;
11471200
}
11481201

@@ -1417,8 +1470,9 @@ function validateNotificationChannelProviders(config: Config): void {
14171470
const hasEmail = channel.email?.enabled;
14181471
const hasDiscord = channel.discord?.enabled;
14191472
const hasNtfy = channel.ntfy?.enabled;
1473+
const hasTelegram = channel.telegram?.enabled;
14201474

1421-
if (!hasEmail && !hasDiscord && !hasNtfy) {
1475+
if (!hasEmail && !hasDiscord && !hasNtfy && !hasTelegram) {
14221476
errors.push(`Notification channel '${channelId}' is enabled but has no providers configured`);
14231477
}
14241478
}

src/notifications/channel-manager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EmailProvider } from "./providers/email";
22
import { DiscordProvider } from "./providers/discord";
33
import { NtfyProvider } from "./providers/ntfy";
4+
import { TelegramProvider } from "./providers/telegram";
45
import { Logger } from "../logger";
56
import type { NotificationChannel, NotificationEvent, NotificationProvider } from "../types";
67

@@ -33,6 +34,10 @@ export class NotificationChannelManager {
3334
channelProviders.push(new NtfyProvider(channel.ntfy));
3435
}
3536

37+
if (channel.telegram?.enabled) {
38+
channelProviders.push(new TelegramProvider(channel.telegram));
39+
}
40+
3641
if (channelProviders.length > 0) {
3742
this.providers.set(channelId, channelProviders);
3843
Logger.info("Notification channel initialized", {

0 commit comments

Comments
 (0)