ok?
This commit is contained in:
parent
0e9d6c681f
commit
c5ad7e8cb4
24
.gitignore
vendored
24
.gitignore
vendored
@ -1 +1,25 @@
|
||||
config.php
|
||||
.env
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
*.log
|
||||
logs/
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
docker-compose.override.yml
|
||||
*.sql
|
||||
*.db
|
||||
cache/
|
||||
tmp/
|
||||
sessions/
|
||||
*.bak
|
||||
*.backup
|
||||
|
50
AnonymousID.php
Normal file
50
AnonymousID.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
class AnonymousID {
|
||||
private $db;
|
||||
private $ip;
|
||||
private $boardId;
|
||||
|
||||
public function __construct($db, $ip, $boardId) {
|
||||
$this->db = $db;
|
||||
$this->ip = $ip;
|
||||
$this->boardId = $boardId;
|
||||
}
|
||||
|
||||
public function getOrCreateID() {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT anonymous_id FROM posts
|
||||
WHERE ip_address = ? AND board_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([$this->ip, $this->boardId]);
|
||||
$existingId = $stmt->fetchColumn();
|
||||
|
||||
if ($existingId) {
|
||||
return $existingId;
|
||||
}
|
||||
|
||||
return $this->generateNewID();
|
||||
}
|
||||
|
||||
private function generateNewID() {
|
||||
return 'ID:' . sprintf('%06d', mt_rand(1, 999999));
|
||||
}
|
||||
|
||||
public function getIDForThread() {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT anonymous_id FROM threads
|
||||
WHERE ip_address = ? AND board_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([$this->ip, $this->boardId]);
|
||||
$existingId = $stmt->fetchColumn();
|
||||
|
||||
if ($existingId) {
|
||||
return $existingId;
|
||||
}
|
||||
|
||||
return $this->generateNewID();
|
||||
}
|
||||
}
|
17
MarkdownParser.php
Normal file
17
MarkdownParser.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
class MarkdownParser {
|
||||
public static function parse($text) {
|
||||
$text = htmlspecialchars($text);
|
||||
$text = preg_replace('/>>(\d{6})/', '<a href="#$1" class="reply-link">>>$1</a>', $text);
|
||||
$text = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $text);
|
||||
$text = preg_replace('/\*(.*?)\*/', '<em>$1</em>', $text);
|
||||
$text = preg_replace('/_(.*?)_/', '<u>$1</u>', $text);
|
||||
$text = preg_replace('/~~(.*?)~~/', '<del>$1</del>', $text);
|
||||
$text = preg_replace('/`(.*?)`/', '<code>$1</code>', $text);
|
||||
$text = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2" target="_blank">$1</a>', $text);
|
||||
$text = preg_replace('/^\* (.+)$/m', '<li>$1</li>', $text);
|
||||
$text = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $text);
|
||||
$text = nl2br($text);
|
||||
return $text;
|
||||
}
|
||||
}
|
42
README
42
README
@ -1,10 +1,44 @@
|
||||
mkach - анонимный имиджборд
|
||||
|
||||
Установка:
|
||||
1. Создайте базу данных MySQL
|
||||
2. Импортируйте sql/create.sql
|
||||
3. Настройте config.php
|
||||
4. Создайте папку uploads/ с правами 755
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
git clone <url>
|
||||
cd mkach
|
||||
|
||||
2. Создайте базу данных MySQL:
|
||||
CREATE DATABASE mkach;
|
||||
CREATE USER 'mkach'@'localhost' IDENTIFIED BY 'your_password';
|
||||
GRANT ALL PRIVILEGES ON mkach.* TO 'mkach'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
3. Импортируйте схему:
|
||||
mysql -u mkach -p mkach < sql/create.sql
|
||||
|
||||
4. Настройте config.php:
|
||||
cp config.php.example config.php
|
||||
# Отредактируйте параметры БД
|
||||
|
||||
5. Создайте папку uploads:
|
||||
mkdir uploads
|
||||
chmod 755 uploads
|
||||
|
||||
6. Настройте Apache:
|
||||
# Ubuntu/Debian:
|
||||
sudo cp configs/apache.conf.debian /etc/apache2/sites-available/mkach
|
||||
sudo a2ensite mkach
|
||||
sudo systemctl reload apache2
|
||||
|
||||
# CentOS/RHEL:
|
||||
sudo cp configs/apache.conf.centos /etc/httpd/conf.d/mkach.conf
|
||||
sudo systemctl reload httpd
|
||||
|
||||
7. Настройте PHP:
|
||||
sudo cp configs/php.ini /etc/php/8.1/apache2/conf.d/mkach.ini
|
||||
sudo systemctl reload apache2
|
||||
|
||||
Docker установка:
|
||||
docker-compose up -d
|
||||
|
||||
Особенности:
|
||||
- Анонимные посты без регистрации
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?php
|
||||
class RateLimiter {
|
||||
private $db;
|
||||
private $maxRequests = 3;
|
||||
private $timeWindow = 30;
|
||||
private $maxRequests = 30;
|
||||
private $timeWindow = 6;
|
||||
|
||||
public function __construct($db) {
|
||||
$this->db = $db;
|
||||
|
@ -1,6 +1,9 @@
|
||||
Listen 65511
|
||||
|
||||
<VirtualHost *:65511>
|
||||
DocumentRoot /var/www/html
|
||||
ServerName localhost
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
<Directory /var/www/html>
|
||||
AllowOverride All
|
||||
|
257
board.php
257
board.php
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
session_start();
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
@ -10,6 +11,8 @@ if (!isset($_SESSION['authenticated'])) {
|
||||
}
|
||||
|
||||
$config = require 'config.php';
|
||||
$boardId = $_GET['board'] ?? 'b';
|
||||
$threadId = $_GET['thread'] ?? null;
|
||||
|
||||
try {
|
||||
$db = new PDO(
|
||||
@ -23,10 +26,13 @@ try {
|
||||
}
|
||||
|
||||
require_once 'RateLimiter.php';
|
||||
require_once 'AnonymousID.php';
|
||||
require_once 'MarkdownParser.php';
|
||||
$rateLimiter = new RateLimiter($db);
|
||||
$rateLimiter->cleanup();
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
$anonymousID = new AnonymousID($db, $ip, $boardId);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$rateLimiter->isAllowed($ip)) {
|
||||
@ -34,8 +40,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} else {
|
||||
$message = trim($_POST['message'] ?? '');
|
||||
$file = $_FILES['file'] ?? null;
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if ($message || ($file && $file['error'] === UPLOAD_ERR_OK)) {
|
||||
if ($message || $title || ($file && $file['error'] === UPLOAD_ERR_OK)) {
|
||||
$postId = sprintf('%06d', mt_rand(1, 999999));
|
||||
|
||||
$fileName = null;
|
||||
@ -68,13 +76,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
if (empty($error)) {
|
||||
$stmt = $db->prepare('
|
||||
INSERT INTO posts (post_id, message, file_name, file_size, file_type, ip_address)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([$postId, $message, $fileName, $fileSize, $fileType, $ip]);
|
||||
if ($threadId) {
|
||||
$anonymousId = $anonymousID->getOrCreateID();
|
||||
$stmt = $db->prepare('
|
||||
INSERT INTO posts (post_id, thread_id, board_id, message, file_name, file_size, file_type, ip_address, anonymous_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([$postId, $threadId, $boardId, $message, $fileName, $fileSize, $fileType, $ip, $anonymousId]);
|
||||
} else {
|
||||
header('Location: newthread.php?board=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: board.php');
|
||||
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
@ -83,32 +97,77 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['logout'])) {
|
||||
session_destroy();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $db->query('SELECT * FROM posts ORDER BY created_at DESC LIMIT 100');
|
||||
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$db->exec('SET NAMES utf8');
|
||||
$stmt = $db->prepare('SELECT * FROM boards WHERE board_id = ?');
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$board) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die('Database error');
|
||||
}
|
||||
|
||||
if ($threadId) {
|
||||
try {
|
||||
$stmt = $db->prepare('
|
||||
SELECT p.*, t.title as thread_title
|
||||
FROM posts p
|
||||
JOIN threads t ON p.thread_id = t.thread_id
|
||||
WHERE p.thread_id = ? AND p.board_id = ?
|
||||
ORDER BY p.created_at ASC
|
||||
');
|
||||
$stmt->execute([$threadId, $boardId]);
|
||||
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($posts)) {
|
||||
header('Location: board.php?board=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die('Database error');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$stmt = $db->prepare('
|
||||
SELECT t.*,
|
||||
COUNT(p.id) as post_count,
|
||||
MAX(p.created_at) as last_post_time
|
||||
FROM threads t
|
||||
LEFT JOIN posts p ON t.thread_id = p.thread_id
|
||||
WHERE t.board_id = ?
|
||||
GROUP BY t.id
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT 20
|
||||
');
|
||||
$stmt->execute([$boardId]);
|
||||
$threads = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
die('Database error');
|
||||
}
|
||||
}
|
||||
|
||||
function formatMessage($message) {
|
||||
return MarkdownParser::parse($message);
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>mkach</title>
|
||||
<title>mkach - /<?= htmlspecialchars($boardId) ?>/</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>mkach</h1>
|
||||
<h1><a href="index.php" class="home-link">mkach</a> - /<?= htmlspecialchars($boardId) ?>/ - <?= htmlspecialchars($board['name']) ?></h1>
|
||||
<div class="header-buttons">
|
||||
<a href="index.php" class="boards-btn">Доски</a>
|
||||
<a href="?logout=1" class="logout-btn">Выход</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -118,70 +177,126 @@ try {
|
||||
<div class="error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="posts-container" id="posts">
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<div class="post">
|
||||
<div class="post-header">
|
||||
<span class="post-id">№<?= htmlspecialchars($post['post_id']) ?></span>
|
||||
<span class="post-time"><?= date('d.m.Y H:i:s', strtotime($post['created_at'])) ?></span>
|
||||
</div>
|
||||
|
||||
<?php if ($post['file_name']): ?>
|
||||
<div class="post-file">
|
||||
<a href="uploads/<?= htmlspecialchars($post['file_name']) ?>" target="_blank">
|
||||
<img src="uploads/<?= htmlspecialchars($post['file_name']) ?>" alt="File" class="post-image">
|
||||
</a>
|
||||
<div class="file-info">
|
||||
<?= htmlspecialchars($post['file_name']) ?>
|
||||
(<?= number_format($post['file_size'] / 1024, 1) ?> KB)
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($post['message']): ?>
|
||||
<div class="post-message"><?= nl2br(htmlspecialchars($post['message'])) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($threadId): ?>
|
||||
<div class="thread-container">
|
||||
<div class="thread-header">
|
||||
<h2><?= htmlspecialchars($posts[0]['thread_title'] ?? 'Без названия') ?></h2>
|
||||
<a href="board.php?board=<?= htmlspecialchars($boardId) ?>" class="back-link">← Назад к списку</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="posts-container" id="posts">
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<div class="post" id="<?= htmlspecialchars($post['post_id']) ?>">
|
||||
<div class="post-header">
|
||||
<span class="post-id">№<?= htmlspecialchars($post['post_id']) ?></span>
|
||||
<?php if ($post['anonymous_id']): ?>
|
||||
<span class="anonymous-id"><?= htmlspecialchars($post['anonymous_id']) ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="post-time"><?= date('d.m.Y H:i:s', strtotime($post['created_at'])) ?></span>
|
||||
</div>
|
||||
|
||||
<?php if ($post['file_name']): ?>
|
||||
<div class="post-file">
|
||||
<a href="uploads/<?= htmlspecialchars($post['file_name']) ?>" target="_blank">
|
||||
<img src="uploads/<?= htmlspecialchars($post['file_name']) ?>" alt="File" class="post-image">
|
||||
</a>
|
||||
<div class="file-info">
|
||||
<?= htmlspecialchars($post['file_name']) ?>
|
||||
(<?= number_format($post['file_size'] / 1024, 1) ?> KB)
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($post['message']): ?>
|
||||
<div class="post-message"><?= formatMessage($post['message']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="threads-container">
|
||||
<div class="threads-list" id="threads">
|
||||
<?php foreach ($threads as $thread): ?>
|
||||
<div class="thread-item">
|
||||
<div class="thread-header">
|
||||
<a href="board.php?board=<?= htmlspecialchars($boardId) ?>&thread=<?= htmlspecialchars($thread['thread_id']) ?>" class="thread-link">
|
||||
<span class="thread-title"><?= htmlspecialchars($thread['title'] ?? 'Без названия') ?></span>
|
||||
</a>
|
||||
<span class="thread-info">
|
||||
Постов: <?= $thread['post_count'] ?> |
|
||||
Обновлено: <?= date('d.m.Y H:i', strtotime($thread['last_post_time'] ?? $thread['created_at'])) ?>
|
||||
<?php if ($thread['anonymous_id']): ?>
|
||||
| <?= htmlspecialchars($thread['anonymous_id']) ?>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php if ($thread['file_name']): ?>
|
||||
<div class="thread-file">
|
||||
<a href="uploads/<?= htmlspecialchars($thread['file_name']) ?>" target="_blank">
|
||||
<img src="uploads/<?= htmlspecialchars($thread['file_name']) ?>" alt="File" class="thread-image">
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($thread['description']): ?>
|
||||
<div class="thread-description"><?= formatMessage($thread['description']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="post-form">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="form-row">
|
||||
<textarea name="message" placeholder="Сообщение (необязательно)" class="message-input"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input type="file" name="file" accept=".jpg,.jpeg,.png,.gif,.webp" class="file-input">
|
||||
<button type="submit" class="send-btn">Отправить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php if (!$threadId): ?>
|
||||
<div class="new-thread-button">
|
||||
<a href="newthread.php?board=<?= htmlspecialchars($boardId) ?>" class="new-thread-btn">Создать новый тред</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="post-form">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="form-row">
|
||||
<textarea name="message" placeholder="Сообщение (поддерживает Markdown)" class="message-input"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input type="file" name="file" accept=".jpg,.jpeg,.png,.gif,.webp" class="file-input">
|
||||
<button type="submit" class="send-btn">Отправить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let isScrolledToBottom = true;
|
||||
const postsContainer = document.getElementById('posts');
|
||||
const container = document.getElementById('posts') || document.getElementById('threads');
|
||||
|
||||
postsContainer.addEventListener('scroll', () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = postsContainer;
|
||||
isScrolledToBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 1;
|
||||
});
|
||||
if (container) {
|
||||
container.addEventListener('scroll', () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||
isScrolledToBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 1;
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
fetch(window.location.href)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const oldHeight = postsContainer.scrollHeight;
|
||||
postsContainer.innerHTML = doc.getElementById('posts').innerHTML;
|
||||
setInterval(() => {
|
||||
fetch(window.location.href)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newContainer = doc.getElementById('posts') || doc.getElementById('threads');
|
||||
if (newContainer) {
|
||||
const oldHeight = container.scrollHeight;
|
||||
container.innerHTML = newContainer.innerHTML;
|
||||
|
||||
if (isScrolledToBottom) {
|
||||
postsContainer.scrollTop = postsContainer.scrollHeight;
|
||||
}
|
||||
});
|
||||
}, 10000);
|
||||
if (isScrolledToBottom) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -8,6 +8,9 @@ return [
|
||||
],
|
||||
'access_key' => 'mkalwaysthebest1337',
|
||||
'upload_path' => 'uploads/',
|
||||
'max_file_size' => 5242880,
|
||||
'allowed_types' => ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
||||
'max_file_size' => 26214400,
|
||||
'allowed_types' => ['jpg', 'jpeg', 'png', 'gif', 'webp','JPG','JPEG','PNG','GIF','WEBP'],
|
||||
'motd' => 'Добро пожаловать на mkach - анонимный имиджборд от МК',
|
||||
'logo_enabled' => true,
|
||||
'logo_text' => 'mkach'
|
||||
];
|
16
config.php.example
Normal file
16
config.php.example
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
return [
|
||||
'db' => [
|
||||
'host' => $_ENV['DB_HOST'] ?? 'mysql',
|
||||
'name' => $_ENV['DB_NAME'] ?? 'mkach',
|
||||
'user' => $_ENV['DB_USER'] ?? 'mkach',
|
||||
'pass' => $_ENV['DB_PASS'] ?? 'mkach'
|
||||
],
|
||||
'access_key' => '129381923812093780198273098172',
|
||||
'upload_path' => 'uploads/',
|
||||
'max_file_size' => 26214400,
|
||||
'allowed_types' => ['jpg', 'jpeg', 'png', 'gif', 'webp','JPG','JPEG','PNG','GIF','WEBP'],
|
||||
'motd' => 'Добро пожаловать на mkach - анонимный имиджборд от МК',
|
||||
'logo_enabled' => true,
|
||||
'logo_text' => 'mkach'
|
||||
];
|
17
configs/apache.conf
Normal file
17
configs/apache.conf
Normal file
@ -0,0 +1,17 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /var/www/html/mkach
|
||||
ServerName localhost
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
<Directory /var/www/html/mkach>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /var/www/html/mkach/uploads>
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/mkach_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/mkach_access.log combined
|
||||
</VirtualHost>
|
17
configs/apache.conf.centos
Normal file
17
configs/apache.conf.centos
Normal file
@ -0,0 +1,17 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /var/www/html/mkach
|
||||
ServerName localhost
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
<Directory /var/www/html/mkach>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /var/www/html/mkach/uploads>
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog /var/log/httpd/mkach_error.log
|
||||
CustomLog /var/log/httpd/mkach_access.log combined
|
||||
</VirtualHost>
|
17
configs/apache.conf.debian
Normal file
17
configs/apache.conf.debian
Normal file
@ -0,0 +1,17 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /var/www/html/mkach
|
||||
ServerName localhost
|
||||
AddDefaultCharset UTF-8
|
||||
|
||||
<Directory /var/www/html/mkach>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /var/www/html/mkach/uploads>
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/mkach_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/mkach_access.log combined
|
||||
</VirtualHost>
|
7
configs/php.ini
Normal file
7
configs/php.ini
Normal file
@ -0,0 +1,7 @@
|
||||
upload_max_filesize = 25M
|
||||
post_max_size = 26M
|
||||
max_execution_time = 30
|
||||
memory_limit = 128M
|
||||
file_uploads = On
|
||||
max_file_uploads = 1
|
||||
default_charset = UTF-8
|
70
index.php
70
index.php
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
session_start();
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
|
||||
$config = require 'config.php';
|
||||
|
||||
@ -11,7 +13,7 @@ if (!isset($_SESSION['authenticated'])) {
|
||||
$key = $_POST['access_key'] ?? '';
|
||||
if (hash('sha256', $key) === hash('sha256', $config['access_key'])) {
|
||||
$_SESSION['authenticated'] = true;
|
||||
header('Location: board.php');
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Неверный ключ';
|
||||
@ -100,4 +102,68 @@ if (!isset($_SESSION['authenticated'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: board.php');
|
||||
try {
|
||||
$db = new PDO(
|
||||
"mysql:host={$config['db']['host']};dbname={$config['db']['name']}",
|
||||
$config['db']['user'],
|
||||
$config['db']['pass']
|
||||
);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
die('Connection failed');
|
||||
}
|
||||
|
||||
try {
|
||||
$db->exec('SET NAMES utf8');
|
||||
$stmt = $db->query('SELECT * FROM boards ORDER BY board_id');
|
||||
$boards = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
die('Database error');
|
||||
}
|
||||
|
||||
if (isset($_GET['logout'])) {
|
||||
session_destroy();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>mkach - Доски</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><?= $config['logo_enabled'] ? htmlspecialchars($config['logo_text']) : 'mkach' ?></h1>
|
||||
<div class="header-buttons">
|
||||
<a href="?logout=1" class="logout-btn">Выход</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($config['motd']): ?>
|
||||
<div class="motd-container">
|
||||
<div class="motd"><?= htmlspecialchars($config['motd']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="boards-container">
|
||||
<h2>Доски</h2>
|
||||
<div class="boards-list">
|
||||
<?php foreach ($boards as $board): ?>
|
||||
<div class="board-item">
|
||||
<a href="board.php?board=<?= htmlspecialchars($board['board_id']) ?>" class="board-link">
|
||||
<span class="board-id">/<?= htmlspecialchars($board['board_id']) ?>/</span>
|
||||
<span class="board-name"><?= htmlspecialchars($board['name']) ?></span>
|
||||
<span class="board-desc"><?= htmlspecialchars($board['description']) ?></span>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
186
newthread.php
Normal file
186
newthread.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
session_start();
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
|
||||
if (!isset($_SESSION['authenticated'])) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$config = require 'config.php';
|
||||
$boardId = $_GET['board'] ?? 'b';
|
||||
|
||||
try {
|
||||
$db = new PDO(
|
||||
"mysql:host={$config['db']['host']};dbname={$config['db']['name']}",
|
||||
$config['db']['user'],
|
||||
$config['db']['pass']
|
||||
);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
die('Connection failed');
|
||||
}
|
||||
|
||||
require_once 'RateLimiter.php';
|
||||
require_once 'AnonymousID.php';
|
||||
$rateLimiter = new RateLimiter($db);
|
||||
$rateLimiter->cleanup();
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
$anonymousID = new AnonymousID($db, $ip, $boardId);
|
||||
|
||||
try {
|
||||
$db->exec('SET NAMES utf8');
|
||||
$stmt = $db->prepare('SELECT * FROM boards WHERE board_id = ?');
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$board) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die('Database error');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$rateLimiter->isAllowed($ip)) {
|
||||
$error = 'Слишком много запросов';
|
||||
} else {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$message = trim($_POST['message'] ?? '');
|
||||
$file = $_FILES['file'] ?? null;
|
||||
|
||||
if ($title && ($description || $message || ($file && $file['error'] === UPLOAD_ERR_OK))) {
|
||||
$threadId = sprintf('%06d', mt_rand(1, 999999));
|
||||
$postId = sprintf('%06d', mt_rand(1, 999999));
|
||||
$anonymousId = $anonymousID->getIDForThread();
|
||||
|
||||
$fileName = null;
|
||||
$fileSize = null;
|
||||
$fileType = null;
|
||||
|
||||
if ($file && $file['error'] === UPLOAD_ERR_OK) {
|
||||
$fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($fileExt, $config['allowed_types'])) {
|
||||
$error = 'Неподдерживаемый тип файла';
|
||||
} elseif ($file['size'] > $config['max_file_size']) {
|
||||
$error = 'Файл слишком большой';
|
||||
} else {
|
||||
$uploadDir = $config['upload_path'];
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$fileName = $threadId . '.' . $fileExt;
|
||||
$filePath = $uploadDir . $fileName;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $filePath)) {
|
||||
$fileSize = $file['size'];
|
||||
$fileType = $fileExt;
|
||||
} else {
|
||||
$error = 'Ошибка загрузки файла';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($error)) {
|
||||
$stmt = $db->prepare('
|
||||
INSERT INTO threads (thread_id, board_id, title, description, file_name, file_size, file_type, ip_address, anonymous_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([$threadId, $boardId, $title, $description, $fileName, $fileSize, $fileType, $ip, $anonymousId]);
|
||||
|
||||
if ($message) {
|
||||
$stmt = $db->prepare('
|
||||
INSERT INTO posts (post_id, thread_id, board_id, message, ip_address, anonymous_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([$postId, $threadId, $boardId, $message, $ip, $anonymousId]);
|
||||
}
|
||||
|
||||
header('Location: board.php?board=' . $boardId . '&thread=' . $threadId);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$error = 'Введите название треда и хотя бы описание, сообщение или файл';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>mkach - Создать тред в /<?= htmlspecialchars($boardId) ?>/</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><a href="index.php" class="home-link">mkach</a> - Создать тред в /<?= htmlspecialchars($boardId) ?>/</h1>
|
||||
<div class="header-buttons">
|
||||
<a href="board.php?board=<?= htmlspecialchars($boardId) ?>" class="boards-btn">← Назад</a>
|
||||
<a href="index.php" class="boards-btn">Доски</a>
|
||||
<a href="?logout=1" class="logout-btn">Выход</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="new-thread-container">
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="thread-form">
|
||||
<h2>Создать новый тред</h2>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="form-row">
|
||||
<label>Название треда:</label>
|
||||
<input type="text" name="title" placeholder="Введите название треда" class="title-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Описание:</label>
|
||||
<textarea name="description" placeholder="Описание треда (поддерживает Markdown)" class="description-input"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Первое сообщение (необязательно):</label>
|
||||
<textarea name="message" placeholder="Первое сообщение в треде (поддерживает Markdown)" class="message-input"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Файл (необязательно):</label>
|
||||
<input type="file" name="file" accept=".jpg,.jpeg,.png,.gif,.webp" class="file-input">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<button type="submit" class="send-btn">Создать тред</button>
|
||||
<a href="board.php?board=<?= htmlspecialchars($boardId) ?>" class="cancel-btn">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="markdown-help">
|
||||
<h3>Поддерживаемые теги Markdown:</h3>
|
||||
<ul>
|
||||
<li><code>**жирный**</code> - жирный текст</li>
|
||||
<li><code>*курсив*</code> - курсив</li>
|
||||
<li><code>_подчеркнутый_</code> - подчеркнутый</li>
|
||||
<li><code>~~зачеркнутый~~</code> - зачеркнутый</li>
|
||||
<li><code>`код`</code> - код</li>
|
||||
<li><code>>>123456</code> - ссылка на пост</li>
|
||||
<li><code>[текст](url)</code> - ссылка</li>
|
||||
<li><code>* элемент</code> - список</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,14 +1,51 @@
|
||||
CREATE TABLE boards (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
board_id VARCHAR(10) NOT NULL UNIQUE,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_board_id (board_id)
|
||||
);
|
||||
|
||||
CREATE TABLE threads (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
thread_id VARCHAR(6) NOT NULL UNIQUE,
|
||||
board_id VARCHAR(10) NOT NULL,
|
||||
title VARCHAR(255),
|
||||
description TEXT,
|
||||
file_name VARCHAR(255),
|
||||
file_size INT,
|
||||
file_type VARCHAR(10),
|
||||
ip_address VARCHAR(45),
|
||||
anonymous_id VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_thread_id (thread_id),
|
||||
INDEX idx_board_id (board_id),
|
||||
INDEX idx_updated_at (updated_at),
|
||||
INDEX idx_anonymous_id (anonymous_id),
|
||||
FOREIGN KEY (board_id) REFERENCES boards(board_id)
|
||||
);
|
||||
|
||||
CREATE TABLE posts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
post_id VARCHAR(6) NOT NULL UNIQUE,
|
||||
thread_id VARCHAR(6) NOT NULL,
|
||||
board_id VARCHAR(10) NOT NULL,
|
||||
message TEXT,
|
||||
file_name VARCHAR(255),
|
||||
file_size INT,
|
||||
file_type VARCHAR(10),
|
||||
ip_address VARCHAR(45),
|
||||
anonymous_id VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_post_id (post_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
INDEX idx_thread_id (thread_id),
|
||||
INDEX idx_board_id (board_id),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_anonymous_id (anonymous_id),
|
||||
FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
|
||||
FOREIGN KEY (board_id) REFERENCES boards(board_id)
|
||||
);
|
||||
|
||||
CREATE TABLE rate_limits (
|
||||
@ -19,3 +56,7 @@ CREATE TABLE rate_limits (
|
||||
INDEX idx_ip_action (ip_address, action_type),
|
||||
INDEX idx_created_at (created_at)
|
||||
);
|
||||
|
||||
INSERT INTO boards (board_id, name, description) VALUES
|
||||
('b', 'Random', 'Случайные темы'),
|
||||
('mk', 'MK', 'MK темы');
|
348
styles.css
348
styles.css
@ -34,29 +34,313 @@ body {
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
.home-link {
|
||||
color: #800000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.home-link:hover {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.boards-btn, .logout-btn {
|
||||
background: #d6daf0;
|
||||
border: 1px solid #b7c5d9;
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
color: #800000;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
.boards-btn:hover, .logout-btn:hover {
|
||||
background: #e5e9f0;
|
||||
}
|
||||
|
||||
.boards-container {
|
||||
background: #efefef;
|
||||
border: 1px solid #b7c5d9;
|
||||
margin-bottom: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.boards-container h2 {
|
||||
color: #800000;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.boards-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.board-item {
|
||||
background: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
padding: 10px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.board-item:hover {
|
||||
background: #e5d4c0;
|
||||
}
|
||||
|
||||
.board-link {
|
||||
text-decoration: none;
|
||||
color: #800000;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.board-id {
|
||||
font-weight: bold;
|
||||
color: #ff6b6b;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.board-name {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.board-desc {
|
||||
color: #707070;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.motd-container {
|
||||
background: #efefef;
|
||||
border: 1px solid #b7c5d9;
|
||||
margin-bottom: 10px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.motd {
|
||||
color: #800000;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.board-container {
|
||||
background: #efefef;
|
||||
border: 1px solid #b7c5d9;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.thread-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.thread-header {
|
||||
background: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thread-header h2 {
|
||||
color: #800000;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #ff6b6b;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.threads-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.threads-list {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.thread-item {
|
||||
background: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.thread-item:hover {
|
||||
background: #e5d4c0;
|
||||
}
|
||||
|
||||
.thread-link {
|
||||
text-decoration: none;
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
font-weight: bold;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.thread-info {
|
||||
color: #707070;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thread-file {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.thread-image {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border: 1px solid #d9bfb7;
|
||||
}
|
||||
|
||||
.thread-description {
|
||||
color: #800000;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #e5d4c0;
|
||||
border-left: 3px solid #ff6b6b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.anonymous-id {
|
||||
color: #ff6b6b;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.new-thread-button {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.new-thread-btn {
|
||||
background: #ff6b6b;
|
||||
border: 1px solid #ff6b6b;
|
||||
padding: 15px 30px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.new-thread-btn:hover {
|
||||
background: #ff5252;
|
||||
}
|
||||
|
||||
.new-thread-container {
|
||||
background: #efefef;
|
||||
border: 1px solid #b7c5d9;
|
||||
margin-bottom: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.thread-form {
|
||||
background: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.thread-form h2 {
|
||||
color: #800000;
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.markdown-help {
|
||||
background: #f0e0d6;
|
||||
border: 1px solid #d9bfb7;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.markdown-help h3 {
|
||||
color: #800000;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown-help ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-help li {
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.markdown-help code {
|
||||
background: #e5d4c0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #d6daf0;
|
||||
border: 1px solid #b7c5d9;
|
||||
padding: 8px 15px;
|
||||
color: #800000;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: #e5e9f0;
|
||||
}
|
||||
|
||||
.post-message strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.post-message em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.post-message u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post-message del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.post-message code {
|
||||
background: #e5d4c0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.post-message ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.post-message li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.posts-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.post {
|
||||
@ -64,16 +348,19 @@ body {
|
||||
border: 1px solid #d9bfb7;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-left: 3px solid #ff6b6b;
|
||||
}
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #d9bfb7;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.post-id {
|
||||
font-weight: bold;
|
||||
color: #800000;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.post-time {
|
||||
@ -100,6 +387,17 @@ body {
|
||||
.post-message {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.reply-link {
|
||||
color: #ffa500;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reply-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post-form {
|
||||
@ -112,6 +410,38 @@ body {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 100%;
|
||||
border: 1px solid #b7c5d9;
|
||||
padding: 8px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
background: #f0e0d6;
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.title-input:focus {
|
||||
outline: none;
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.description-input {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
border: 1px solid #b7c5d9;
|
||||
padding: 8px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
background: #f0e0d6;
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.description-input:focus {
|
||||
outline: none;
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
@ -120,6 +450,13 @@ body {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
background: #f0e0d6;
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
outline: none;
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
@ -127,6 +464,8 @@ body {
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
background: #f0e0d6;
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
@ -136,6 +475,7 @@ body {
|
||||
color: #800000;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
|
Loading…
x
Reference in New Issue
Block a user