const Parser = require('rss-parser');
const axios = require('axios');
const { aiConfig } = require('../config');
const rssParser = new Parser({
timeout: 10000,
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; KZBot/1.0)' },
customFields: { item: ['quant', 'change', 'description'] }
});
function log(msg) {
const ts = new Date().toLocaleString('en-CA', { timeZone: 'America/Vancouver' });
console.log(`[${ts}] ${msg}`);
}
const NB_RATES_URL = 'https://nationalbank.kz/rss/rates_all.xml';
// Валюты для поста (код → флаг)
const WATCH = [
{ code: 'USD', flag: '🇺🇸', name: 'Доллар' },
{ code: 'EUR', flag: '🇪🇺', name: 'Евро' },
{ code: 'RUB', flag: '🇷🇺', name: 'Рубль' },
{ code: 'CNY', flag: '🇨🇳', name: 'Юань' }
];
// Дата по Казахстану в формате "13 июня"
function kzDateLabel() {
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
const s = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Almaty' }); // YYYY-MM-DD
const [, m, d] = s.split('-');
return `${parseInt(d, 10)} ${months[parseInt(m, 10) - 1]}`;
}
// Курс валют от Нацбанка РК → готовый пост (или null при сбое)
async function getCurrencyPost(hashtags = []) {
try {
const feed = await rssParser.parseURL(NB_RATES_URL);
const byCode = {};
for (const item of feed.items) {
if (item.title) byCode[item.title.trim().toUpperCase()] = item;
}
const lines = [];
for (const w of WATCH) {
const it = byCode[w.code];
if (!it) continue;
const val = parseFloat(it.description);
if (!isFinite(val)) continue;
const ch = parseFloat(it.change);
let arrow = '';
if (isFinite(ch) && ch > 0) arrow = ` 🔺${ch.toFixed(2)}`;
else if (isFinite(ch) && ch < 0) arrow = ` 🔻${Math.abs(ch).toFixed(2)}`;
lines.push(`${w.flag} ${w.code} — <b>${val.toFixed(2)} ₸</b>${arrow}`);
}
if (lines.length === 0) return null;
let text = `💱 <b>Курс валют на ${kzDateLabel()}</b>\n\n${lines.join('\n')}\n\n`;
text += `Официальный курс Нацбанка РК. Как вам тенге сегодня? 👇`;
if (hashtags.length > 0) text += `\n\n${hashtags.join(' ')}`;
return text;
} catch (err) {
log(`❌ Курс валют: ${err.message}`);
return null;
}
}
// --- Рубрика «Опрос дня» ---
// Статичные опросы-фолбэки по типу канала (если AI не сгенерил)
const FALLBACK_POLLS = {
news: { question: 'Какое настроение сегодня? 🇰🇿', options: ['🔥 Отличное', '😐 Нормально', '😴 Так себе', '😤 Лучше не спрашивай'] },
earnings: { question: 'Сколько хочешь зарабатывать в месяц? 💰', options: ['до 300к ₸', '300–700к ₸', '700к–1.5млн ₸', 'больше 1.5млн ₸'] },
sports: { question: 'За какой спорт болеешь больше? 🇰🇿', options: ['🥊 Бокс / ММА', '⚽ Футбол', '🏒 Хоккей', '🏋️ Другое'] }
};
// Обрезать строку до лимита Telegram
function clip(s, max) {
s = (s || '').trim();
return s.length > max ? s.slice(0, max - 1).trim() + '…' : s;
}
// Сгенерировать опрос через AI (или вернуть фолбэк по типу канала)
async function getPoll(channel) {
const apiKey = process.env.OPENROUTER_API_KEY;
const fallback = FALLBACK_POLLS[channel.type] || FALLBACK_POLLS.news;
if (!apiKey) return fallback;
const system = `Ты придумываешь короткие вовлекающие опросы для Telegram-канала "${channel.name}" (тематика: ${channel.type}, аудитория Казахстана).
Опрос должен быть лёгким, без политики и негатива, вызывать желание проголосовать.
Ответь СТРОГО в формате (без вступлений и пояснений):
ВОПРОС: <текст вопроса с эмодзи, до 180 символов>
ВАРИАНТ: <вариант 1>
ВАРИАНТ: <вариант 2>
ВАРИАНТ: <вариант 3>
2-4 варианта, каждый до 80 символов.`;
try {
const { data } = await axios.post(`${aiConfig.baseUrl}/chat/completions`, {
model: aiConfig.model,
messages: [
{ role: 'system', content: system },
{ role: 'user', content: 'Придумай один опрос на сегодня.' }
],
max_tokens: 300,
temperature: 0.9
}, {
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
timeout: 15000
});
const raw = data.choices?.[0]?.message?.content || '';
let question = null;
const options = [];
for (const line of raw.split('\n')) {
const q = line.match(/^\s*ВОПРОС:\s*(.+)$/i);
const o = line.match(/^\s*ВАРИАНТ:\s*(.+)$/i);
if (q) question = clip(q[1], 290);
else if (o) options.push(clip(o[1].replace(/^[-*\d.)\s]+/, ''), 95));
}
// Валидация: вопрос + 2-4 непустых уникальных варианта
const uniq = [...new Set(options.filter(Boolean))];
if (question && uniq.length >= 2) {
return { question, options: uniq.slice(0, 4) };
}
log(`⚠️ Опрос ${channel.name}: AI вернул невалидный формат, фолбэк`);
return fallback;
} catch (err) {
log(`❌ Опрос ${channel.name}: ${err.message}, фолбэк`);
return fallback;
}
}
module.exports = { getCurrencyPost, getPoll };
📜 Git History
31dcfdffeat: вечерняя рубрика Опрос дня (sendPoll, AI + статичный фолбэк)3 weeks ago
ecd6693feat: утренняя рубрика Курс валют от Нацбанка РК (B3)3 weeks ago
Show last diff
Loading...