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
# ============================================================================
# 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_PORT="587"
SMTP_USER="[email protected]"
@@ -21,7 +21,6 @@ import {
import { cn } from '@/lib/utils';
interface NotificationSettings {
telegramBotToken: string | null;
telegramChatId: string | null;
telegramEnabled: boolean;
emailEnabled: boolean;
@@ -80,7 +79,6 @@ function ToggleButton({
export default function SettingsPage() {
const [settings, setSettings] = useState<NotificationSettings>({
telegramBotToken: null,
telegramChatId: null,
telegramEnabled: false,
emailEnabled: false,
@@ -96,8 +94,7 @@ export default function SettingsPage() {
const [testing, setTesting] = useState<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)
const [telegramToken, setTelegramToken] = useState('');
// Form state for Telegram chat ID (separate from saved settings for editing)
const [telegramChatId, setTelegramChatId] = useState('');
useEffect(() => {
@@ -107,7 +104,6 @@ export default function SettingsPage() {
if (res.ok) {
const data = await res.json();
setSettings(data.data);
setTelegramToken(data.data.telegramBotToken || '');
setTelegramChatId(data.data.telegramChatId || '');
}
} catch {
@@ -132,7 +128,6 @@ export default function SettingsPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...settings,
telegramBotToken: telegramToken || null,
telegramChatId: telegramChatId || null,
}),
});
@@ -150,7 +145,7 @@ export default function SettingsPage() {
} finally {
setSaving(false);
}
}, [settings, telegramToken, telegramChatId, showMessage]);
}, [settings, telegramChatId, showMessage]);
const handleTest = useCallback(
async (channel: 'telegram' | 'email') => {
@@ -161,7 +156,6 @@ export default function SettingsPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel,
telegramBotToken: telegramToken,
telegramChatId,
}),
});
@@ -177,7 +171,7 @@ export default function SettingsPage() {
setTesting(null);
}
},
[telegramToken, telegramChatId, showMessage]
[telegramChatId, showMessage]
);
if (loading) {
@@ -309,35 +303,52 @@ export default function SettingsPage() {
</Badge>
</div>
<CardDescription>
Get instant notifications via a Telegram bot
Get instant notifications via Telegram
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div>
<Label htmlFor="telegram-token">Bot Token</Label>
<Input
id="telegram-token"
type="password"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
value={telegramToken}
onChange={(e) => setTelegramToken(e.target.value)}
className="mt-1 font-mono text-sm"
/>
<div className="rounded-md border bg-muted/40 p-3 space-y-2 text-sm text-muted-foreground">
<p className="font-medium text-foreground">Setup instructions</p>
<ol className="space-y-1.5 list-decimal list-inside">
<li>
Message{' '}
<a
href="https://t.me/UserInfeBot"
target="_blank"
rel="noopener noreferrer"
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>
<Label htmlFor="telegram-chat-id">Chat ID</Label>
<Label htmlFor="telegram-chat-id">Your Chat ID</Label>
<Input
id="telegram-chat-id"
placeholder="-1001234567890"
placeholder="123456789"
value={telegramChatId}
onChange={(e) => setTelegramChatId(e.target.value)}
className="mt-1 font-mono text-sm"
/>
</div>
</div>
<div className="flex items-center gap-2">
<ToggleButton
enabled={settings.telegramEnabled}
onToggle={() =>
@@ -345,13 +356,12 @@ export default function SettingsPage() {
}
label="Enable Telegram notifications"
/>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleTest('telegram')}
disabled={!telegramToken || !telegramChatId || testing === 'telegram'}
disabled={!telegramChatId || testing === 'telegram'}
>
{testing === 'telegram' ? (
<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
const response = successResponse(
settings ?? {
telegramBotToken: null,
telegramChatId: null,
telegramEnabled: false,
emailEnabled: false,
@@ -54,7 +53,6 @@ export async function PUT(request: NextRequest) {
const body = await request.json();
const {
telegramBotToken,
telegramChatId,
telegramEnabled,
emailEnabled,
@@ -66,16 +64,18 @@ export async function PUT(request: NextRequest) {
timezone,
} = body;
// Validate: if enabling Telegram, both token and chatId are required
if (telegramEnabled && (!telegramBotToken || !telegramChatId)) {
return apiErrors.badRequest('Telegram Bot Token and Chat ID are required to enable Telegram notifications');
// Validate: if enabling Telegram, chatId is required and must be a valid Telegram ID
if (telegramChatId && !/^-?\d{1,20}$/.test(telegramChatId)) {
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({
where: { userId: session.user.id },
create: {
userId: session.user.id,
telegramBotToken: telegramBotToken || null,
telegramChatId: telegramChatId || null,
telegramEnabled: !!telegramEnabled,
emailEnabled: !!emailEnabled,
@@ -87,7 +87,6 @@ export async function PUT(request: NextRequest) {
timezone: timezone || 'UTC',
},
update: {
telegramBotToken: telegramBotToken || null,
telegramChatId: telegramChatId || null,
telegramEnabled: !!telegramEnabled,
emailEnabled: !!emailEnabled,
@@ -120,11 +119,18 @@ export async function POST(request: NextRequest) {
}
const body = await request.json();
const { channel, telegramBotToken, telegramChatId } = body;
const { channel, telegramChatId } = body;
if (channel === 'telegram') {
if (!telegramBotToken || !telegramChatId) {
return apiErrors.badRequest('Bot Token and Chat ID are required');
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
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`;
+3 -2
View File
@@ -456,10 +456,11 @@ export async function notifyUsers(userIds: string[], event: NotificationEvent):
const promises: Promise<boolean>[] = [];
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);
promises.push(sendTelegram(
settings.telegramBotToken,
telegramBotToken,
settings.telegramChatId,
msg.text,
msg.buttonLabel,
+1 -2
View File
@@ -610,8 +610,7 @@ model NotificationSetting {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Telegram webhook
telegramBotToken String? // Bot token from @BotFather
telegramChatId String? // Chat/group ID to send messages to
telegramChatId String? // Chat ID to send messages to (bot token is in TELEGRAM_BOT_TOKEN env var)
telegramEnabled Boolean @default(false)
// Email notifications (uses account email by default)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB