RU

Тема

Цвет акцента

Тема и цвет сохранятся на satelit.md и в приложении.

Панель Выбрать тариф

Автоблогер

Каждое утро пост в Markdown с самыми серьёзными новостями, курсом BNM и предельной ценой ANRE. Сохраняется в файл и, если хотите, отправляется в канал Telegram.

Как это работает

  1. Берёт новости за последние 24 часа с серьёзностью не ниже 50.
  2. Берёт курс и предельную цену дня.
  3. Пишет пост со ссылками на оригиналы и со строкой указания источника из meta.attribution.
  4. Сохраняет его в posts/ и отправляет в Telegram, если вы задали токен.

Сколько стоит: 2 кредита за новости, 1 за курс, 1 за топливо: 4 кредита в день, 120 в месяц. Укладывается в тариф Start. На любом тарифе новости приходят без задержки и с кратким содержанием.

Как запустить

bash
export SATELIT_KEY=sat_live_...
POST_LANG=ro MIN_GRAVITY=50 node blogger.mjs

Что выводит

stdout
posts/2026-11-02-ro.md: 4 stories, 4 credits

Полный скрипт

blogger.mjs
#!/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`);