Working news
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import axios from "axios";
|
||||
|
||||
export type NewsItem = {
|
||||
title: string;
|
||||
link: string;
|
||||
source: string;
|
||||
publishedAt: number;
|
||||
};
|
||||
|
||||
const FEEDS: { href: string; source: string }[] = [
|
||||
{
|
||||
href: "/news/thn",
|
||||
source: "The Hacker News",
|
||||
},
|
||||
{
|
||||
href: "/news/cisa",
|
||||
source: "CISA",
|
||||
},
|
||||
{
|
||||
href: "/news/krebs",
|
||||
source: "Krebs on Security",
|
||||
},
|
||||
{
|
||||
href: "/news/bc",
|
||||
source: "BleepingComputer",
|
||||
},
|
||||
];
|
||||
|
||||
const MAX_ITEMS = 15;
|
||||
const REQUEST_TIMEOUT_MS = 10_000;
|
||||
|
||||
function parseDate(value: string | null | undefined): number {
|
||||
if (!value) return 0;
|
||||
|
||||
const ms = Date.parse(value);
|
||||
return Number.isNaN(ms) ? 0 : ms;
|
||||
}
|
||||
|
||||
function textOf(el: Element | undefined | null, tag: string): string {
|
||||
return el?.getElementsByTagName(tag)[0]?.textContent?.trim() ?? "";
|
||||
}
|
||||
|
||||
function dateOf(el: Element): number {
|
||||
const pubDate = textOf(el, "pubDate");
|
||||
if (pubDate) return parseDate(pubDate);
|
||||
|
||||
const dc = el.getElementsByTagName("dc:date")[0]?.textContent?.trim();
|
||||
return parseDate(dc);
|
||||
}
|
||||
|
||||
function parseFeed(xml: string, source: string): NewsItem[] {
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
|
||||
if (doc.querySelector("parsererror")) return [];
|
||||
|
||||
const items: NewsItem[] = [];
|
||||
|
||||
doc.querySelectorAll("item").forEach((item) => {
|
||||
const title = textOf(item, "title");
|
||||
const link = textOf(item, "link");
|
||||
if (!title || !link) return;
|
||||
|
||||
items.push({
|
||||
title,
|
||||
link,
|
||||
source,
|
||||
publishedAt: dateOf(item),
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function getCyberNews(): Promise<NewsItem[]> {
|
||||
const results = await Promise.allSettled(
|
||||
FEEDS.map(({ href, source }) =>
|
||||
axios
|
||||
.get<string>(href, { timeout: REQUEST_TIMEOUT_MS })
|
||||
.then((res) => parseFeed(res.data, source)),
|
||||
),
|
||||
);
|
||||
|
||||
const items: NewsItem[] = [];
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result.status === "fulfilled") {
|
||||
items.push(...result.value);
|
||||
}
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error("No cyber news available right now.");
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const deduped = items
|
||||
.sort((a, b) => b.publishedAt - a.publishedAt)
|
||||
.filter((item) => {
|
||||
const key = item.link;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
return deduped.slice(0, MAX_ITEMS);
|
||||
}
|
||||
@@ -2,6 +2,46 @@ import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
import type { ProxyOptions } from "vite";
|
||||
|
||||
const newsProxy: Record<string, ProxyOptions> = {
|
||||
"/news/thn": {
|
||||
target: "https://feeds.feedburner.com",
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/news\/thn/, "/TheHackersNews"),
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
},
|
||||
},
|
||||
"/news/krebs": {
|
||||
target: "https://krebsonsecurity.com",
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/news\/krebs/, "/feed/"),
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
},
|
||||
},
|
||||
"/news/cisa": {
|
||||
target: "https://www.cisa.gov",
|
||||
changeOrigin: true,
|
||||
rewrite: (p) =>
|
||||
p.replace(/^\/news\/cisa/, "/cybersecurity-advisories/all.xml"),
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
},
|
||||
},
|
||||
"/news/bc": {
|
||||
target: "https://www.bleepingcomputer.com",
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/news\/bc/, "/feed/"),
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
|
||||
Accept:
|
||||
"application/rss+xml, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.7",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
@@ -24,6 +64,7 @@ export default defineConfig(async () => ({
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
proxy: newsProxy,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
|
||||
Reference in New Issue
Block a user