Skip to content

Commit c9573b7

Browse files
committed
Added ntfy topic field to feed configuration
1 parent af46022 commit c9573b7

12 files changed

+144
-85
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A modern web application for monitoring RSS feeds and receiving notifications wh
88
- 🔑 Define custom keywords for each feed
99
- 🔔 Real-time notifications via [ntfy.sh](https://ntfy.sh)
1010
- Customize notifications per feed
11+
- Set custom ntfy topic per feed
1112
- Use post content or custom titles and descriptions
1213
- Set notification priority
1314
- Include post links and matched keywords
@@ -68,6 +69,7 @@ All configuration is managed through the web interface:
6869
- Add, edit, or remove RSS feeds through the "Feeds" tab
6970
- Set keywords for each feed to monitor
7071
- Configure notification settings for each feed:
72+
- Set ntfy topic for notifications
7173
- Choose between post title or custom title
7274
- Choose between post description or custom description
7375
- Set notification priority (urgent, high, default, low, min)
@@ -78,7 +80,6 @@ All configuration is managed through the web interface:
7880

7981
### Settings
8082
- Configure ntfy.sh notification settings in the "Settings" tab
81-
- Set your ntfy topic
8283
- Configure ntfy server address (defaults to https://ntfy.sh)
8384
- Adjust feed check interval
8485

client/src/App.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { AppConfig, FeedStatus as FeedStatusType } from './server/types';
99
export default function App() {
1010
const [config, setConfig] = useState<AppConfig>({
1111
feeds: [],
12-
ntfyTopic: '',
1312
ntfyServerAddress: 'https://ntfy.sh',
1413
checkIntervalMinutes: 15,
14+
defaultNtfyTopic: 'rss',
1515
});
1616
const [status, setStatus] = useState<Record<string, FeedStatusType>>({});
1717
const [loading, setLoading] = useState(true);
@@ -61,13 +61,13 @@ export default function App() {
6161
}
6262
};
6363

64-
const handleSettingsSubmit = async (ntfyTopic: string, ntfyServerAddress: string, checkIntervalMinutes: number) => {
64+
const handleSettingsSubmit = async (ntfyServerAddress: string, checkIntervalMinutes: number, defaultNtfyTopic: string) => {
6565
try {
6666
setLoading(true);
6767
await fetch('/api/config', {
6868
method: 'POST',
6969
headers: { 'Content-Type': 'application/json' },
70-
body: JSON.stringify({ ...config, ntfyTopic, ntfyServerAddress, checkIntervalMinutes }),
70+
body: JSON.stringify({ ...config, ntfyServerAddress, checkIntervalMinutes, defaultNtfyTopic }),
7171
});
7272
await fetchConfig();
7373
} catch (error) {
@@ -115,9 +115,9 @@ export default function App() {
115115

116116
<Tabs.Panel value="settings">
117117
<SettingsForm
118-
initialNtfyTopic={config.ntfyTopic}
119118
initialNtfyServerAddress={config.ntfyServerAddress}
120119
initialCheckInterval={config.checkIntervalMinutes}
120+
initialDefaultNtfyTopic={config.defaultNtfyTopic}
121121
onSubmit={handleSettingsSubmit}
122122
/>
123123
</Tabs.Panel>

client/src/components/NotificationSettingsModal.tsx

+37-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface NotificationSettingsModalProps {
1111

1212
export function NotificationSettingsModal({ opened, onClose, settings, onSave }: NotificationSettingsModalProps) {
1313
const [localSettings, setLocalSettings] = useState<NotificationSettings>(settings);
14+
const [isTesting, setIsTesting] = useState(false);
1415

1516
useEffect(() => {
1617
setLocalSettings(settings);
@@ -28,6 +29,25 @@ export function NotificationSettingsModal({ opened, onClose, settings, onSave }:
2829
}));
2930
};
3031

32+
const handleTestNotification = async () => {
33+
try {
34+
setIsTesting(true);
35+
await fetch('/api/test-notification', {
36+
method: 'POST',
37+
headers: {
38+
'Content-Type': 'application/json',
39+
},
40+
body: JSON.stringify({
41+
topic: localSettings.ntfyTopic
42+
})
43+
});
44+
} catch (error) {
45+
console.error('Failed to send test notification:', error);
46+
} finally {
47+
setIsTesting(false);
48+
}
49+
};
50+
3151
return (
3252
<Modal
3353
opened={opened}
@@ -74,8 +94,16 @@ export function NotificationSettingsModal({ opened, onClose, settings, onSave }:
7494
{/* Options Section */}
7595
<Paper withBorder p="md">
7696
<Stack>
77-
<Text fw={500} size="sm">Priority</Text>
97+
<Text fw={500} size="sm">Notification Settings</Text>
98+
<TextInput
99+
label="Ntfy Topic"
100+
placeholder="your-topic-name"
101+
description="Leave empty to use the default topic from settings"
102+
value={localSettings.ntfyTopic || ''}
103+
onChange={(event) => updateSettings({ ntfyTopic: event.currentTarget.value || undefined })}
104+
/>
78105
<Select
106+
label="Priority"
79107
data={[
80108
{ value: 'urgent', label: 'Urgent' },
81109
{ value: 'high', label: 'High' },
@@ -108,9 +136,14 @@ export function NotificationSettingsModal({ opened, onClose, settings, onSave }:
108136
</Stack>
109137
</Paper>
110138

111-
<Group justify="flex-end" mt="md">
112-
<Button variant="light" onClick={onClose}>Cancel</Button>
113-
<Button onClick={handleSave}>Save Changes</Button>
139+
<Group justify="space-between" mt="md">
140+
<Button onClick={handleTestNotification} loading={isTesting} variant="light">
141+
Test Notification
142+
</Button>
143+
<Group>
144+
<Button variant="light" onClick={onClose}>Cancel</Button>
145+
<Button onClick={handleSave}>Save Changes</Button>
146+
</Group>
114147
</Group>
115148
</Stack>
116149
</Modal>
+54-57
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,86 @@
1-
import { TextInput, NumberInput, Button, Stack, Group } from '@mantine/core';
1+
import { TextInput, NumberInput, Button, Stack, Group, Paper, Text } from '@mantine/core';
22
import { useForm } from '@mantine/form';
3-
import { useEffect, useState } from 'react';
3+
import { useEffect } from 'react';
44

55
interface SettingsFormProps {
6-
initialNtfyTopic: string;
76
initialNtfyServerAddress: string;
87
initialCheckInterval: number;
9-
onSubmit: (ntfyTopic: string, ntfyServerAddress: string, checkInterval: number) => void;
8+
initialDefaultNtfyTopic: string;
9+
onSubmit: (ntfyServerAddress: string, checkInterval: number, defaultNtfyTopic: string) => void;
1010
}
1111

1212
export function SettingsForm({
13-
initialNtfyTopic,
1413
initialNtfyServerAddress,
1514
initialCheckInterval,
15+
initialDefaultNtfyTopic,
1616
onSubmit,
1717
}: SettingsFormProps) {
18-
const [isTesting, setIsTesting] = useState(false);
1918
const form = useForm({
2019
initialValues: {
21-
ntfyTopic: initialNtfyTopic,
2220
ntfyServerAddress: initialNtfyServerAddress,
2321
checkInterval: initialCheckInterval,
22+
defaultNtfyTopic: initialDefaultNtfyTopic,
2423
},
2524
validate: {
26-
ntfyTopic: (value) =>
27-
value.length === 0 ? 'Ntfy topic is required' : null,
2825
ntfyServerAddress: (value) =>
29-
value.length === 0 ? 'Ntfy server address is required' : null,
26+
!value || value.trim().length === 0 ? 'Ntfy server address is required' : null,
3027
checkInterval: (value) =>
31-
value < 1 ? 'Check interval must be at least 1 minute' : null,
28+
!value || value < 1 ? 'Check interval must be at least 1 minute' : null,
29+
defaultNtfyTopic: (value) =>
30+
!value || value.trim().length === 0 ? 'Default ntfy topic is required' : null,
3231
},
3332
});
3433

3534
useEffect(() => {
3635
form.setValues({
37-
ntfyTopic: initialNtfyTopic,
3836
ntfyServerAddress: initialNtfyServerAddress,
3937
checkInterval: initialCheckInterval,
38+
defaultNtfyTopic: initialDefaultNtfyTopic,
4039
});
41-
}, [initialNtfyTopic, initialNtfyServerAddress, initialCheckInterval]);
42-
43-
const handleTestNotification = async () => {
44-
if (!form.isValid()) return;
45-
46-
try {
47-
setIsTesting(true);
48-
await fetch('/api/test-notification', { method: 'POST' });
49-
} catch (error) {
50-
console.error('Failed to send test notification:', error);
51-
} finally {
52-
setIsTesting(false);
53-
}
54-
};
40+
}, [initialNtfyServerAddress, initialCheckInterval, initialDefaultNtfyTopic]);
5541

5642
return (
57-
<Stack h="100%" style={{ flex: 1 }}>
58-
<form onSubmit={form.onSubmit((values) => onSubmit(values.ntfyTopic, values.ntfyServerAddress, values.checkInterval))}>
59-
<Stack>
60-
<TextInput
61-
label="Ntfy Server Address"
62-
placeholder="https://ntfy.sh"
63-
required
64-
{...form.getInputProps('ntfyServerAddress')}
65-
/>
66-
<TextInput
67-
label="Ntfy Topic"
68-
placeholder="your-topic-name"
69-
required
70-
{...form.getInputProps('ntfyTopic')}
71-
/>
72-
<NumberInput
73-
label="Check Interval (minutes)"
74-
placeholder="15"
75-
min={1}
76-
required
77-
{...form.getInputProps('checkInterval')}
78-
/>
79-
<Group justify="space-between">
80-
<Button onClick={handleTestNotification} loading={isTesting} variant="light">
81-
Test Notification
82-
</Button>
83-
<Button type="submit">Save Settings</Button>
84-
</Group>
85-
</Stack>
86-
</form>
87-
</Stack>
43+
<form onSubmit={form.onSubmit((values) => onSubmit(values.ntfyServerAddress, values.checkInterval, values.defaultNtfyTopic))}>
44+
<Stack>
45+
{/* Notification Settings Section */}
46+
<Paper withBorder p="md">
47+
<Stack>
48+
<Text fw={500} size="sm">Ntfy Settings</Text>
49+
<TextInput
50+
label="Server Address"
51+
placeholder="https://ntfy.sh"
52+
required
53+
{...form.getInputProps('ntfyServerAddress')}
54+
/>
55+
<TextInput
56+
label="Default Topic"
57+
placeholder="rss"
58+
description="Used when a feed doesn't have a specific topic configured"
59+
required
60+
{...form.getInputProps('defaultNtfyTopic')}
61+
/>
62+
</Stack>
63+
</Paper>
64+
65+
{/* Feed Check Settings Section */}
66+
<Paper withBorder p="md">
67+
<Stack>
68+
<Text fw={500} size="sm">Monitoring Settings</Text>
69+
<NumberInput
70+
label="Check Interval"
71+
description="How often to check feeds for new posts (in minutes)"
72+
placeholder="15"
73+
min={1}
74+
required
75+
{...form.getInputProps('checkInterval')}
76+
/>
77+
</Stack>
78+
</Paper>
79+
80+
<Group justify="flex-end" mt="md">
81+
<Button type="submit">Save Settings</Button>
82+
</Group>
83+
</Stack>
84+
</form>
8885
);
8986
}

server/src/config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
88

99
const DEFAULT_CONFIG: AppConfig = {
1010
feeds: [],
11-
ntfyTopic: '',
1211
ntfyServerAddress: 'https://ntfy.sh',
13-
checkIntervalMinutes: 15
12+
checkIntervalMinutes: 15,
13+
defaultNtfyTopic: 'rss',
1414
};
1515

1616
export class ConfigManager {

server/src/feedMonitor.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,18 @@ export class FeedMonitor {
154154

155155
private async sendNotification(item: FeedItem, matchedKeywords: string[], feedConfig: FeedConfig) {
156156
const config = this.configManager.getConfig();
157-
const ntfyUrl = `${config.ntfyServerAddress}/${config.ntfyTopic}`;
158157
const settings = feedConfig.notificationSettings;
158+
const ntfyTopic = settings.ntfyTopic || config.defaultNtfyTopic;
159+
const ntfyUrl = `${config.ntfyServerAddress}/${ntfyTopic}`;
159160

160161
// Determine title
161-
const title = settings.usePostTitle ? item.title : (settings.customTitle || item.title);
162+
const title = settings.usePostTitle ? item.title : (settings.customTitle || "");
162163
const sanitizedTitle = title.replace(/[^\x20-\x7E]/g, '');
163164

164165
// Determine description
165-
let description = settings.usePostDescription ? item.description : (settings.customDescription || item.description);
166+
let description = settings.usePostDescription ? item.description : (settings.customDescription || "");
166167
if (settings.appendLink && item.link) {
167-
description = `${description}\n\nLink: ${item.link}`;
168+
description = `${description}\n\n${item.link}`;
168169
}
169170

170171
try {
@@ -207,10 +208,10 @@ export class FeedMonitor {
207208
}
208209

209210
public updateConfig() {
211+
const config = this.configManager.getConfig();
210212
this.setupCronJob();
211213

212214
// Get current config and URLs
213-
const config = this.configManager.getConfig();
214215
const configuredUrls = new Set(config.feeds.map((feed: FeedConfig) => feed.url));
215216

216217
// Remove status entries for feeds that are no longer in the config
@@ -246,7 +247,7 @@ export class FeedMonitor {
246247
return this.postHistoryManager.getPostHistoryEntries();
247248
}
248249

249-
public async sendTestNotification(): Promise<void> {
250+
public async sendTestNotification(topic?: string): Promise<void> {
250251
const config = this.configManager.getConfig();
251252
const testItem: FeedItem = {
252253
title: "Test Notification",
@@ -258,19 +259,20 @@ export class FeedMonitor {
258259
pubDate: new Date().toISOString()
259260
};
260261

261-
const testFeedConfig: FeedConfig = {
262+
const testConfig: FeedConfig = {
262263
url: "test",
263-
keywords: ["test"],
264+
keywords: [],
264265
notificationSettings: {
265266
usePostTitle: true,
266267
usePostDescription: true,
267268
appendLink: true,
268269
priority: 'default',
269270
includeKeywordTags: true,
270271
includeOpenAction: true,
272+
ntfyTopic: topic || config.defaultNtfyTopic
271273
}
272274
};
273275

274-
await this.sendNotification(testItem, ["test"], testFeedConfig);
276+
await this.sendNotification(testItem, ["keyword1", "keyword2"], testConfig);
275277
}
276278
}

server/src/server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ export class Server {
112112
});
113113

114114
// Send test notification
115-
this.app.post('/api/test-notification', async (_req, res) => {
115+
this.app.post('/api/test-notification', async (req, res) => {
116116
try {
117-
await this.feedMonitor.sendTestNotification();
117+
await this.feedMonitor.sendTestNotification(req.body.topic);
118118
res.json({ success: true });
119119
} catch (error) {
120120
console.error('Failed to send test notification:', error);

server/src/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Parser from 'rss-parser';
2-
import { FeedConfig, NotificationSettings, NtfyPriority } from '../../shared/types.js';
2+
import { FeedConfig, NotificationSettings, NtfyPriority } from 'shared/types.js';
33

44
export { FeedConfig, NotificationSettings, NtfyPriority };
55

@@ -44,9 +44,9 @@ export interface FeedHistoryEntry extends FeedItem {
4444

4545
export interface AppConfig {
4646
feeds: FeedConfig[];
47-
ntfyTopic: string;
4847
ntfyServerAddress: string;
4948
checkIntervalMinutes: number;
49+
defaultNtfyTopic: string;
5050
}
5151

5252
export interface PostHistory {

0 commit comments

Comments
 (0)