initial commit

This commit is contained in:
JaysonCleve 2026-04-14 08:37:57 +02:00
commit 59da3d909e
7 changed files with 399 additions and 0 deletions

7
Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json .
RUN npm install --production
COPY src/ ./src/
VOLUME ["/data"]
CMD ["node", "src/index.js"]

71
README.md Normal file
View file

@ -0,0 +1,71 @@
# noctura newsbot — Setup
## 1. Bot-User anlegen (falls noch nicht getan)
```bash
docker exec -it synapse register_new_matrix_user -u newsbot -p sicherespasswort --no-admin -c /data/homeserver.yaml http://localhost:8008
```
## 2. Access Token holen
```bash
curl -s -X POST http://localhost:8008/_matrix/client/v3/login \
-H "Content-Type: application/json" \
-d '{"type":"m.login.password","user":"newsbot","password":"sicherespasswort"}' \
| grep -o '"access_token":"[^"]*"'
```
Den Token aus der Ausgabe kopieren.
## 3. Room ID herausfinden
Im Matrix Client (z.B. Element):
- In den gewünschten Room gehen
- Raumeinstellungen → Erweitert → Room ID kopieren (beginnt mit !)
Den Bot in den Room einladen:
```
/invite @newsbot:noctura.dev
```
## 4. docker-compose.yml anpassen
```yaml
MATRIX_ACCESS_TOKEN: "slt_DEIN_TOKEN"
MATRIX_ROOM_ID: "!xxxxx:noctura.dev"
```
## 5. Bot starten
```bash
cd /opt/noctura/newsbot
mkdir data
docker compose up -d --build
```
## 6. Test-Digest sofort senden
```bash
# TEST_RUN kurz auf true setzen
docker compose run --rm -e TEST_RUN=true newsbot
```
## Feeds anpassen
Einfach in `src/feeds.js` neue Feeds hinzufügen:
```js
{
category: "🎮 Gaming",
url: "https://www.rockpapershotgun.com/feed",
name: "Rock Paper Shotgun"
}
```
## Cron-Zeiten
| Wert | Bedeutung |
|------|-----------|
| `0 8 * * *` | Täglich 08:00 Uhr |
| `0 8,18 * * *`| Täglich 08:00 und 18:00 Uhr |
| `0 8 * * 1-5` | Nur MontagFreitag 08:00 Uhr |

26
docker-compose.yml Normal file
View file

@ -0,0 +1,26 @@
services:
newsbot:
build: .
container_name: newsbot
restart: unless-stopped
volumes:
- ./data:/data
environment:
# Matrix Homeserver (intern via Docker Host)
MATRIX_HOMESERVER: "http://host.docker.internal:8008"
# Access Token des newsbot Users (siehe README)
MATRIX_ACCESS_TOKEN: "DEIN_ACCESS_TOKEN_HIER"
# Room ID des Ziel-Rooms (beginnt mit !)
MATRIX_ROOM_ID: "!ROOM_ID_HIER:noctura.dev"
# Wann soll der Digest gesendet werden? (Cron-Format)
# Standard: täglich 08:00 Uhr
CRON_SCHEDULE: "0 8 * * *"
# Auf 'true' setzen um beim Start sofort einen Test zu senden
TEST_RUN: "false"
extra_hosts:
- "host.docker.internal:host-gateway"

94
feeds.js Normal file
View file

