initial commit
This commit is contained in:
commit
59da3d909e
7 changed files with 399 additions and 0 deletions
7
Dockerfile
Normal file
7
Dockerfile
Normal 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
71
README.md
Normal 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 Montag–Freitag 08:00 Uhr |
|
||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal 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
94
feeds.js
Normal 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
55
formatter.js
Normal 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
132
index.js
Normal 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
14
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue