bidaangmamamayan/lib/sources.js
Ryan 6b06e08c08 BidaAngMamayan: public supporter tribute news desk for Senator BAM Aquino
Forked from Forthena (PERSONAL/forthena) and retargeted:
- NO login, NO LLM: the edition is public and the ranking is deterministic
  (stories that name Sen. BAM lead; freshest first) — Daniel: no
  like/dislike or store profile for visitors.
- Crawler scoped to BAM: Bing news keyword queries + Inquirer/Rappler
  fallback wires, with a two-tier relevance gate (named-BAM always;
  Aquino+legislative-context only from fallback) so op-eds and non-Bam
  senators are dropped.
- Desks: BAM (lead), Gaming, Education, Politics, Energy, PH, Business.
- Community board /board: 'Thank you Senator BAM' — nickname-only posts,
  HTML-stripped, URL-only rejected, IP rate-limited (3/10min, 20/day).
- Yellow-gamer theme (Orbitron/Rajdhani/JetBrains Mono, dark + gold,
  HUD corners), supporter disclaimer in header and footer.
- Tests: category/date/relevance/text suites + Mongo board suite +
  full HTTP e2e (all passing).

Infra: 66.181.46.132 (bidaangmamamayan.com), local Mongo 7.0,
nginx + certbot TLS, PM2 bama (:3000) + 15-min crawl cron.
2026-09-15 08:37:03 +00:00

125 lines
5.6 KiB
JavaScript

// BidaAngMamamayan source registry — the crawler's supply line.
//
// This is a TRIBUTE site for Sen. BAM Aquino, so every feed is scoped to him:
// 1. BAM_QUERIES — keyword news searches (Bing News RSS, the same aggregator
// forthena learned to love 2026-08-31: it hands us the publisher's own URL
// inside the apiclick wrapper, which the reader pane needs).
// 2. PH_NATIVE — general PH news wires, kept only as an ASSOCIATED pool
// fallback: when a tick finds zero named-BAM stories, a couple of his
// senate colleagues' / committee's coverage still belongs on the site.
//
// Freshness: qft=interval="8" = last month. Bing's 7-day window only ever held
// 12 unique stories for "Bam Aquino" (measured 2026-09-15) — a senator's
// mention volume is low, so the crawl window is a month and the relevance
// gate (lib/crawl.js) does the actual filtering. The 48h crawl maxAge from
// forthena does NOT apply here: a named-BAM story from last Thursday is still
// front-page material this weekend.
//
// Bing quirks (all verified live on the agent host):
// * `OR` in queries is nil — single phrases only.
// * setlang=en-PH&cc=PH biases toward PH coverage.
// * A quoted exact phrase ("Bam Aquino") returns FEWER results than the
// unquoted phrase — Bing drops results whose snippet doesn't contain the
// exact string. Keep queries unquoted; the gate handles relevance.
// ── BAM queries ──────────────────────────────────────────────────────────
// Each phrase was measured through the live Bing news RSS on 2026-09-15.
// "Bam Aquino" alone is the workhorse (12 items/week, the rest overlap it).
export const BAM_QUERIES = [
{ query: "Bam Aquino", name: "BAM Wire", category: "bam" },
{ query: "Sen. Bam Aquino", name: "Senate Desk", category: "politics" },
{ query: "Bam Aquino education", name: "Education Desk", category: "education" },
{ query: "Bam Aquino college students scholarship", name: "Campus Desk", category: "education" },
{ query: "Bam Aquino power electricity", name: "Energy Desk", category: "energy" },
{ query: "Bam Aquino gaming esports", name: "Gamer Desk", category: "gaming" },
{ query: "Bam Aquino bill Senate", name: "Legislative Desk", category: "politics" },
];
// ── Associated pool (fallback only) ──────────────────────────────────────
// General PH wires. The crawler runs these ONLY when the BAM queries produced
// zero named stories this tick (verified: that happens on quiet days). Their
// items pass through the same gate, so only genuine senate/BAM-adjacent copy
// ever prints from here.
export const PH_NATIVE = [
{ key: "pna", name: "Philippine News Agency", category: "ph", feeds: ["https://www.pna.gov.ph/feed/"] },
{ key: "inqnews", name: "Inquirer News", category: "ph", feeds: ["https://newsinfo.inquirer.net/feed"] },
];
// ── Bing News RSS ────────────────────────────────────────────────────────
const BING = "https://www.bing.com/news/search?q=";
const BING_PARAMS = "&format=RSS&setlang=en-PH&cc=PH&qft=interval%3d%228%22";
export function queryFeed(q) {
return `${BING}${encodeURIComponent(String(q).slice(0, 120))}${BING_PARAMS}`;
}
// Bing wraps item links in apiclick.aspx?...&url=<publisher url>. Returns the
// publisher's own URL when the wrapper carries one, else the original.
export function unwrapFeedLink(link) {
let l = String(link || "").trim();
if (!l) return l;
l = l.replace(/&amp;/g, "&").replace(/&#38;/g, "&");
if (/bing\.com\/news\/apiclick/i.test(l)) {
const m = /[?&]url=([^&]+)/.exec(l);
if (m) {
try {
const real = decodeURIComponent(m[1]);
if (/^https?:\/\//i.test(real)) return real;
} catch { /* leave the wrapper */ }
}
}
if (/^https?:\/\/(www\.)?bing\.com\//i.test(l)) {
const m = /[?&](?:url|u)=([^&]+)/.exec(l);
if (m) { try { return decodeURIComponent(m[1]); } catch {} }
}
return l;
}
// Human publisher label: <News:Source> first, then the URL's host, then the
// "Headline - Publisher" tail.
const HOST_NAMES = {
"pna.gov.ph": "PNA",
"inquirer.net": "Inquirer",
"newsinfo.inquirer.net": "Inquirer News",
"gmanetwork.com": "GMA News",
"abs-cbn.com": "ABS-CBN",
"mb.com.ph": "Manila Bulletin",
"rappler.com": "Rappler",
"bworldonline.com": "BusinessWorld",
"philstar.com": "Philstar",
"sunstar.com.ph": "SunStar",
"cnnphilippines.com": "CNN Philippines",
"businessmirror.com": "BusinessMirror",
"manilatimes.net": "Manila Times",
"dailytribune.com.ph": "Daily Tribune",
"newsline.ph": "NewsLine",
"bombardyo.com": "Bombo Radyo",
};
export function publisherFromUrl(url) {
try {
const host = new URL(url).hostname.replace(/^www\./, "").toLowerCase();
const reg = Object.keys(HOST_NAMES).find((h) => host === h || host.endsWith("." + h));
if (reg) return HOST_NAMES[reg];
const parts = host.split(".");
return parts.length > 2 ? parts[parts.length - 3] : parts[0];
} catch {
return null;
}
}
export function publisherFrom(title, fallback) {
const t = String(title || "");
const i = t.lastIndexOf(" - ");
if (i > 24) return t.slice(i + 3).trim().slice(0, 40);
return fallback || "Wire";
}
// Aggregator titles sometimes still carry the " - Publisher" tail.
export function stripPublisherSuffix(title) {
const t = String(title || "");
const i = t.lastIndexOf(" - ");
if (i > 24) return t.slice(0, i).trim();
return t.trim();
}