Автоматичний блогер
Щоранку допис у Markdown із найсерйознішими новинами, курсом BNM і граничною ціною ANRE. Скрипт зберігає його у файл і, якщо хочете, надсилає в канал Telegram.
Як це працює
- Бере новини за останні 24 години із серйозністю щонайменше 50.
- Бере курс і граничну ціну дня.
- Пише допис, з посиланнями на оригінали та рядком авторства з
meta.attribution. - Зберігає його в
posts/і надсилає в Telegram, якщо ви вказали токен.
Скільки коштує: 2 кредити за новини, 1 за курс, 1 за пальне: 4 кредити на день, 120 на місяць. Вміщується в тариф Start. На будь-якому тарифі новини надходять без затримки й з коротким викладом.
Як запустити
export SATELIT_KEY=sat_live_...
POST_LANG=ro MIN_GRAVITY=50 node blogger.mjsЩо виводить
posts/2026-11-02-ro.md: 4 stories, 4 creditsПовний скрипт
#!/usr/bin/env node
// Automated blogger: one Markdown post a day from the Satelit API.
// SATELIT_KEY=sat_live_... node blogger.mjs
// Optional: TELEGRAM_TOKEN and TELEGRAM_CHAT send the post to a channel.
// No dependencies, Node 18 or newer. Put it in cron: 0 8 * * * node /path/blogger.mjs
import { mkdirSync, writeFileSync } from "node:fs";
const BASE = process.env.SATELIT_BASE ?? "https://api.satelit.md/v1";
const KEY = process.env.SATELIT_KEY;
const LANG = process.env.POST_LANG ?? "ro";
const MIN_GRAVITY = Number(process.env.MIN_GRAVITY ?? 50);
const OUT_DIR = process.env.OUT_DIR ?? "posts";
if (!KEY) throw new Error("Set SATELIT_KEY");
const WORDS = {
ro: { title: "Ziua în Moldova", news: "Știrile zilei", money: "Curs și carburanți", cap: "Plafon A-95", gravity: "gravitate" },
ru: { title: "День в Молдове", news: "Новости дня", money: "Курс и топливо", cap: "Предел A-95", gravity: "серьёзность" },
en: { title: "The day in Moldova", news: "Today's news", money: "Rates and fuel", cap: "A-95 cap", gravity: "gravity" },
uk: { title: "День у Молдові", news: "Новини дня", money: "Курс і пальне", cap: "Межа A-95", gravity: "серйозність" },
};
const w = WORDS[LANG];
async function api(path, params = {}) {
const url = new URL(BASE + path);
for (const [k, v] of Object.entries({ lang: LANG, ...params })) url.searchParams.set(k, v);
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
const retry = Number(res.headers.get("retry-after"));
if ((res.status === 429 || res.status === 503) && retry && attempt < 2) {
await new Promise((ok) => setTimeout(ok, retry * 1000));
continue;
}
const body = await res.json();
if (!res.ok) throw new Error(`${path}: ${body.error.code}: ${body.error.message}`);
return body;
}
}
const today = new Date().toISOString().slice(0, 10);
const since = new Date(Date.now() - 86400e3).toISOString().slice(0, 10);
const [news, rates, fuel] = await Promise.all([
api("/news", { min_gravity: MIN_GRAVITY, from: since, per: 10 }),
api("/rates"),
api("/fuel"),
]);
const lines = [`# ${w.title}: ${today}`, "", `## ${w.news}`, ""];
for (const n of [...news.data].sort((a, b) => b.gravity - a.gravity)) {
const link = n.sources[0]?.url;
lines.push(`- **${n.title}** (${w.gravity} ${n.gravity})${link ? ` [${n.sources[0].name}](${link})` : ""}`);
if (n.summary ?? n.dek) lines.push(` ${n.summary ?? n.dek}`);
}
const eur = rates.data.find((r) => r.code === "EUR");
const usd = rates.data.find((r) => r.code === "USD");
lines.push("", `## ${w.money}`, "", `- EUR: ${eur.value} (${eur.change_7d_pct}%, 7d)`, `- USD: ${usd.value} (${usd.change_7d_pct}%, 7d)`, `- ${w.cap}: ${fuel.data.cap.a95} lei/l`);
// The plan's attribution line, when it asks for one
const attribution = news.meta.attribution ?? rates.meta.attribution;
if (attribution) lines.push("", `*${attribution}*`);
const post = lines.join("\n") + "\n";
mkdirSync(OUT_DIR, { recursive: true });
const file = `${OUT_DIR}/${today}-${LANG}.md`;
writeFileSync(file, post);
if (process.env.TELEGRAM_TOKEN && process.env.TELEGRAM_CHAT) {
const res = await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: process.env.TELEGRAM_CHAT, text: post.slice(0, 4000) }),
});
if (!res.ok) throw new Error(`Telegram: ${res.status}`);
}
const credits = news.meta.credits + rates.meta.credits + fuel.meta.credits;
console.log(`${file}: ${news.data.length} stories, ${credits} credits`);