mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
feat(notifications): update Telegram bot support and update settings management
This commit is contained in:
@@ -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,49 +303,65 @@ 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"
|
||||||
/>
|
>
|
||||||
</div>
|
@UserInfeBot
|
||||||
<div>
|
</a>
|
||||||
<Label htmlFor="telegram-chat-id">Chat ID</Label>
|
{' '}on Telegram and send <code className="bg-muted px-1 rounded text-xs">/start</code> to get your Chat ID
|
||||||
<Input
|
</li>
|
||||||
id="telegram-chat-id"
|
<li>
|
||||||
placeholder="-1001234567890"
|
Start{' '}
|
||||||
value={telegramChatId}
|
<a
|
||||||
onChange={(e) => setTelegramChatId(e.target.value)}
|
href="https://t.me/openframe_bot"
|
||||||
className="mt-1 font-mono text-sm"
|
target="_blank"
|
||||||
/>
|
rel="noopener noreferrer"
|
||||||
</div>
|
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 className="flex items-center gap-2">
|
<div>
|
||||||
<ToggleButton
|
<Label htmlFor="telegram-chat-id">Your Chat ID</Label>
|
||||||
enabled={settings.telegramEnabled}
|
<Input
|
||||||
onToggle={() =>
|
id="telegram-chat-id"
|
||||||
setSettings((s) => ({ ...s, telegramEnabled: !s.telegramEnabled }))
|
placeholder="123456789"
|
||||||
}
|
value={telegramChatId}
|
||||||
label="Enable Telegram notifications"
|
onChange={(e) => setTelegramChatId(e.target.value)}
|
||||||
|
className="mt-1 font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
enabled={settings.telegramEnabled}
|
||||||
|
onToggle={() =>
|
||||||
|
setSettings((s) => ({ ...s, telegramEnabled: !s.telegramEnabled }))
|
||||||
|
}
|
||||||
|
label="Enable Telegram notifications"
|
||||||
|
/>
|
||||||
|
|
||||||
<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" />
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Reference in New Issue
Block a user