Initial commit - Last War messaging system

This commit is contained in:
2026-02-19 01:33:28 -06:00
commit 38a8447a64
2162 changed files with 216183 additions and 0 deletions

31
includes/activity_logger.php Executable file
View File

@@ -0,0 +1,31 @@
<?php
function logActivity(int $userId, string $action, string $details): void
{
$pdo = getDbConnection();
$username = $_SESSION['username'] ?? 'system';
$stmt = $pdo->prepare("INSERT INTO activity_log (user_id, username, action, details, timestamp) VALUES (?, ?, ?, ?, NOW())");
$stmt->execute([$userId, $username, $action, $details]);
}
function getActivityLog(?int $limit = 100): array
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("SELECT * FROM activity_log ORDER BY timestamp DESC LIMIT ?");
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
function getUserActivity(int $userId, ?int $limit = 50): array
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("SELECT * FROM activity_log WHERE user_id = ? ORDER BY timestamp DESC LIMIT ?");
$stmt->execute([$userId, $limit]);
return $stmt->fetchAll();
}

87
includes/auth.php Executable file
View File

@@ -0,0 +1,87 @@
<?php
require_once __DIR__ . '/activity_logger.php';
function loginUser(string $username, string $password): ?array
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
return null;
}
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
logActivity($user['id'], 'login', 'Usuario inició sesión');
return $user;
}
function registerUser(string $username, string $password, string $role = 'user'): ?int
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetch()) {
return null;
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
$stmt->execute([$username, $hashedPassword, $role]);
return (int) $pdo->lastInsertId();
}
function updateUserPassword(int $userId, string $newPassword): bool
{
$pdo = getDbConnection();
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
return $stmt->execute([$hashedPassword, $userId]);
}
function getUserById(int $userId): ?array
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("SELECT id, username, role, telegram_chat_id, created_at FROM users WHERE id = ?");
$stmt->execute([$userId]);
return $stmt->fetch() ?: null;
}
function getAllUsers(): array
{
$pdo = getDbConnection();
$stmt = $pdo->query("SELECT id, username, role, telegram_chat_id, created_at FROM users ORDER BY username");
return $stmt->fetchAll();
}
function updateUserTelegramChatId(int $userId, string $telegramChatId): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("UPDATE users SET telegram_chat_id = ? WHERE id = ?");
return $stmt->execute([$telegramChatId, $userId]);
}
function deleteUser(int $userId): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
return $stmt->execute([$userId]);
}

35
includes/db.php Executable file
View File

@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/env_loader.php';
function getDbConnection(): PDO
{
static $pdo = null;
if ($pdo === null) {
$host = $_ENV['DB_HOST'] ?? getenv('DB_HOST') ?? 'localhost';
$port = $_ENV['DB_PORT'] ?? getenv('DB_PORT') ?? '3306';
$dbname = $_ENV['DB_NAME'] ?? getenv('DB_NAME') ?? 'bot';
$user = $_ENV['DB_USER'] ?? getenv('DB_USER') ?? 'root';
$pass = $_ENV['DB_PASS'] ?? getenv('DB_PASS') ?? '';
$dsn = "mysql:host={$host};port={$port};dbname={$dbname};charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, $user, $pass, $options);
// Configurar zona horaria
$timezone = $_ENV['TZ'] ?? getenv('TZ') ?? 'America/Mexico_City';
date_default_timezone_set($timezone);
// Configurar timezone de MySQL
$pdo->exec("SET time_zone = '" . date('P') . "'");
}
return $pdo;
}

32
includes/env_loader.php Executable file
View File

@@ -0,0 +1,32 @@
<?php
function loadEnv(string $path): void
{
if (!file_exists($path)) {
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
if (!array_key_exists($key, $_ENV)) {
$_ENV[$key] = $value;
}
if (!getenv($key)) {
putenv("$key=$value");
}
}
}
}
loadEnv(__DIR__ . '/../.env');

41
includes/logger.php Executable file
View File

@@ -0,0 +1,41 @@
<?php
function custom_log(string $message, string $level = 'INFO'): void
{
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$logFile = $logDir . '/app.log';
$timestamp = date('Y-m-d H:i:s');
$ip = $_SERVER['REMOTE_ADDR'] ?? 'CLI';
$user = $_SESSION['username'] ?? 'guest';
$logMessage = "[{$timestamp}] [{$level}] [{$ip}] [{$user}] {$message}" . PHP_EOL;
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
function logError(string $message): void
{
custom_log($message, 'ERROR');
}
function logWarning(string $message): void
{
custom_log($message, 'WARNING');
}
function logInfo(string $message): void
{
custom_log($message, 'INFO');
}
function logDebug(string $message): void
{
if ($_ENV['APP_ENVIRONMENT'] ?? getenv('APP_ENVIRONMENT') === 'pruebas') {
custom_log($message, 'DEBUG');
}
}

229
includes/message_handler.php Executable file
View File

@@ -0,0 +1,229 @@
<?php
require_once __DIR__ . '/activity_logger.php';
function createMessage(array $data): int
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("
INSERT INTO messages (user_id, content, created_at)
VALUES (?, ?, NOW())
");
$stmt->execute([$data['user_id'], $data['content']]);
return (int) $pdo->lastInsertId();
}
function createSchedule(array $data): int
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("
INSERT INTO schedules (
message_id, recipient_id, send_time, status,
is_recurring, recurring_days, recurring_time, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
");
$stmt->execute([
$data['message_id'],
$data['recipient_id'],
$data['send_time'],
$data['status'] ?? 'pending',
($data['is_recurring'] ?? false) ? 1 : 0,
$data['recurring_days'] ?? null,
$data['recurring_time'] ?? null
]);
return (int) $pdo->lastInsertId();
}
function handleCreateMessage(array $postData): array
{
$required = ['content', 'recipient_id', 'send_type'];
foreach ($required as $field) {
if (empty($postData[$field])) {
return ['success' => false, 'error' => "Falta el campo: {$field}"];
}
}
$userId = $_SESSION['user_id'] ?? 0;
$messageId = createMessage([
'user_id' => $userId,
'content' => $postData['content']
]);
$sendTime = match ($postData['send_type']) {
'now' => date('Y-m-d H:i:s'),
'later' => $postData['send_datetime'],
'recurring' => calculateNextSendTime($postData['recurring_days'], $postData['recurring_time']),
default => date('Y-m-d H:i:s')
};
$scheduleId = createSchedule([
'message_id' => $messageId,
'recipient_id' => $postData['recipient_id'],
'send_time' => $sendTime,
'status' => $postData['send_type'] === 'now' ? 'pending' : 'pending',
'is_recurring' => $postData['send_type'] === 'recurring',
'recurring_days' => $postData['recurring_days'] ?? null,
'recurring_time' => $postData['recurring_time'] ?? null
]);
logActivity($userId, 'create_message', "Mensaje creado ID: {$messageId}, Programación ID: {$scheduleId}");
return [
'success' => true,
'message_id' => $messageId,
'schedule_id' => $scheduleId
];
}
function updateMessage(int $messageId, string $content): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("UPDATE messages SET content = ? WHERE id = ?");
return $stmt->execute([$content, $messageId]);
}
function updateSchedule(int $scheduleId, array $data): bool
{
$pdo = getDbConnection();
$fields = [];
$values = [];
foreach ($data as $key => $value) {
$fields[] = "{$key} = ?";
$values[] = $value;
}
$values[] = $scheduleId;
$sql = "UPDATE schedules SET " . implode(', ', $fields) . " WHERE id = ?";
$stmt = $pdo->prepare($sql);
return $stmt->execute($values);
}
function handleEditMessage(int $messageId, array $postData): array
{
if (empty($postData['content']) || empty($postData['recipient_id'])) {
return ['success' => false, 'error' => 'Faltan campos requeridos'];
}
$userId = $_SESSION['user_id'] ?? 0;
updateMessage($messageId, $postData['content']);
if (!empty($postData['schedule_id'])) {
$sendTime = match ($postData['send_type']) {
'now' => date('Y-m-d H:i:s'),
'later' => $postData['send_datetime'],
'recurring' => calculateNextSendTime($postData['recurring_days'] ?? '', $postData['recurring_time'] ?? ''),
default => $postData['send_datetime']
};
updateSchedule((int) $postData['schedule_id'], [
'recipient_id' => $postData['recipient_id'],
'send_time' => $sendTime,
'is_recurring' => $postData['send_type'] === 'recurring',
'recurring_days' => $postData['recurring_days'] ?? null,
'recurring_time' => $postData['recurring_time'] ?? null,
'status' => 'pending'
]);
}
logActivity($userId, 'edit_message', "Mensaje actualizado ID: {$messageId}");
return ['success' => true, 'message_id' => $messageId];
}
function deleteMessage(int $messageId): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("DELETE FROM schedules WHERE message_id = ?");
$stmt->execute([$messageId]);
$stmt = $pdo->prepare("DELETE FROM messages WHERE id = ?");
$result = $stmt->execute([$messageId]);
if ($result) {
logActivity($_SESSION['user_id'] ?? 0, 'delete_message', "Mensaje eliminado ID: {$messageId}");
}
return $result;
}
function getMessageById(int $messageId): ?array
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("SELECT * FROM messages WHERE id = ?");
$stmt->execute([$messageId]);
return $stmt->fetch() ?: null;
}
function getScheduleById(int $scheduleId): ?array
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("SELECT * FROM schedules WHERE id = ?");
$stmt->execute([$scheduleId]);
return $stmt->fetch() ?: null;
}
function getScheduledMessages(?int $userId = null): array
{
$pdo = getDbConnection();
$sql = "
SELECT s.*, m.content, r.name as recipient_name, r.platform, r.platform_id
FROM schedules s
JOIN messages m ON s.message_id = m.id
JOIN recipients r ON s.recipient_id = r.id
";
if ($userId) {
$sql .= " WHERE m.user_id = ?";
$sql .= " ORDER BY s.send_time ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$userId]);
} else {
$sql .= " ORDER BY s.send_time ASC";
$stmt = $pdo->query($sql);
}
return $stmt->fetchAll();
}
function getSentMessages(?int $userId = null, ?int $limit = 50): array
{
$pdo = getDbConnection();
$sql = "
SELECT sm.*, r.name as recipient_name, r.platform, r.platform_id
FROM sent_messages sm
JOIN recipients r ON sm.recipient_id = r.id
";
if ($userId) {
$sql .= " WHERE sm.user_id = ?";
$sql .= " ORDER BY sm.sent_at DESC LIMIT ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$userId, $limit]);
} else {
$sql .= " ORDER BY sm.sent_at DESC LIMIT ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$limit]);
}
return $stmt->fetchAll();
}

98
includes/schedule_actions.php Executable file
View File

@@ -0,0 +1,98 @@
<?php
function disableSchedule(int $scheduleId): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("UPDATE schedules SET status = 'disabled' WHERE id = ?");
$result = $stmt->execute([$scheduleId]);
if ($result) {
logActivity($_SESSION['user_id'] ?? 0, 'disable_schedule', "Programación deshabilitada ID: {$scheduleId}");
}
return $result;
}
function enableSchedule(int $scheduleId): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("UPDATE schedules SET status = 'pending' WHERE id = ? AND status = 'disabled'");
$result = $stmt->execute([$scheduleId]);
if ($result) {
logActivity($_SESSION['user_id'] ?? 0, 'enable_schedule', "Programación habilitada ID: {$scheduleId}");
}
return $result;
}
function cancelSchedule(int $scheduleId): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("UPDATE schedules SET status = 'cancelled' WHERE id = ?");
$result = $stmt->execute([$scheduleId]);
if ($result) {
logActivity($_SESSION['user_id'] ?? 0, 'cancel_schedule', "Programación cancelada ID: {$scheduleId}");
}
return $result;
}
function retrySchedule(int $scheduleId): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("UPDATE schedules SET status = 'pending', error_message = NULL WHERE id = ? AND status = 'failed'");
$result = $stmt->execute([$scheduleId]);
if ($result) {
logActivity($_SESSION['user_id'] ?? 0, 'retry_schedule', "Programación reintentada ID: {$scheduleId}");
}
return $result;
}
function deleteSchedule(int $scheduleId): bool
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("SELECT message_id FROM schedules WHERE id = ?");
$stmt->execute([$scheduleId]);
$schedule = $stmt->fetch();
if ($schedule) {
$stmt = $pdo->prepare("DELETE FROM schedules WHERE id = ?");
$stmt->execute([$scheduleId]);
$stmt = $pdo->prepare("DELETE FROM messages WHERE id = ?");
$stmt->execute([$schedule['message_id']]);
logActivity($_SESSION['user_id'] ?? 0, 'delete_schedule', "Programación eliminada ID: {$scheduleId}");
return true;
}
return false;
}
function handleScheduleAction(int $scheduleId, string $action): array
{
$result = match ($action) {
'disable' => disableSchedule($scheduleId),
'enable' => enableSchedule($scheduleId),
'cancel' => cancelSchedule($scheduleId),
'retry' => retrySchedule($scheduleId),
'delete' => deleteSchedule($scheduleId),
default => false
};
return [
'success' => $result,
'action' => $action,
'schedule_id' => $scheduleId
];
}