@ -0,0 +1,94 @@
// ============================================================
// feeds.js — RSS-Feed Konfiguration
// Einfach neue Feeds hinzufügen oder Kategorien anpassen
// ============================================================
const FEEDS = [
// --- KI & Machine Learning ---
{
category: "🤖 KI & Machine Learning",
url: "https://feeds.feedburner.com/blogspot/gJZg",
name: "Google AI Blog"
},
{
category: "🤖 KI & Machine Learning",
url: "https://openai.com/blog/rss.xml",
name: "OpenAI Blog"
},
{
category: "🤖 KI & Machine Learning",
url: "https://www.anthropic.com/rss.xml",
name: "Anthropic"
},
{
category: "🤖 KI & Machine Learning",
url: "https://huggingface.co/blog/feed.xml",
name: "Hugging Face"
},
// --- IT Security ---
{
category: "🔐 IT Security",
url: "https://feeds.feedburner.com/TheHackersNews",
name: "The Hacker News"
},
{
category: "🔐 IT Security",
url: "https://www.bleepingcomputer.com/feed/",
name: "BleepingComputer"
},
{
category: "🔐 IT Security",
url: "https://krebsonsecurity.com/feed/",
name: "Krebs on Security"
},
// --- Smart Home & IoT ---
{
category: "🏠 Smart Home & IoT",
url: "https://www.home-assistant.io/atom.xml",
name: "Home Assistant"
},
{
category: "🏠 Smart Home & IoT",
url: "https://www.heise.de/thema/smart-home.rss",
name: "Heise Smart Home"
},
// --- Web Development ---
{
category: "⟨/⟩ Web Development",
url: "https://css-tricks.com/feed/",
name: "CSS-Tricks"
},
{
category: "⟨/⟩ Web Development",
url: "https://dev.to/feed",
name: "DEV.to"
},
{
category: "⟨/⟩ Web Development",
url: "https://github.blog/feed/",
name: "GitHub Blog"
},
// --- Self-hosting & Linux ---
{
category: "🖥️ Self-hosting & Linux",
url: "https://selfh.st/articles/index.xml",
name: "selfh.st"
},
{
category: "🖥️ Self-hosting & Linux",
url: "https://www.heise.de/open/news-atom.xml",
name: "Heise Open"
},
];
// Wie viele Artikel pro Feed maximal gepostet werden
const MAX_ITEMS_PER_FEED = 2;
// Wie viele Stunden rückwirkend Artikel berücksichtigt werden
const HOURS_LOOKBACK = 24;
module.exports = { FEEDS, MAX_ITEMS_PER_FEED, HOURS_LOOKBACK };

55
formatter.js Normal file
View file

@ -0,0 +1,55 @@
// ============================================================
// formatter.js — Formatiert News-Nachrichten für Matrix
// ============================================================
function formatDailyDigest(grouped) {
const date = new Date().toLocaleDateString('de-DE', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
});
// Plain text version
let plain = `📰 Täglicher News-Digest — ${date}\n`;
plain += '─'.repeat(50) + '\n\n';
// HTML version (für Matrix Clients die Formatting unterstützen)
let html = `<h3>📰 Täglicher News-Digest</h3>`;
html += `<p><em>${date}</em></p><hr>`;
for (const [category, items] of Object.entries(grouped)) {
if (items.length === 0) continue;
plain += `${category}\n${'─'.repeat(30)}\n`;
html += `<h4>${category}</h4><ul>`;
for (const item of items) {
const source = item.feedName ? ` [${item.feedName}]` : '';
plain += `${item.title}${source}\n ${item.link}\n\n`;
html += `<li><a href="${item.link}"><strong>${item.title}</strong></a>`;
if (item.feedName) html += ` <em>(${item.feedName})</em>`;
if (item.summary) html += `<br><small>${item.summary}</small>`;
html += `</li>`;
}
plain += '\n';
html += `</ul>`;
}
const totalItems = Object.values(grouped).flat().length;
plain += `\n📊 ${totalItems} Artikel heute · Powered by noctura newsbot`;
html += `<p><small>📊 ${totalItems} Artikel heute · Powered by noctura newsbot</small></p>`;
return { plain, html };
}
function formatNoNews() {
return {
plain: '📰 Heute keine neuen Artikel in den konfigurierten Feeds.',
html: '<p>📰 Heute keine neuen Artikel in den konfigurierten Feeds.</p>'
};
}
function formatError(feedName, error) {
return `⚠️ Feed-Fehler [${feedName}]: ${error.message}`;
}
module.exports = { formatDailyDigest, formatNoNews, formatError };

132
index.js Normal file
View file

