Cleanup: channels support, security hardening, remove cruft

This commit is contained in:
Your Name 2026-07-22 02:15:25 +03:00
parent 19e115c926
commit b8daea66f2
9 changed files with 394 additions and 707 deletions

3
Anime/README.md Normal file
View File

@ -0,0 +1,3 @@
# Anime Channel
This directory is dedicated to discussions and content about anime.

3
Games/README.md Normal file
View File

@ -0,0 +1,3 @@
# Games Channel
This directory is dedicated to discussions and content about games.

3
Politics/README.md Normal file
View File

@ -0,0 +1,3 @@
# Politics Channel
This directory is dedicated to discussions and content about politics.

View File

@ -1,2 +1,53 @@
# FuckingChat
# FuckingChat 🔒
Encrypted chat with channel support. Messages are encrypted at rest using Fernet (PBKDF2 + SHA256).
## Channels
- 💬 **General** — general discussion
- 🎮 **Games** — gaming chat
- 🎌 **Anime** — anime & manga
- ⚖️ **Politics** — politics discussion
## Setup
```bash
# Install dependencies
pip install bottle cryptography sqlalchemy
# Run (development)
python index.py
# Run (production - recommended)
CHAT_SECRET="your-strong-secret" \
CHAT_SALT="your-random-salt" \
CHAT_ADMIN_TOKEN="your-admin-token" \
CHAT_RATE_LIMIT=5 \
CHAT_HOST=0.0.0.0 \
CHAT_PORT=8080 \
python index.py
```
## Environment Variables
| Variable | Default | Description |
|---|---|---|
| `CHAT_SECRET` | `default_secret_change_me` | Encryption key |
| `CHAT_SALT` | `default_salt_change_me` | PBKDF2 salt |
| `CHAT_ADMIN_TOKEN` | (none) | Token for DELETE API |
| `CHAT_RATE_LIMIT` | `5` | Max messages per window |
| `CHAT_RATE_WINDOW` | `10` | Rate limit window (seconds) |
| `CHAT_MAX_MSG` | `2000` | Max message length |
| `CHAT_DB_PATH` | `./chat.db` | SQLite database path |
| `CHAT_HOST` | `localhost` | Listen address |
| `CHAT_PORT` | `8080` | Listen port |
| `CHAT_DEBUG` | `0` | Debug mode (set to `1` for dev) |
## Security Features
- ✅ Messages encrypted at rest (AES via Fernet)
- ✅ Rate limiting (configurable)
- ✅ IP hashing (privacy)
- ✅ XSS protection (HTML tag stripping)
- ✅ Admin token required for deletion
- ✅ No debug mode in production

BIN
chat.db

Binary file not shown.

View File

@ -1,191 +0,0 @@
import sqlalchemy
from sqlalchemy import create_engine, String, Integer, Text, DateTime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlalchemy.orm import sessionmaker
from datetime import datetime
import time
import os
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
# Encryption setup
SALT = b'salt_123456789'
PASSWORD = b'chat_secret_key_123'
def get_cipher():
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=SALT,
iterations=100000,
)
key = base64.urlsafe_b64encode(kdf.derive(PASSWORD))
return Fernet(key)
class Base(DeclarativeBase):
pass
class Messages(Base):
__tablename__ = "messages"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
ip: Mapped[str] = mapped_column(String(64), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
timestamp: Mapped[int] = mapped_column(Integer, nullable=False)
# MariaDB configuration
DB_USER = 'chat'
DB_PASSWORD = 'uqhyUb5eBGg3qad.'
DB_HOST = 'localhost'
DB_PORT = '3306'
DB_NAME = 'chat_db'
# Create engine for MariaDB
DATABASE_URL = f"mariadb+mariadbconnector://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
# Alternative with pymysql (uncomment if needed):
# DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
engine = create_engine(
DATABASE_URL,
echo=False,
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
pool_recycle=3600
)
# Create tables
Base.metadata.create_all(engine)
class DBfrontend():
def __init__(self):
self.Session = sessionmaker(bind=engine)
self.cipher = get_cipher()
def encrypt_message(self, message: str) -> str:
"""Encrypt message before storing"""
return self.cipher.encrypt(message.encode()).decode()
def decrypt_message(self, encrypted_message: str) -> str:
"""Decrypt message after retrieval"""
try:
return self.cipher.decrypt(encrypted_message.encode()).decode()
except:
return "[Decryption failed]"
def add_message(self, ip: str, content: str) -> int:
"""Add a new message to the database and return its ID"""
with self.Session() as session:
encrypted_content = self.encrypt_message(content)
new_message = Messages(
ip=ip,
content=encrypted_content,
timestamp=int(time.time() * 1000)
)
session.add(new_message)
session.commit()
return new_message.id
def get_all_messages(self) -> list:
"""Get all messages from the database"""
with self.Session() as session:
messages = session.query(Messages).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
def get_message_by_id(self, message_id: int) -> Messages | None:
"""Get a specific message by its ID"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
message.content = self.decrypt_message(message.content)
return message
def get_messages_by_ip(self, ip: str) -> list:
"""Get all messages from a specific IP address"""
with self.Session() as session:
messages = session.query(Messages).filter(Messages.ip == ip).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
def update_message(self, message_id: int, new_content: str) -> bool:
"""Update the content of a message"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
message.content = self.encrypt_message(new_content)
session.commit()
return True
return False
def delete_message(self, message_id: int) -> bool:
"""Delete a message by its ID"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
session.delete(message)
session.commit()
return True
return False
def delete_messages_by_ip(self, ip: str) -> int:
"""Delete all messages from a specific IP address and return count"""
with self.Session() as session:
messages = session.query(Messages).filter(Messages.ip == ip).all()
count = len(messages)
for message in messages:
session.delete(message)
session.commit()
return count
def get_message_count(self) -> int:
"""Get total number of messages"""
with self.Session() as session:
return session.query(Messages).count()
def get_latest_messages(self, limit: int = 10) -> list:
"""Get the latest N messages"""
with self.Session() as session:
messages = session.query(Messages).order_by(Messages.id.desc()).limit(limit).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
def clear_all_messages(self) -> int:
"""Delete all messages and return count"""
with self.Session() as session:
count = session.query(Messages).count()
session.query(Messages).delete()
session.commit()
return count
def get_messages_by_date_range(self, start_timestamp: int, end_timestamp: int) -> list:
"""Get messages within a timestamp range"""
with self.Session() as session:
messages = session.query(Messages).filter(
Messages.timestamp >= start_timestamp,
Messages.timestamp <= end_timestamp
).order_by(Messages.timestamp.desc()).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
# Optional: Test connection function
def test_connection():
"""Test if MariaDB connection is working"""
try:
with engine.connect() as conn:
result = conn.execute(sqlalchemy.text("SELECT 1"))
print("✅ MariaDB connection successful!")
return True
except Exception as e:
print(f"❌ MariaDB connection failed: {e}")
return False
# Run test if executed directly
if __name__ == "__main__":
test_connection()

View File

@ -3,459 +3,297 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Secure Chat</title>
<title>FuckingChat</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #2c2c2c;
color: #d4d4d4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.chat-container {
background: #3a3a3a;
background: #16213e;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
width: 100%;
max-width: 800px;
max-width: 900px;
height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background: #2f2f2f;
padding: 20px;
border-bottom: 1px solid #4a4a4a;
background: #0f3460;
padding: 16px 20px;
border-bottom: 1px solid #1a4a7a;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h1 {
color: #e0e0e0;
font-size: 20px;
font-weight: 600;
letter-spacing: 0.5px;
color: #e94560;
font-size: 18px;
font-weight: 700;
letter-spacing: 1px;
}
.chat-header .status {
color: #888;
font-size: 12px;
background: #3f3f3f;
background: #1a4a7a;
padding: 4px 12px;
border-radius: 12px;
border: 1px solid #4a4a4a;
color: #a0c4ff;
}
.channel-bar {
display: flex;
background: #1a2744;
padding: 8px 12px;
gap: 6px;
border-bottom: 1px solid #0f3460;
overflow-x: auto;
}
.channel-btn {
padding: 6px 16px;
border: 1px solid #2a4a7a;
border-radius: 20px;
background: transparent;
color: #8899aa;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
transition: all 0.2s;
}
.channel-btn:hover {
background: #1a3a5a;
color: #c0d0e0;
}
.channel-btn.active {
background: #e94560;
color: #fff;
border-color: #e94560;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #333;
padding: 16px 20px;
background: #121e38;
}
.chat-messages::-webkit-scrollbar {
width: 8px;
}
.chat-messages::-webkit-scrollbar-track {
background: #2c2c2c;
}
.chat-messages::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: #666;
}
.chat-messages::-webkit-scrollbar { width: 6px; }
.chat-messages::-webkit-scrollbar-track { background: #0d1528; }
.chat-messages::-webkit-scrollbar-thumb { background: #2a4a7a; border-radius: 3px; }
.message {
margin-bottom: 16px;
display: flex;
flex-direction: column;
}
.message-ip {
font-size: 11px;
color: #888;
margin-bottom: 4px;
font-family: 'Courier New', monospace;
}
.message-content {
background: #404040;
padding: 10px 14px;
background: #1a2d4a;
border-radius: 8px;
border-left: 3px solid #666;
word-wrap: break-word;
line-height: 1.5;
position: relative;
padding-right: 60px;
padding: 10px 14px;
margin-bottom: 8px;
border-left: 3px solid #e94560;
}
.message-content .timestamp {
font-size: 10px;
color: #777;
position: absolute;
right: 10px;
bottom: 6px;
}
.message-actions {
margin-top: 4px;
display: flex;
gap: 8px;
}
.message-actions button {
background: #4a4a4a;
border: none;
color: #aaa;
padding: 2px 10px;
border-radius: 4px;
.message .msg-ip {
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #555;
color: #6a8aaa;
margin-bottom: 4px;
}
.message-actions button:hover {
background: #555;
color: #ddd;
border-color: #666;
.message .msg-content {
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
}
.message-actions button.delete-btn:hover {
background: #5a3a3a;
border-color: #8a4a4a;
color: #ff8888;
.message .msg-time {
font-size: 11px;
color: #556677;
text-align: right;
margin-top: 4px;
}
.message-input-area {
background: #2f2f2f;
padding: 20px;
border-top: 1px solid #4a4a4a;
.empty-state {
text-align: center;
color: #556677;
padding: 40px;
font-size: 14px;
}
.input-area {
background: #0f3460;
padding: 12px 16px;
border-top: 1px solid #1a4a7a;
display: flex;
gap: 12px;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.message-input-area input {
.input-area input {
flex: 1;
padding: 10px 14px;
background: #3f3f3f;
border: 1px solid #4a4a4a;
border: 1px solid #1a4a7a;
border-radius: 8px;
background: #121e38;
color: #e0e0e0;
font-size: 14px;
min-width: 150px;
transition: border-color 0.2s;
}
.message-input-area input:focus {
outline: none;
border-color: #666;
background: #454545;
transition: border 0.2s;
}
.message-input-area input::placeholder {
color: #777;
}
.message-input-area button {
padding: 10px 24px;
background: #4a4a4a;
border: 1px solid #5a5a5a;
.input-area input:focus { border-color: #e94560; }
.input-area input::placeholder { color: #4a6a8a; }
.input-area button {
padding: 10px 20px;
background: #e94560;
color: #fff;
border: none;
border-radius: 8px;
color: #e0e0e0;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
transition: background 0.2s;
}
.message-input-area button:hover {
background: #555;
border-color: #666;
transform: translateY(-1px);
.input-area button:hover { background: #d63850; }
.input-area button:disabled {
background: #4a5a7a;
cursor: not-allowed;
}
.message-input-area button:active {
transform: translateY(0);
}
.loading {
color: #666;
text-align: center;
padding: 20px;
font-style: italic;
}
.empty-state {
color: #666;
text-align: center;
padding: 40px;
font-size: 16px;
}
.toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: #4a4a4a;
color: #e0e0e0;
padding: 10px 24px;
border-radius: 8px;
border: 1px solid #5a5a5a;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
white-space: nowrap;
}
.toast.visible {
opacity: 1;
}
.toast.success {
border-color: #5a8a5a;
background: #3a5a3a;
}
.toast.error {
border-color: #8a5a5a;
background: #5a3a3a;
}
@media (max-width: 600px) {
.chat-container {
height: 100vh;
border-radius: 0;
}
.message-input-area {
flex-direction: column;
}
.message-input-area input {
min-width: unset;
}
.message-input-area button {
width: 100%;
}
.chat-header h1 {
font-size: 16px;
}
.message-content {
padding-right: 50px;
}
z-index: 1000;
}
.toast.visible { opacity: 1; }
.toast.error { background: #c0392b; color: #fff; }
.toast.success { background: #27ae60; color: #fff; }
.toast.info { background: #2980b9; color: #fff; }
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h1>🔒 Secure Chat</h1>
<span class="status">Encrypted</span>
<h1>💬 FuckingChat</h1>
<span class="status" id="status-badge">Encrypted 🔒</span>
</div>
<div class="chat-messages" id="messageContainer">
<div class="loading">Loading messages...</div>
</div>
<div class="message-input-area">
<input
type="text"
id="messageInput"
placeholder="Type your message..."
maxlength="500"
autocomplete="off"
>
<button id="sendButton">Send</button>
<div class="channel-bar" id="channel-bar"></div>
<div class="chat-messages" id="messages"></div>
<div class="input-area">
<input type="text" id="msg-input" placeholder="Type a message..." maxlength="2000">
<button id="send-btn">Send</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
class ChatApp {
constructor() {
this.messageContainer = document.getElementById('messageContainer');
this.messageInput = document.getElementById('messageInput');
this.sendButton = document.getElementById('sendButton');
this.toast = document.getElementById('toast');
this.toastTimeout = null;
this.encryptionEnabled = true;
const CHANNELS = [
{ id: 'general', name: 'General', icon: '💬' },
{ id: 'games', name: 'Games', icon: '🎮' },
{ id: 'anime', name: 'Anime', icon: '🎌' },
{ id: 'politics', name: 'Politics', icon: '⚖️' }
];
this.init();
}
let currentChannel = 'general';
let pollInterval = null;
init() {
this.loadMessages();
this.setupEventListeners();
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('msg-input');
const sendBtn = document.getElementById('send-btn');
const toastEl = document.getElementById('toast');
const channelBar = document.getElementById('channel-bar');
// Auto-refresh every 10 seconds
setInterval(() => this.loadMessages(), 10000);
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
setupEventListeners() {
this.sendButton.addEventListener('click', () => this.sendMessage());
this.messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.sendMessage();
});
}
function showToast(msg, type = 'info') {
toastEl.textContent = msg;
toastEl.className = 'toast visible ' + type;
setTimeout(() => toastEl.classList.remove('visible'), 3000);
}
async sendMessage() {
const message = this.messageInput.value.trim();
if (!message) {
this.showToast('Please enter a message', 'error');
return;
}
function renderChannels() {
channelBar.innerHTML = '';
CHANNELS.forEach(ch => {
const btn = document.createElement('button');
btn.className = 'channel-btn' + (ch.id === currentChannel ? ' active' : '');
btn.textContent = ch.icon + ' ' + ch.name;
btn.onclick = () => switchChannel(ch.id);
channelBar.appendChild(btn);
});
}
this.sendButton.disabled = true;
this.sendButton.textContent = 'Sending...';
function switchChannel(channel) {
if (channel === currentChannel) return;
currentChannel = channel;
renderChannels();
messagesEl.innerHTML = '<div class="empty-state">Loading...</div>';
loadMessages();
}
try {
const response = await fetch('/api/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message })
});
const data = await response.json();
if (data.success) {
this.messageInput.value = '';
this.showToast('Message sent! ✓', 'success');
this.loadMessages(); // Refresh messages
} else {
this.showToast('Error: ' + (data.error || 'Unknown error'), 'error');
}
} catch (error) {
this.showToast('Network error: ' + error.message, 'error');
} finally {
this.sendButton.disabled = false;
this.sendButton.textContent = 'Send';
}
}
async loadMessages() {
try {
const response = await fetch('/api/messages?limit=50');
const data = await response.json();
if (data.success) {
this.renderMessages(data.messages || []);
} else {
this.showToast('Failed to load messages', 'error');
}
} catch (error) {
this.showToast('Network error loading messages', 'error');
}
}
renderMessages(messages) {
if (messages.length === 0) {
this.messageContainer.innerHTML = `
<div class="empty-state">💬 No messages yet. Start the conversation!</div>
`;
return;
}
let html = '';
// Show newest messages at bottom
messages.slice().reverse().forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
html += `
<div class="message" data-id="${msg.id}">
<div class="message-ip">🔑 ${msg.ip}</div>
<div class="message-content">
${this.escapeHtml(msg.content)}
<span class="timestamp">${time}</span>
</div>
<div class="message-actions">
<button onclick="chatApp.deleteMessage(${msg.id})" class="delete-btn">Delete</button>
</div>
</div>
`;
});
this.messageContainer.innerHTML = html;
this.scrollToBottom();
}
async deleteMessage(id) {
if (!confirm('Delete this message?')) return;
try {
const response = await fetch(`/api/messages/${id}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
this.showToast('Message deleted ✓', 'success');
this.loadMessages();
} else {
this.showToast('Failed to delete message', 'error');
}
} catch (error) {
this.showToast('Network error deleting message', 'error');
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
scrollToBottom() {
this.messageContainer.scrollTop = this.messageContainer.scrollHeight;
}
showToast(message, type = 'info') {
this.toast.textContent = message;
this.toast.className = 'toast visible ' + type;
if (this.toastTimeout) {
clearTimeout(this.toastTimeout);
}
this.toastTimeout = setTimeout(() => {
this.toast.classList.remove('visible');
}, 3000);
async function loadMessages() {
try {
const r = await fetch(`/api/messages?channel=${currentChannel}&limit=50`);
const data = await r.json();
if (data.success) renderMessages(data.messages || []);
else showToast('Failed to load messages', 'error');
} catch (e) {
showToast('Network error', 'error');
}
}
// Initialize the chat app
const chatApp = new ChatApp();
function renderMessages(msgs) {
if (msgs.length === 0) {
messagesEl.innerHTML = '<div class="empty-state">💬 No messages yet. Be the first!</div>';
return;
}
let html = '';
msgs.forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
html += `<div class="message">
<div class="msg-ip">🔑 ${escapeHtml(msg.ip)}</div>
<div class="msg-content">${escapeHtml(msg.content)}</div>
<div class="msg-time">${time}</div>
</div>`;
});
messagesEl.innerHTML = html;
messagesEl.scrollTop = messagesEl.scrollHeight;
}
async function sendMessage() {
const text = inputEl.value.trim();
if (!text) return;
sendBtn.disabled = true;
try {
const r = await fetch('/api/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel: currentChannel, message: text })
});
const data = await r.json();
if (data.success) {
inputEl.value = '';
loadMessages();
} else {
showToast(data.error || 'Failed to send', 'error');
}
} catch (e) {
showToast('Network error', 'error');
}
sendBtn.disabled = false;
}
// Init
renderChannels();
loadMessages();
sendBtn.addEventListener('click', sendMessage);
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); });
// Auto-poll every 3 seconds
setInterval(() => loadMessages(), 3000);
</script>
</body>
</html>

254
index.py
View File

@ -1,21 +1,43 @@
from bottle import route, request, response, run, static_file
from bottle import route, request, response, run, static_file, template
import hashlib
import time
import json
import os
import re
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import sqlalchemy
from sqlalchemy import create_engine, String, Integer, DateTime
from sqlalchemy import create_engine, String, Integer, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlalchemy.orm import sessionmaker
from datetime import datetime
# Encryption setup
SALT = b'salt_123456789' # In production, use a random salt
PASSWORD = b'chat_secret_key_123' # In production, use environment variable
# --- Security config from environment ---
SALT = os.environ.get('CHAT_SALT', 'default_salt_change_me').encode()
PASSWORD = os.environ.get('CHAT_SECRET', 'default_secret_change_me').encode()
ADMIN_TOKEN = os.environ.get('CHAT_ADMIN_TOKEN', None)
RATE_LIMIT = int(os.environ.get('CHAT_RATE_LIMIT', 5)) # messages per window
RATE_WINDOW = int(os.environ.get('CHAT_RATE_WINDOW', 10)) # seconds
MAX_MSG_LENGTH = int(os.environ.get('CHAT_MAX_MSG', 2000))
DB_PATH = os.environ.get('CHAT_DB_PATH', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chat.db'))
# --- Rate limiting (in-memory) ---
rate_store = {}
def check_rate_limit(ip):
now = time.time()
window_start = now - RATE_WINDOW
if ip in rate_store:
rate_store[ip] = [t for t in rate_store[ip] if t > window_start]
if len(rate_store[ip]) >= RATE_LIMIT:
return False
rate_store[ip].append(now)
else:
rate_store[ip] = [now]
return True
# --- Encryption ---
def get_cipher():
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
@ -33,11 +55,12 @@ class Messages(Base):
__tablename__ = "messages"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
ip: Mapped[str] = mapped_column(String, nullable=False)
content: Mapped[str] = mapped_column(String, nullable=False)
channel: Mapped[str] = mapped_column(String(32), nullable=False, default='general')
ip: Mapped[str] = mapped_column(String(64), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
timestamp: Mapped[int] = mapped_column(Integer, nullable=False)
engine = create_engine("sqlite:///chat.db", echo=False)
engine = create_engine(f"sqlite:///{DB_PATH}", echo=False)
Base.metadata.create_all(engine)
class DBfrontend():
@ -45,136 +68,109 @@ class DBfrontend():
self.Session = sessionmaker(bind=engine)
self.cipher = get_cipher()
def encrypt_message(self, message: str) -> str:
"""Encrypt message before storing"""
def _encrypt(self, message):
return self.cipher.encrypt(message.encode()).decode()
def decrypt_message(self, encrypted_message: str) -> str:
"""Decrypt message after retrieval"""
def _decrypt(self, encrypted):
try:
return self.cipher.decrypt(encrypted_message.encode()).decode()
return self.cipher.decrypt(encrypted.encode()).decode()
except:
return "[Decryption failed]"
def add_message(self, ip: str, content: str) -> int:
"""Add a new encrypted message to the database"""
def add_message(self, channel, ip, content):
with self.Session() as session:
encrypted_content = self.encrypt_message(content)
new_message = Messages(
msg = Messages(
channel=channel,
ip=ip,
content=encrypted_content,
content=self._encrypt(content),
timestamp=int(time.time() * 1000)
)
session.add(new_message)
session.add(msg)
session.commit()
return new_message.id
return msg.id
def get_all_messages(self) -> list:
"""Get all messages and decrypt them"""
def get_messages(self, channel='general', limit=50):
with self.Session() as session:
messages = session.query(Messages).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
q = session.query(Messages).filter(Messages.channel == channel)
q = q.order_by(Messages.id.desc()).limit(limit)
msgs = q.all()
result = []
for msg in reversed(msgs):
result.append({
'id': msg.id,
'channel': msg.channel,
'ip': msg.ip,
'content': self._decrypt(msg.content),
'timestamp': msg.timestamp
})
return result
def get_message_by_id(self, message_id: int) -> Messages | None:
"""Get a specific message by its ID"""
def delete_message(self, msg_id):
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
message.content = self.decrypt_message(message.content)
return message
def get_messages_by_ip(self, ip: str) -> list:
"""Get all messages from a specific IP address"""
with self.Session() as session:
messages = session.query(Messages).filter(Messages.ip == ip).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
def update_message(self, message_id: int, new_content: str) -> bool:
"""Update the content of a message"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
message.content = self.encrypt_message(new_content)
msg = session.query(Messages).get(msg_id)
if msg:
session.delete(msg)
session.commit()
return True
return False
def delete_message(self, message_id: int) -> bool:
"""Delete a message by its ID"""
with self.Session() as session:
message = session.query(Messages).get(message_id)
if message:
session.delete(message)
session.commit()
return True
return False
def delete_messages_by_ip(self, ip: str) -> int:
"""Delete all messages from a specific IP address"""
with self.Session() as session:
messages = session.query(Messages).filter(Messages.ip == ip).all()
count = len(messages)
for message in messages:
session.delete(message)
session.commit()
return count
def get_message_count(self) -> int:
"""Get total number of messages"""
with self.Session() as session:
return session.query(Messages).count()
def get_latest_messages(self, limit: int = 10) -> list:
"""Get the latest N messages"""
with self.Session() as session:
messages = session.query(Messages).order_by(Messages.id.desc()).limit(limit).all()
for msg in messages:
msg.content = self.decrypt_message(msg.content)
return messages
# Initialize database
db = DBfrontend()
# Routes
import os
VALID_CHANNELS = {'general', 'games', 'anime', 'politics'}
CHANNEL_NAMES = {
'general': 'General',
'games': 'Games',
'anime': 'Anime',
'politics': 'Politics'
}
@route('/')
def serve_frontend():
# Получаем путь к директории, где находится index.py
current_dir = os.path.dirname(os.path.abspath(__file__))
return static_file('index.html', root=current_dir)
return static_file('index.html', root=os.path.dirname(os.path.abspath(__file__)))
@route('/api/channels', method='GET')
def list_channels():
response.content_type = 'application/json'
channels = [{'id': k, 'name': v} for k, v in CHANNEL_NAMES.items()]
return {'success': True, 'channels': channels}
@route('/api/messages', method='POST')
def post_message():
ip = request.environ.get('REMOTE_ADDR', '127.0.0.1')
if not check_rate_limit(ip):
response.status = 429
return {'success': False, 'error': 'Rate limit exceeded. Slow down.'}
try:
data = request.json
if not data or 'message' not in data:
if not data:
response.status = 400
return {'success': False, 'error': 'No message provided'}
return {'success': False, 'error': 'No data provided'}
message = data.get('message', '').strip()
if not message:
channel = data.get('channel', 'general')
content = data.get('message', '').strip()
if channel not in VALID_CHANNELS:
response.status = 400
return {'success': False, 'error': 'Invalid channel'}
if not content:
response.status = 400
return {'success': False, 'error': 'Empty message'}
# Get IP hash for privacy
ip_hash = hashlib.sha256(('salt' + ip).encode('utf-8')).hexdigest()[:16]
if len(content) > MAX_MSG_LENGTH:
response.status = 400
return {'success': False, 'error': f'Message too long (max {MAX_MSG_LENGTH} chars)'}
# Save message with encrypted content
message_id = db.add_message(ip_hash, message)
# XSS filter - strip HTML tags
content = re.sub(r'<[^>]*>', '', content)
ip_hash = hashlib.sha256((os.environ.get('CHAT_SALT', 'salt') + ip).encode()).hexdigest()[:16]
msg_id = db.add_message(channel, ip_hash, content)
response.content_type = 'application/json'
return {
'success': True,
'message_id': message_id,
'ip': ip_hash
}
return {'success': True, 'message_id': msg_id, 'ip': ip_hash}
except Exception as e:
response.status = 500
@ -183,42 +179,15 @@ def post_message():
@route('/api/messages', method='GET')
def get_messages():
try:
limit = int(request.query.get('limit', 20))
messages = db.get_latest_messages(limit)
channel = request.query.get('channel', 'general')
limit = min(int(request.query.get('limit', 50)), 200)
result = []
for msg in messages:
result.append({
'id': msg.id,
'ip': msg.ip,
'content': msg.content,
'timestamp': msg.timestamp
})
if channel not in VALID_CHANNELS:
channel = 'general'
messages = db.get_messages(channel=channel, limit=limit)
response.content_type = 'application/json'
return {'success': True, 'messages': result}
except Exception as e:
response.status = 500
return {'success': False, 'error': str(e)}
@route('/api/messages/<message_id>', method='PUT')
def update_message(message_id):
try:
data = request.json
if not data or 'message' not in data:
response.status = 400
return {'success': False, 'error': 'No message provided'}
new_content = data.get('message', '').strip()
if not new_content:
response.status = 400
return {'success': False, 'error': 'Empty message'}
success = db.update_message(int(message_id), new_content)
response.content_type = 'application/json'
return {'success': success}
return {'success': True, 'messages': messages}
except Exception as e:
response.status = 500
@ -226,15 +195,28 @@ def update_message(message_id):
@route('/api/messages/<message_id>', method='DELETE')
def delete_message(message_id):
# Admin token required for deletion
auth = request.get_header('Authorization', '')
token = auth.replace('Bearer ', '')
if ADMIN_TOKEN and token != ADMIN_TOKEN:
response.status = 403
return {'success': False, 'error': 'Forbidden'}
try:
success = db.delete_message(int(message_id))
response.content_type = 'application/json'
return {'success': success}
except Exception as e:
response.status = 500
return {'success': False, 'error': str(e)}
if __name__ == '__main__':
run(host='localhost', port=8080, debug=True)
host = os.environ.get('CHAT_HOST', 'localhost')
port = int(os.environ.get('CHAT_PORT', 8080))
debug_mode = os.environ.get('CHAT_DEBUG', '0') == '1'
if debug_mode:
print(f"⚠️ WARNING: Debug mode is ON - do not use in production!")
run(host=host, port=port, debug=debug_mode)

View File

@ -1,2 +0,0 @@
[tool.pyright]
python.pythonPath = "~/myenv/bin/python"