feat: Переделка Telegram интеграции на единого бота (v2.1.0)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 54s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 54s
- Единый бот для всех пользователей (токен из .env) - Deep link для подключения через /start команду - Отдельная таблица todoist_integrations для Todoist webhook - Персональные отчеты для каждого пользователя - Автоматическое применение миграции 012 при старте - Обновлен Frontend: кнопка подключения вместо поля ввода токена
This commit is contained in:
@@ -28,8 +28,10 @@ WEB_PORT=3001
|
|||||||
# ============================================
|
# ============================================
|
||||||
# Telegram Bot Configuration
|
# Telegram Bot Configuration
|
||||||
# ============================================
|
# ============================================
|
||||||
# Bot Token и Chat ID настраиваются через UI приложения в разделе "Интеграции" -> "Telegram"
|
# Токен единого бота для всех пользователей
|
||||||
# Get token from @BotFather in Telegram: https://t.me/botfather
|
# Получить у @BotFather: https://t.me/botfather
|
||||||
|
TELEGRAM_BOT_TOKEN=your-bot-token-here
|
||||||
|
|
||||||
# Base URL для автоматической настройки webhook
|
# Base URL для автоматической настройки webhook
|
||||||
# Примеры:
|
# Примеры:
|
||||||
# - Для production с HTTPS: https://your-domain.com
|
# - Для production с HTTPS: https://your-domain.com
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
|||||||
|
-- Migration: Refactor telegram_integrations for single shared bot
|
||||||
|
-- and move Todoist webhook_token to separate table
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 1. Создаем таблицу todoist_integrations
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS todoist_integrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
webhook_token VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT todoist_integrations_user_id_unique UNIQUE (user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_todoist_integrations_webhook_token
|
||||||
|
ON todoist_integrations(webhook_token);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_todoist_integrations_user_id
|
||||||
|
ON todoist_integrations(user_id);
|
||||||
|
|
||||||
|
COMMENT ON TABLE todoist_integrations IS 'Todoist webhook integration settings per user';
|
||||||
|
COMMENT ON COLUMN todoist_integrations.webhook_token IS 'Unique token for Todoist webhook URL';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 2. Мигрируем webhook_token из telegram_integrations в todoist_integrations
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO todoist_integrations (user_id, webhook_token, created_at, updated_at)
|
||||||
|
SELECT user_id, webhook_token, COALESCE(created_at, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP
|
||||||
|
FROM telegram_integrations
|
||||||
|
WHERE webhook_token IS NOT NULL
|
||||||
|
AND webhook_token != ''
|
||||||
|
AND user_id IS NOT NULL
|
||||||
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 3. Модифицируем telegram_integrations
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Удаляем bot_token (будет в .env)
|
||||||
|
ALTER TABLE telegram_integrations
|
||||||
|
DROP COLUMN IF EXISTS bot_token;
|
||||||
|
|
||||||
|
-- Удаляем webhook_token (перенесли в todoist_integrations)
|
||||||
|
ALTER TABLE telegram_integrations
|
||||||
|
DROP COLUMN IF EXISTS webhook_token;
|
||||||
|
|
||||||
|
-- Добавляем telegram_user_id
|
||||||
|
ALTER TABLE telegram_integrations
|
||||||
|
ADD COLUMN IF NOT EXISTS telegram_user_id BIGINT;
|
||||||
|
|
||||||
|
-- Добавляем start_token для deep links
|
||||||
|
ALTER TABLE telegram_integrations
|
||||||
|
ADD COLUMN IF NOT EXISTS start_token VARCHAR(255);
|
||||||
|
|
||||||
|
-- Добавляем timestamps если их нет
|
||||||
|
ALTER TABLE telegram_integrations
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
ALTER TABLE telegram_integrations
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 4. Создаем индексы
|
||||||
|
-- ============================================
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_telegram_integrations_start_token
|
||||||
|
ON telegram_integrations(start_token)
|
||||||
|
WHERE start_token IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_telegram_integrations_telegram_user_id
|
||||||
|
ON telegram_integrations(telegram_user_id)
|
||||||
|
WHERE telegram_user_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Уникальность user_id
|
||||||
|
DROP INDEX IF EXISTS idx_telegram_integrations_user_id;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_telegram_integrations_user_id_unique
|
||||||
|
ON telegram_integrations(user_id)
|
||||||
|
WHERE user_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Индекс для поиска по chat_id
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_telegram_integrations_chat_id
|
||||||
|
ON telegram_integrations(chat_id)
|
||||||
|
WHERE chat_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Удаляем старый индекс webhook_token
|
||||||
|
DROP INDEX IF EXISTS idx_telegram_integrations_webhook_token;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 5. Очищаем данные Telegram для переподключения
|
||||||
|
-- ============================================
|
||||||
|
UPDATE telegram_integrations
|
||||||
|
SET chat_id = NULL,
|
||||||
|
telegram_user_id = NULL,
|
||||||
|
start_token = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Комментарии
|
||||||
|
-- ============================================
|
||||||
|
COMMENT ON COLUMN telegram_integrations.telegram_user_id IS 'Telegram user ID (message.from.id)';
|
||||||
|
COMMENT ON COLUMN telegram_integrations.chat_id IS 'Telegram chat ID для отправки сообщений';
|
||||||
|
COMMENT ON COLUMN telegram_integrations.start_token IS 'Временный токен для deep link при первом подключении';
|
||||||
|
|
||||||
@@ -4,12 +4,9 @@ import './Integrations.css'
|
|||||||
|
|
||||||
function TelegramIntegration({ onBack }) {
|
function TelegramIntegration({ onBack }) {
|
||||||
const { authFetch } = useAuth()
|
const { authFetch } = useAuth()
|
||||||
const [botToken, setBotToken] = useState('')
|
const [integration, setIntegration] = useState(null)
|
||||||
const [chatId, setChatId] = useState('')
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [success, setSuccess] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchIntegration()
|
fetchIntegration()
|
||||||
@@ -23,8 +20,7 @@ function TelegramIntegration({ onBack }) {
|
|||||||
throw new Error('Ошибка при загрузке интеграции')
|
throw new Error('Ошибка при загрузке интеграции')
|
||||||
}
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setBotToken(data.bot_token || '')
|
setIntegration(data)
|
||||||
setChatId(data.chat_id || '')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching integration:', error)
|
console.error('Error fetching integration:', error)
|
||||||
setError('Не удалось загрузить данные интеграции')
|
setError('Не удалось загрузить данные интеграции')
|
||||||
@@ -33,40 +29,22 @@ function TelegramIntegration({ onBack }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleOpenBot = () => {
|
||||||
if (!botToken.trim()) {
|
if (integration?.deep_link) {
|
||||||
setError('Bot Token обязателен для заполнения')
|
window.open(integration.deep_link, '_blank')
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const handleRefresh = () => {
|
||||||
setSaving(true)
|
fetchIntegration()
|
||||||
setError('')
|
}
|
||||||
setSuccess('')
|
|
||||||
|
|
||||||
const response = await authFetch('/api/integrations/telegram', {
|
if (loading) {
|
||||||
method: 'POST',
|
return (
|
||||||
headers: {
|
<div className="p-4 md:p-6">
|
||||||
'Content-Type': 'application/json',
|
<div className="text-gray-500">Загрузка...</div>
|
||||||
},
|
</div>
|
||||||
body: JSON.stringify({ bot_token: botToken }),
|
)
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json()
|
|
||||||
throw new Error(errorData.error || 'Ошибка при сохранении')
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
setBotToken(data.bot_token || '')
|
|
||||||
setChatId(data.chat_id || '')
|
|
||||||
setSuccess('Bot Token успешно сохранен!')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving integration:', error)
|
|
||||||
setError(error.message || 'Не удалось сохранить Bot Token')
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -77,96 +55,77 @@ function TelegramIntegration({ onBack }) {
|
|||||||
|
|
||||||
<h1 className="text-2xl font-bold mb-6">Telegram интеграция</h1>
|
<h1 className="text-2xl font-bold mb-6">Telegram интеграция</h1>
|
||||||
|
|
||||||
{loading ? (
|
{error && (
|
||||||
<div className="text-gray-500">Загрузка...</div>
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
||||||
) : (
|
{error}
|
||||||
<>
|
</div>
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
)}
|
||||||
<h2 className="text-lg font-semibold mb-4">Настройки</h2>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<h2 className="text-lg font-semibold mb-4">Статус подключения</h2>
|
||||||
Telegram Bot Token
|
|
||||||
</label>
|
{integration?.is_connected ? (
|
||||||
<input
|
<div className="space-y-4">
|
||||||
type="text"
|
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
value={botToken}
|
<div className="flex items-center text-green-700">
|
||||||
onChange={(e) => setBotToken(e.target.value)}
|
<span className="text-xl mr-2">✓</span>
|
||||||
placeholder="Введите Bot Token"
|
<span className="font-medium">Telegram подключен</span>
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{chatId && (
|
{integration.telegram_user_id && (
|
||||||
<div className="mb-4">
|
<div className="text-sm text-gray-600">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
Telegram ID: <span className="font-mono">{integration.telegram_user_id}</span>
|
||||||
Chat ID (устанавливается автоматически)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={chatId}
|
|
||||||
readOnly
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{success && (
|
|
||||||
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
|
|
||||||
{success}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleOpenBot}
|
||||||
disabled={saving || !botToken.trim()}
|
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
|
||||||
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
||||||
>
|
>
|
||||||
{saving ? 'Сохранение...' : 'Сохранить Bot Token'}
|
Открыть бота
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<div className="flex items-center text-yellow-700">
|
||||||
|
<span className="text-xl mr-2">⚠</span>
|
||||||
|
<span className="font-medium">Telegram не подключен</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-gray-600">
|
||||||
|
Нажмите кнопку ниже и отправьте команду /start в боте
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
|
<button
|
||||||
<h3 className="text-lg font-semibold mb-3 text-blue-900">
|
onClick={handleOpenBot}
|
||||||
Откуда взять Bot Token
|
disabled={!integration?.deep_link}
|
||||||
</h3>
|
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||||
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
>
|
||||||
<li>Откройте Telegram и найдите бота @BotFather</li>
|
Подключить Telegram
|
||||||
<li>Отправьте команду /newbot</li>
|
</button>
|
||||||
<li>Следуйте инструкциям для создания нового бота</li>
|
|
||||||
<li>
|
|
||||||
После создания бота BotFather предоставит вам Bot Token
|
|
||||||
</li>
|
|
||||||
<li>Скопируйте токен и вставьте его в поле выше</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
<button
|
||||||
<h3 className="text-lg font-semibold mb-3 text-yellow-900">
|
onClick={handleRefresh}
|
||||||
Что нужно сделать после сохранения Bot Token
|
className="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||||
</h3>
|
>
|
||||||
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
Проверить подключение
|
||||||
<li>После сохранения Bot Token отправьте первое сообщение вашему боту в Telegram</li>
|
</button>
|
||||||
<li>
|
|
||||||
Chat ID будет автоматически сохранен после обработки первого
|
|
||||||
сообщения
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
После этого бот сможет отправлять вам ответные сообщения
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-blue-900">Инструкция</h3>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
||||||
|
<li>Нажмите кнопку "Подключить Telegram"</li>
|
||||||
|
<li>В открывшемся Telegram нажмите "Start" или отправьте /start</li>
|
||||||
|
<li>Вернитесь сюда и нажмите "Проверить подключение"</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TelegramIntegration
|
export default TelegramIntegration
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user