feat(notifications): update Telegram bot support and update settings management

This commit is contained in:
Yusuf İpek
2026-03-01 17:28:25 +03:00
parent f011ce4f05
commit 147e5520a3
6 changed files with 74 additions and 54 deletions
+4
View File
@@ -44,6 +44,10 @@ R2_BUCKET_NAME="openframe"
# EMAIL & NOTIFICATIONS # EMAIL & NOTIFICATIONS
# ============================================================================ # ============================================================================
# Telegram bot token from @BotFather — shared across all users
# Users only need to provide their own Chat ID in Settings
TELEGRAM_BOT_TOKEN="your-telegram-bot-token"
SMTP_HOST="smtp.gmail.com" SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587" SMTP_PORT="587"
SMTP_USER="[email protected]" SMTP_USER="[email protected]"
@@ -21,7 +21,6 @@ import {
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
interface NotificationSettings { interface NotificationSettings {
telegramBotToken: string | null;
telegramChatId: string | null; telegramChatId: string | null;
telegramEnabled: boolean; telegramEnabled: boolean;
emailEnabled: boolean; emailEnabled: boolean;
@@ -80,7 +79,6 @@ function ToggleButton({
export default function SettingsPage() { export default function SettingsPage() {
const [settings, setSettings] = useState<NotificationSettings>({ const [settings, setSettings] = useState<NotificationSettings>({
telegramBotToken: null,
telegramChatId: null, telegramChatId: null,
telegramEnabled: false, telegramEnabled: false,
emailEnabled: false, emailEnabled: false,
@@ -96,8 +94,7 @@ export default function SettingsPage() {
const [testing, setTesting] = useState<string | null>(null); const [testing, setTesting] = useState<string | null>(null);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
// Form state for Telegram fields (separate from saved settings for editing) // Form state for Telegram chat ID (separate from saved settings for editing)
const [telegramToken, setTelegramToken] = useState('');
const [telegramChatId, setTelegramChatId] = useState(''); const [telegramChatId, setTelegramChatId] = useState('');
useEffect(() => { useEffect(() => {
@@ -107,7 +104,6 @@ export default function SettingsPage() {
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setSettings(data.data); setSettings(data.data);
setTelegramToken(data.data.telegramBotToken || '');
setTelegramChatId(data.data.telegramChatId || ''); setTelegramChatId(data.data.telegramChatId || '');
} }
} catch { } catch {
@@ -132,7 +128,6 @@ export default function SettingsPage() {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
...settings, ...settings,
telegramBotToken: telegramToken || null,
telegramChatId: telegramChatId || null, telegramChatId: telegramChatId || null,
}), }),
}); });
@@ -150,7 +145,7 @@ export default function SettingsPage() {
} finally { } finally {
setSaving(false); setSaving(false);
} }
}, [settings, telegramToken, telegramChatId, showMessage]); }, [settings, telegramChatId, showMessage]);
const handleTest = useCallback( const handleTest = useCallback(
async (channel: 'telegram' | 'email') => { async (channel: 'telegram' | 'email') => {
@@ -161,7 +156,6 @@ export default function SettingsPage() {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
channel, channel,
telegramBotToken: telegramToken,
telegramChatId, telegramChatId,
}), }),
}); });
@@ -177,7 +171,7 @@ export default function SettingsPage() {
setTesting(null); setTesting(null);
} }
}, },
[telegramToken, telegramChatId, showMessage] [telegramChatId, showMessage]
); );
if (loading) { if (loading) {
@@ -309,35 +303,52 @@ export default function SettingsPage() {
</Badge> </Badge>
</div> </div>
<CardDescription> <CardDescription>
Get instant notifications via a Telegram bot Get instant notifications via Telegram
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-3"> <div className="rounded-md border bg-muted/40 p-3 space-y-2 text-sm text-muted-foreground">
<div> <p className="font-medium text-foreground">Setup instructions</p>
<Label htmlFor="telegram-token">Bot Token</Label> <ol className="space-y-1.5 list-decimal list-inside">
<Input <li>
id="telegram-token" Message{' '}
type="password" <a
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" href="https://t.me/UserInfeBot"
value={telegramToken} target="_blank"
onChange={(e) => setTelegramToken(e.target.value)} rel="noopener noreferrer"
className="mt-1 font-mono text-sm" className="text-primary underline underline-offset-2"
/> >
@UserInfeBot
</a>
{' '}on Telegram and send <code className="bg-muted px-1 rounded text-xs">/start</code> to get your Chat ID
</li>
<li>
Start{' '}
<a
href="https://t.me/openframe_bot"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline underline-offset-2"
>
@openframe_bot
</a>
{' '}and send <code className="bg-muted px-1 rounded text-xs">/start</code> so it can message you
</li>
<li>Paste your Chat ID below and enable notifications</li>
</ol>
</div> </div>
<div> <div>
<Label htmlFor="telegram-chat-id">Chat ID</Label> <Label htmlFor="telegram-chat-id">Your Chat ID</Label>
<Input <Input
id="telegram-chat-id" id="telegram-chat-id"
placeholder="-1001234567890" placeholder="123456789"
value={telegramChatId} value={telegramChatId}
onChange={(e) => setTelegramChatId(e.target.value)} onChange={(e) => setTelegramChatId(e.target.value)}
className="mt-1 font-mono text-sm" className="mt-1 font-mono text-sm"
/> />
</div> </div>
</div>
<div className="flex items-center gap-2">
<ToggleButton <ToggleButton
enabled={settings.telegramEnabled} enabled={settings.telegramEnabled}
onToggle={() => onToggle={() =>
@@ -345,13 +356,12 @@ export default function SettingsPage() {
} }
label="Enable Telegram notifications" label="Enable Telegram notifications"
/> />
</div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => handleTest('telegram')} onClick={() => handleTest('telegram')}
disabled={!telegramToken || !telegramChatId || testing === 'telegram'} disabled={!telegramChatId || testing === 'telegram'}
> >
{testing === 'telegram' ? ( {testing === 'telegram' ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" /> <Loader2 className="h-4 w-4 animate-spin mr-2" />
+16 -10
View File
@@ -21,7 +21,6 @@ export async function GET() {
// Return defaults if no settings exist yet // Return defaults if no settings exist yet
const response = successResponse( const response = successResponse(
settings ?? { settings ?? {
telegramBotToken: null,
telegramChatId: null, telegramChatId: null,
telegramEnabled: false, telegramEnabled: false,
emailEnabled: false, emailEnabled: false,
@@ -54,7 +53,6 @@ export async function PUT(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const { const {
telegramBotToken,
telegramChatId, telegramChatId,
telegramEnabled, telegramEnabled,
emailEnabled, emailEnabled,
@@ -66,16 +64,18 @@ export async function PUT(request: NextRequest) {
timezone, timezone,
} = body; } = body;
// Validate: if enabling Telegram, both token and chatId are required // Validate: if enabling Telegram, chatId is required and must be a valid Telegram ID
if (telegramEnabled && (!telegramBotToken || !telegramChatId)) { if (telegramChatId && !/^-?\d{1,20}$/.test(telegramChatId)) {
return apiErrors.badRequest('Telegram Bot Token and Chat ID are required to enable Telegram notifications'); return apiErrors.badRequest('Invalid Chat ID format');
}
if (telegramEnabled && !telegramChatId) {
return apiErrors.badRequest('Chat ID is required to enable Telegram notifications');
} }
const settings = await db.notificationSetting.upsert({ const settings = await db.notificationSetting.upsert({
where: { userId: session.user.id }, where: { userId: session.user.id },
create: { create: {
userId: session.user.id, userId: session.user.id,
telegramBotToken: telegramBotToken || null,
telegramChatId: telegramChatId || null, telegramChatId: telegramChatId || null,
telegramEnabled: !!telegramEnabled, telegramEnabled: !!telegramEnabled,
emailEnabled: !!emailEnabled, emailEnabled: !!emailEnabled,
@@ -87,7 +87,6 @@ export async function PUT(request: NextRequest) {
timezone: timezone || 'UTC', timezone: timezone || 'UTC',
}, },
update: { update: {
telegramBotToken: telegramBotToken || null,
telegramChatId: telegramChatId || null, telegramChatId: telegramChatId || null,
telegramEnabled: !!telegramEnabled, telegramEnabled: !!telegramEnabled,
emailEnabled: !!emailEnabled, emailEnabled: !!emailEnabled,
@@ -120,11 +119,18 @@ export async function POST(request: NextRequest) {
} }
const body = await request.json(); const body = await request.json();
const { channel, telegramBotToken, telegramChatId } = body; const { channel, telegramChatId } = body;
if (channel === 'telegram') { if (channel === 'telegram') {
if (!telegramBotToken || !telegramChatId) { const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
return apiErrors.badRequest('Bot Token and Chat ID are required'); if (!telegramBotToken) {
return apiErrors.internalError('Telegram bot not configured (TELEGRAM_BOT_TOKEN missing)');
}
if (!telegramChatId) {
return apiErrors.badRequest('Chat ID is required');
}
if (!/^-?\d{1,20}$/.test(telegramChatId)) {
return apiErrors.badRequest('Invalid Chat ID format');
} }
const settingsUrl = `${process.env.NEXTAUTH_URL || ''}/settings`; const settingsUrl = `${process.env.NEXTAUTH_URL || ''}/settings`;
+3 -2
View File
@@ -456,10 +456,11 @@ export async function notifyUsers(userIds: string[], event: NotificationEvent):
const promises: Promise<boolean>[] = []; const promises: Promise<boolean>[] = [];
const tz = settings.timezone || 'UTC'; const tz = settings.timezone || 'UTC';
if (settings.telegramEnabled && settings.telegramBotToken && settings.telegramChatId) { const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
if (settings.telegramEnabled && telegramBotToken && settings.telegramChatId) {
const msg = formatTelegramMessage(event, tz); const msg = formatTelegramMessage(event, tz);
promises.push(sendTelegram( promises.push(sendTelegram(
settings.telegramBotToken, telegramBotToken,
settings.telegramChatId, settings.telegramChatId,
msg.text, msg.text,
msg.buttonLabel, msg.buttonLabel,
+1 -2
View File
@@ -610,8 +610,7 @@ model NotificationSetting {
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Telegram webhook // Telegram webhook
telegramBotToken String? // Bot token from @BotFather telegramChatId String? // Chat ID to send messages to (bot token is in TELEGRAM_BOT_TOKEN env var)
telegramChatId String? // Chat/group ID to send messages to
telegramEnabled Boolean @default(false) telegramEnabled Boolean @default(false)
// Email notifications (uses account email by default) // Email notifications (uses account email by default)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB