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 react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import path from "path";
|
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;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
@@ -24,6 +64,7 @@ export default defineConfig(async () => ({
|
|||||||
port: 1420,
|
port: 1420,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
host: host || false,
|
host: host || false,
|
||||||
|
proxy: newsProxy,
|
||||||
hmr: host
|
hmr: host
|
||||||
? {
|
? {
|
||||||
protocol: "ws",
|
protocol: "ws",
|
||||||
|
|||||||
Reference in New Issue
Block a user