@ -0,0 +1,132 @@
// ============================================================
// index.js — noctura newsbot
// Matrix Bot der täglich News-Feeds postet
// ============================================================
const { MatrixClient, SimpleFsStorageProvider, AutojoinRoomsMixin } = require('matrix-bot-sdk');
const RSSParser = require('rss-parser');
const cron = require('node-cron');
const { FEEDS, MAX_ITEMS_PER_FEED, HOURS_LOOKBACK } = require('./feeds');
const { formatDailyDigest, formatNoNews, formatError } = require('./formatter');
// ---- Konfiguration aus Umgebungsvariablen ----
const HOMESERVER = process.env.MATRIX_HOMESERVER || 'http://localhost:8008';
const ACCESS_TOKEN = process.env.MATRIX_ACCESS_TOKEN;
const ROOM_ID = process.env.MATRIX_ROOM_ID;
const CRON_SCHEDULE= process.env.CRON_SCHEDULE || '0 8 * * *'; // täglich 08:00 Uhr
if (!ACCESS_TOKEN) { console.error('❌ MATRIX_ACCESS_TOKEN fehlt!'); process.exit(1); }
if (!ROOM_ID) { console.error('❌ MATRIX_ROOM_ID fehlt!'); process.exit(1); }
// ---- Matrix Client Setup ----
const storage = new SimpleFsStorageProvider('/data/bot-storage.json');
const client = new MatrixClient(HOMESERVER, ACCESS_TOKEN, storage);
AutojoinRoomsMixin.setupOnClient(client);
const parser = new RSSParser({
timeout: 10000,
headers: { 'User-Agent': 'noctura-newsbot/1.0' }
});
// ---- RSS Feed abrufen ----
async function fetchFeed(feed) {
try {
const parsed = await parser.parseURL(feed.url);
const cutoff = Date.now() - HOURS_LOOKBACK * 60 * 60 * 1000;
return parsed.items
.filter(item => {
const pubDate = item.pubDate ? new Date(item.pubDate).getTime() : Date.now();
return pubDate >= cutoff;
})
.slice(0, MAX_ITEMS_PER_FEED)
.map(item => ({
title: item.title?.trim() || 'Kein Titel',
link: item.link || item.guid || '',
summary: item.contentSnippet
? item.contentSnippet.slice(0, 120).trim() + '…'
: '',
feedName: feed.name,
category: feed.category,
pubDate: item.pubDate
}));
} catch (err) {
console.error(`⚠️ Feed-Fehler [${feed.name}]: ${err.message}`);
return [];
}
}
// ---- Alle Feeds abrufen & gruppieren ----
async function fetchAllNews() {
console.log('🔍 Feeds werden abgerufen...');
const results = await Promise.allSettled(FEEDS.map(fetchFeed));
const grouped = {};
results.forEach((result, i) => {
if (result.status !== 'fulfilled') return;
const items = result.value;
items.forEach(item => {
if (!grouped[item.category]) grouped[item.category] = [];
grouped[item.category].push(item);
});
});
return grouped;
}
// ---- Nachricht in Matrix Room senden ----
async function sendToMatrix(plain, html) {
await client.sendMessage(ROOM_ID, {
msgtype: 'm.text',
body: plain,
format: 'org.matrix.custom.html',
formatted_body: html
});
console.log('✅ Digest erfolgreich gesendet!');
}
// ---- Haupt-Job ----
async function runDigest() {
console.log(`\n⏰ [${new Date().toISOString()}] Täglicher Digest wird erstellt...`);
try {
const grouped = await fetchAllNews();
const totalItems = Object.values(grouped).flat().length;
console.log(`📊 ${totalItems} Artikel gefunden`);
if (totalItems === 0) {
const { plain, html } = formatNoNews();
await sendToMatrix(plain, html);
} else {
const { plain, html } = formatDailyDigest(grouped);
await sendToMatrix(plain, html);
}
} catch (err) {
console.error('❌ Fehler beim Digest:', err);
}
}
// ---- Bot starten ----
async function main() {
console.log('🚀 noctura newsbot startet...');
console.log(` Homeserver: ${HOMESERVER}`);
console.log(` Room: ${ROOM_ID}`);
console.log(` Cron: ${CRON_SCHEDULE}`);
await client.start();
console.log('✅ Matrix Client verbunden!');
// Cron Job registrieren
cron.schedule(CRON_SCHEDULE, runDigest, { timezone: 'Europe/Berlin' });
console.log(`📅 Cron Job aktiv: ${CRON_SCHEDULE} (Europe/Berlin)`);
// Beim Start einmal sofort ausführen wenn TEST_RUN gesetzt
if (process.env.TEST_RUN === 'true') {
console.log('🧪 TEST_RUN aktiv einmalig sofort ausführen...');
await runDigest();
}
}
main().catch(err => {
console.error('❌ Bot-Fehler:', err);
process.exit(1);
});

14
package.json Normal file
View file

@ -0,0 +1,14 @@
{
"name": "noctura-newsbot",
"version": "1.0.0",
"description": "Matrix News Bot für noctura.dev",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"matrix-bot-sdk": "^0.7.1",
"rss-parser": "^3.13.0",
"node-cron": "^3.0.3"
}
}