98
includes/schedule_helpers.php Executable file
View File

@@ -0,0 +1,98 @@
<?php
function calculateNextSendTime(string $recurringDays, string $recurringTime): ?string
{
$daysMap = [
'sunday' => 0,
'monday' => 1,
'tuesday' => 2,
'wednesday' => 3,
'thursday' => 4,
'friday' => 5,
'saturday' => 6
];
$days = array_map('trim', explode(',', strtolower($recurringDays)));
$days = array_filter($days, fn($d) => isset($daysMap[$d]));
$days = array_map(fn($d) => $daysMap[$d], $days);
if (empty($days)) {
return null;
}
$timeParts = explode(':', $recurringTime);
$hour = (int) ($timeParts[0] ?? 0);
$minute = (int) ($timeParts[1] ?? 0);
$now = new DateTime('now', new DateTimeZone('America/Mexico_City'));
$currentDay = (int) $now->format('w');
$currentHour = (int) $now->format('H');
$currentMinute = (int) $now->format('i');
sort($days);
foreach ($days as $day) {
if ($day > $currentDay || ($day === $currentDay && ($hour > $currentHour || ($hour === $currentHour && $minute > $currentMinute)))) {
$next = clone $now;
$next->setTime($hour, $minute);
$next->modify('+' . ($day - $currentDay) . ' days');
return $next->format('Y-m-d H:i:s');
}
}
$daysAhead = (7 - $currentDay) + $days[0];
$next = clone $now;
$next->setTime($hour, $minute);
$next->modify('+' . $daysAhead . ' days');
return $next->format('Y-m-d H:i:s');
}
function getRecurringDaysOptions(): array
{
return [
['value' => 'monday', 'label' => 'Lunes'],
['value' => 'tuesday', 'label' => 'Martes'],
['value' => 'wednesday', 'label' => 'Miércoles'],
['value' => 'thursday', 'label' => 'Jueves'],
['value' => 'friday', 'label' => 'Viernes'],
['value' => 'saturday', 'label' => 'Sábado'],
['value' => 'sunday', 'label' => 'Domingo']
];
}
function formatRecurringDays(string $days): string
{
$daysMap = [
'monday' => 'Lun',
'tuesday' => 'Mar',
'wednesday' => 'Mié',
'thursday' => 'Jue',
'friday' => 'Vie',
'saturday' => 'Sáb',
'sunday' => 'Dom'
];
$daysArray = array_map('trim', explode(',', strtolower($days)));
$result = [];
foreach ($daysArray as $day) {
$result[] = $daysMap[$day] ?? $day;
}
return implode(', ', $result);
}
function isValidRecurringDays(string $days): bool
{
$validDays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$daysArray = array_map('trim', explode(',', strtolower($days)));
foreach ($daysArray as $day) {
if (!in_array($day, $validDays)) {
return false;
}
}
return !empty($daysArray);
}

87
includes/session_check.php Executable file
View File

