Code erstellt von OpenCode.ai
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
messages.db
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
TELEGRAM_TOKEN=your_telegram_bot_token_here
|
||||
RANDOM_CHANCE=0.1
|
||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
RUN python -m spacy download de_core_news_sm
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
CMD ["python", "bot.py"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
45
README.md
45
README.md
@@ -1,2 +1,47 @@
|
||||
# ulfbot
|
||||
|
||||
Ein Telegram Bot, der mithilfe von Markov-Ketten und spaCy grammatikalisch richtige Nachrichten generiert.
|
||||
|
||||
## Features
|
||||
|
||||
- **Markov-Kette (Order 2-3)**: Generiert Sätze basierend auf den letzten 1000 Nachrichten
|
||||
- **spaCy Integration**: Nutzt Tokenisierung und POS-Tagging für bessere Grammatik
|
||||
- **Reaktionsmodi**:
|
||||
- Auf Mention reagieren (`@botname`)
|
||||
- Auf Replys reagieren
|
||||
- Zufällig in 10% der Fälle (konfigurierbar)
|
||||
- **Persistente Speicherung**: SQLite-Datenbank für Nachrichten
|
||||
- **Chat-spezifisch**: Jeder Chat hat seine eigene Wissensbasis
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python -m spacy download de_core_news_sm
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# .env mit Token bearbeiten
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Usage in Telegram
|
||||
|
||||
1. Füge den Bot einer Gruppe hinzu
|
||||
2. Sage etwas im Chat
|
||||
3. Bot speichert die Nachricht automatisch
|
||||
4. Reagiere auf eine Nachricht mit Reply → Bot antwortet
|
||||
5. Oder mentioniere den Bot (`@botname`) → Bot antwortet
|
||||
6. Oder warte zufällig (10% Chance pro Nachricht) → Bot antwortet
|
||||
|
||||
## Commands
|
||||
|
||||
- `/start` - Start Nachricht
|
||||
- `/stats` - Anzahl gespeicherter Nachrichten im aktuellen Chat
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
|
||||
151
bot.py
Normal file
151
bot.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import logging
|
||||
import random
|
||||
from telegram import Update
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
ContextTypes,
|
||||
MessageHandler,
|
||||
filters,
|
||||
CommandHandler,
|
||||
)
|
||||
import sqlite3
|
||||
|
||||
from config import Config
|
||||
from database import init_db, save_message, get_recent_messages, cleanup_old_messages, DB_PATH
|
||||
from markov import build_markov_chain, generate_markov_sentence
|
||||
|
||||
|
||||
def is_admin_or_owner(update: Update) -> tuple[bool, str]:
|
||||
if update.message.chat.type not in ['group', 'supergroup']:
|
||||
return True, ''
|
||||
|
||||
chat_id = update.message.chat_id
|
||||
user_id = update.message.from_user.id
|
||||
bot_id = update.bot.id
|
||||
|
||||
try:
|
||||
chat_member = update.message.chat.get_member(bot_id)
|
||||
status = chat_member.status
|
||||
permissions = chat_member.permissions
|
||||
|
||||
if status not in ['administrator', 'creator']:
|
||||
return False, 'Ich benötige Admin-Rechte, um zu funktionieren.'
|
||||
|
||||
if not permissions.can_send_messages:
|
||||
return False, 'Ich benötige die Berechtigung, Nachrichten zu senden.'
|
||||
|
||||
return True, ''
|
||||
except Exception as e:
|
||||
return False, f'Fehler beim Prüfen der Admin-Rechte: {str(e)}'
|
||||
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
is_admin, message = is_admin_or_owner(update)
|
||||
|
||||
if not is_admin:
|
||||
if message:
|
||||
await update.message.reply_text(message)
|
||||
return
|
||||
|
||||
await update.message.reply_text(
|
||||
'Hallo! Ich bin ein Markov-Bot. '
|
||||
'Sage mir etwas und ich kann später etwas Ähnliches generieren.'
|
||||
)
|
||||
|
||||
|
||||
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
message = update.message
|
||||
chat_id = message.chat_id
|
||||
user_id = message.from_user.id
|
||||
username = message.from_user.username
|
||||
text = message.text or ''
|
||||
|
||||
if not text:
|
||||
return
|
||||
|
||||
save_message(chat_id, user_id, username, text)
|
||||
|
||||
cleanup_old_messages(chat_id, Config.MAX_MESSAGES)
|
||||
|
||||
|
||||
async def generate_and_send(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
chat_id = update.message.chat_id
|
||||
|
||||
recent_texts = get_recent_messages(chat_id, Config.MAX_MESSAGES)
|
||||
|
||||
if not recent_texts:
|
||||
await update.message.reply_text('Noch keine Nachrichten zum Lernen.')
|
||||
return
|
||||
|
||||
chain = build_markov_chain(recent_texts, order=2)
|
||||
|
||||
if not chain:
|
||||
await update.message.reply_text('Nicht genug Daten für Markov-Kette.')
|
||||
return
|
||||
|
||||
sentence = generate_markov_sentence(chain)
|
||||
|
||||
if sentence:
|
||||
await update.message.reply_text(sentence)
|
||||
else:
|
||||
await update.message.reply_text('Konnte keinen Satz generieren.')
|
||||
|
||||
|
||||
async def handle_mention(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
message = update.message
|
||||
|
||||
if message.reply_to_message:
|
||||
await generate_and_send(update, context)
|
||||
return
|
||||
|
||||
bot_username = context.bot.username
|
||||
text = message.text or ''
|
||||
|
||||
if f'@{bot_username}' in text:
|
||||
await generate_and_send(update, context)
|
||||
|
||||
|
||||
async def random_response(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if random.random() < Config.RANDOM_CHANCE:
|
||||
await generate_and_send(update, context)
|
||||
|
||||
|
||||
async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
is_admin, message = is_admin_or_owner(update)
|
||||
|
||||
if not is_admin:
|
||||
if message:
|
||||
await update.message.reply_text(message)
|
||||
return
|
||||
|
||||
chat_id = update.message.chat_id
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT COUNT(*) FROM messages WHERE chat_id = ?', (chat_id,))
|
||||
count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
await update.message.reply_text(f'Gespeicherte Nachrichten in diesem Chat: {count}')
|
||||
|
||||
|
||||
def main():
|
||||
init_db()
|
||||
|
||||
if not Config.TELEGRAM_TOKEN:
|
||||
raise ValueError('TELEGRAM_TOKEN nicht gesetzt in Environment')
|
||||
|
||||
application = Application.builder().token(Config.TELEGRAM_TOKEN).build()
|
||||
|
||||
application.add_handler(CommandHandler('start', start))
|
||||
application.add_handler(CommandHandler('stats', stats))
|
||||
application.add_handler(MessageHandler(filters.TEXT & filters.REPLY, handle_mention))
|
||||
application.add_handler(MessageHandler(filters.TEXT & (filters.ALL & ~filters.COMMAND), handle_message))
|
||||
application.add_handler(MessageHandler(filters.TEXT & (filters.ALL & ~filters.COMMAND), random_response))
|
||||
|
||||
logging.info('Bot startet...')
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
7
config.py
Normal file
7
config.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
|
||||
class Config:
|
||||
TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN')
|
||||
RANDOM_CHANCE = float(os.getenv('RANDOM_CHANCE', '0.1'))
|
||||
MAX_MESSAGES = 1000
|
||||
76
database.py
Normal file
76
database.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import sqlite3
|
||||
from typing import Optional
|
||||
|
||||
|
||||
DB_PATH = '/app/data/messages.db'
|
||||
|
||||
|
||||
def init_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
username TEXT,
|
||||
text TEXT NOT NULL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_chat ON messages(chat_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def save_message(chat_id: int, user_id: int, username: Optional[str], text: str):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO messages (chat_id, user_id, username, text) VALUES (?, ?, ?, ?)
|
||||
''', (chat_id, user_id, username, text))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_recent_messages(chat_id: int, limit: int = 1000) -> list[str]:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT text FROM messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY timestamp DESC LIMIT ?
|
||||
''', (chat_id, limit))
|
||||
messages = [row[0] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
return messages
|
||||
|
||||
|
||||
def get_all_messages_for_chat(chat_id: int) -> list[tuple[str, str]]:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT username, text FROM messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
''', (chat_id,))
|
||||
messages = cursor.fetchall()
|
||||
conn.close()
|
||||
return messages
|
||||
|
||||
|
||||
def cleanup_old_messages(chat_id: int, keep_count: int = 1000):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM messages
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
)
|
||||
''', (chat_id, keep_count))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
18
docker-compose.yaml
Normal file
18
docker-compose.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
ulfbot:
|
||||
build: .
|
||||
container_name: ulfbot
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TELEGRAM_TOKEN=${TELEGRAM_TOKEN}
|
||||
- RANDOM_CHANCE=${RANDOM_CHANCE:-0.1}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
72
markov.py
Normal file
72
markov.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import spacy
|
||||
nlp = spacy.load('de_core_news_sm')
|
||||
except ImportError:
|
||||
print("Spacy model not found. Please run: python -m spacy download de_core_news_sm")
|
||||
raise
|
||||
|
||||
|
||||
def tokenize_text(text: str) -> list[str]:
|
||||
doc = nlp(text)
|
||||
tokens = []
|
||||
for token in doc:
|
||||
if not token.is_space and not token.is_punct:
|
||||
tokens.append(token.text)
|
||||
return tokens
|
||||
|
||||
|
||||
def build_markov_chain(texts: list[str], order: int = 2) -> dict:
|
||||
chain = defaultdict(list)
|
||||
|
||||
for text in texts:
|
||||
tokens = tokenize_text(text)
|
||||
if len(tokens) <= order:
|
||||
continue
|
||||
|
||||
for i in range(len(tokens) - order):
|
||||
key = tuple(tokens[i:i + order])
|
||||
next_word = tokens[i + order]
|
||||
chain[key].append(next_word)
|
||||
|
||||
return chain
|
||||
|
||||
|
||||
def generate_markov_sentence(chain: dict, max_length: int = 30, start_length: int = 2) -> Optional[str]:
|
||||
if not chain:
|
||||
return None
|
||||
|
||||
start_keys = [k for k in chain.keys() if k[0][0].isupper() or k[0].isupper()]
|
||||
if not start_keys:
|
||||
start_keys = list(chain.keys())
|
||||
|
||||
current = random.choice(start_keys)
|
||||
words = list(current)
|
||||
|
||||
while len(words) < max_length:
|
||||
key = tuple(words[-start_length:])
|
||||
if key not in chain:
|
||||
break
|
||||
|
||||
next_word = random.choice(chain[key])
|
||||
words.append(next_word)
|
||||
|
||||
if next_word in '.!?':
|
||||
break
|
||||
|
||||
sentence = ' '.join(words)
|
||||
|
||||
for punct in '.!?':
|
||||
if punct in sentence:
|
||||
sentence = sentence.split(punct)[0] + punct
|
||||
break
|
||||
|
||||
return sentence
|
||||
|
||||
|
||||
def process_texts_for_markov(texts: list[str], order: int = 2) -> dict:
|
||||
chain = build_markov_chain(texts, order)
|
||||
return chain
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
python-telegram-bot==20.7
|
||||
spacy==3.7.5
|
||||
Reference in New Issue
Block a user