@@ -0,0 +1,87 @@
<?php
require_once __DIR__ . '/env_loader.php';
function checkSession(): void
{
if (session_status() === PHP_SESSION_NONE) {
$domain = $_ENV['APP_URL'] ?? getenv('APP_URL') ?? '';
if ($domain) {
$parsed = parse_url($domain);
$host = $parsed['host'] ?? $_SERVER['HTTP_HOST'] ?? '';
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => $host,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
session_start();
}
validateSessionDomain();
if (!isset($_SESSION['user_id'])) {
$basePath = dirname($_SERVER['PHP_SELF']);
if ($basePath === '/' || $basePath === '\\') {
$basePath = '';
}
header('Location: ' . $basePath . '/login.php');
exit;
}
}
function validateSessionDomain(): void
{
$allowedDomain = $_ENV['APP_URL'] ?? getenv('APP_URL') ?? '';
if (empty($allowedDomain)) {
return;
}
$parsed = parse_url($allowedDomain);
$allowedHost = $parsed['host'] ?? '';
$currentHost = $_SERVER['HTTP_HOST'] ?? '';
if (strcasecmp($allowedHost, $currentHost) !== 0) {
session_unset();
session_destroy();
$scheme = $parsed['scheme'] ?? 'https';
$loginUrl = $scheme . '://' . $allowedHost . '/login.php';
header('Location: ' . $loginUrl);
exit;
}
}
function isAdmin(): bool
{
return isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
}
function requireAdmin(): void
{
checkSession();
if (!isAdmin()) {
header('HTTP/1.1 403 Forbidden');
echo 'Acceso denegado';
exit;
}
}
function getCurrentUserId(): int
{
return $_SESSION['user_id'] ?? 0;
}
function getCurrentUsername(): string
{
return $_SESSION['username'] ?? '';
}
function getCurrentUserRole(): string
{
return $_SESSION['role'] ?? 'guest';
}

69
includes/translation_helper.php Executable file
View File

@@ -0,0 +1,69 @@
<?php
require_once __DIR__ . '/src/Translate.php';
function translateText(string $text, string $sourceLang, string $targetLang): ?string
{
$translator = new \src\Translate();
return $translator->translate($text, $sourceLang, $targetLang);
}
function detectLanguage(string $text): ?string
{
$translator = new \src\Translate();
return $translator->detectLanguage($text);
}
function getActiveLanguages(): array
{
$pdo = getDbConnection();
$stmt = $pdo->query("SELECT * FROM supported_languages WHERE is_active = 1 ORDER BY language_name");
return $stmt->fetchAll();
}
function queueTranslation(string $platform, int $chatId, int $userId, string $text, string $sourceLang): int
{
$pdo = getDbConnection();
$stmt = $pdo->prepare("
INSERT INTO translation_queue
(platform, chat_id, user_id, text_to_translate, source_lang, status, attempts, created_at)
VALUES (?, ?, ?, ?, ?, 'pending', 0, NOW())
");
$stmt->execute([$platform, $chatId, $userId, $text, $sourceLang]);
return (int) $pdo->lastInsertId();
}
function translateAndSendToPlatform(string $platform, int $chatId, string $htmlContent): void
{
$translator = new \src\Translate();
$converterClass = $platform === 'discord'
? \Discord\Converters\HtmlToDiscordMarkdownConverter::class
: \Telegram\Converters\HtmlToTelegramHtmlConverter::class;
$converter = new $converterClass();
$textContent = strip_tags($converter->convert($htmlContent));
$sourceLang = $translator->detectLanguage($textContent) ?? 'es';
$languages = getActiveLanguages();
$targetLangs = array_filter($languages, fn($l) => $l['language_code'] !== $sourceLang);
$translations = [];
foreach ($targetLangs as $lang) {
$translations[$lang['language_code']] = $translator->translate($textContent, $sourceLang, $lang['language_code']);
}
if ($platform === 'discord') {
$sender = new \Discord\DiscordSender();
$actions = new \Discord\Actions\DiscordActions();
$actions->sendWithTranslation((string)$chatId, $htmlContent, $translations);
} else {
$sender = new \Telegram\TelegramSender();
$actions = new \Telegram\Actions\TelegramActions();
$actions->sendWithTranslation((int)$chatId, $htmlContent, $translations);
}
}

40
includes/url_helper.php Executable file
View File

@@ -0,0 +1,40 @@
<?php
function getBaseUrl(): string
{
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
return "{$protocol}://{$host}";
}
function getAppUrl(string $path = ''): string
{
return getBaseUrl() . '/' . ltrim($path, '/');
}
function site_url(string $path = ''): string
{
return getAppUrl($path);
}
function asset(string $path): string
{
return getAppUrl('assets/' . ltrim($path, '/'));
}
function url(string $path): string
{
return getAppUrl($path);
}
function redirect(string $path): void
{
header('Location: ' . site_url($path));
exit;
}
function current_url(): string
{
return getBaseUrl() . ($_SERVER['REQUEST_URI'] ?? '/');
}