Bot Discord - Commit completo con todos los cambios

This commit is contained in:
Admin
2026-01-16 20:24:38 -06:00
commit cf8ecfcf64
151 changed files with 28808 additions and 0 deletions

66
includes/Translate.php Executable file
View File

@@ -0,0 +1,66 @@
<?php
// includes/Translate.php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class Translate {
private $client;
public function __construct() {
$this->client = new Client([
'base_uri' => LIBRETRANSLATE_URL,
'timeout' => 10.0,
'verify' => false
]);
}
public function translate($text, $targetLang, $sourceLang = 'auto') {
try {
$response = $this->client->post('/translate', [
'json' => [
'q' => $text,
'source' => $sourceLang,
'target' => $targetLang,
'format' => 'text'
]
]);
$body = json_decode($response->getBody()->getContents(), true);
if (isset($body['translatedText'])) {
return $body['translatedText'];
}
return null;
} catch (RequestException $e) {
error_log("Error en traducción: " . $e->getMessage());
return null;
}
}
public function translateBatch($texts, $targetLang, $sourceLang = 'auto') {
try {
$response = $this->client->post('/translate', [
'json' => [
'q' => $texts,
'source' => $sourceLang,
'target' => $targetLang,
'format' => 'text'
]
]);
$body = json_decode($response->getBody()->getContents(), true);
// LibreTranslate devuelve un array de objetos con la clave 'translatedText'
if (isset($body['translatedText']) && is_array($body['translatedText'])) {
return $body['translatedText'];
}
} catch (RequestException $e) {
error_log("Error en traducción por lotes: " . $e->getMessage());
return null;
}
return null;
}
}

35
includes/activity_logger.php Executable file
View File

@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/db.php';
function log_activity(int $userId, string $action, ?string $details = null): void
{
global $pdo;
// Debugging: Check if $pdo is a valid PDO object
if (!($pdo instanceof PDO)) {
error_log("Error logging activity: $pdo is not a valid PDO object. Type: " . gettype($pdo));
return;
}
try {
// Fetch username from session or database if not available
$username = $_SESSION['username'] ?? 'Unknown';
if ($username === 'Unknown' && $userId > 0) {
$stmt = $pdo->prepare("SELECT username FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
$username = $user['username'];
}
}
$stmt = $pdo->prepare(
"INSERT INTO activity_log (user_id, username, action, details, timestamp)
VALUES (?, ?, ?, ?, NOW())"
);
$stmt->execute([$userId, $username, $action, $details]);
} catch (PDOException $e) {
error_log("Error logging activity: " . $e->getMessage());
}
}
?>

49
includes/auth.php Executable file
View File

@@ -0,0 +1,49 @@
<?php
session_start();
require_once 'db.php';
require_once 'activity_logger.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF token validation
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
header('Location: ../login.php?error=csrf_error');
exit();
}
// Unset the token so it can't be used again
// unset($_SESSION['csrf_token']); // Keep token for re-attempts on failed login
if (!isset($_POST['username']) || !isset($_POST['password'])) {
header('Location: ../login.php?error=missing_fields');
exit();
}
$username = $_POST['username'];
$password = $_POST['password'];
try {
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
session_regenerate_id(true);
log_activity($user['id'], 'User Login', 'User ' . $user['username'] . ' logged in.');
session_write_close();
header('Location: ../index.php');
exit();
} else {
header('Location: ../login.php?error=invalid_credentials');
exit();
}
} catch (PDOException $e) {
// Log the error instead of showing it to the user
error_log('Authentication error: ' . $e->getMessage());
header('Location: ../login.php?error=db_error');
exit();
}
}
?>

126
includes/db.php Executable file
View File

@@ -0,0 +1,126 @@
<?php
require_once __DIR__ . '/../config/config.php';
// Establecer la zona horaria predeterminada
date_default_timezone_set('America/Mexico_City');
/**
* Clase para manejar la conexión a la base de datos con reconexión automática
*/
class DatabaseConnection {
private static $instance = null;
private $pdo = null;
private $config = [];
private function __construct() {
$this->config = [
'host' => $_ENV['DB_HOST'] ?? 'localhost',
'port' => $_ENV['DB_PORT'] ?? '3306',
'name' => $_ENV['DB_NAME'] ?? 'bot',
'user' => $_ENV['DB_USER'] ?? 'nickpons666',
'pass' => $_ENV['DB_PASS'] ?? 'MiPo6425@@',
'charset' => 'utf8mb4',
'timeout' => 30, // Tiempo de espera de conexión en segundos
'reconnect_attempts' => 3, // Número de intentos de reconexión
'reconnect_delay' => 1, // Tiempo de espera entre reconexiones en segundos
];
$this->connect();
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
// Verificar si la conexión sigue activa
try {
$this->pdo->query('SELECT 1');
return $this->pdo;
} catch (PDOException $e) {
// Si la conexión se perdió, intentar reconectar
error_log("La conexión a la base de datos se perdió. Intentando reconectar...");
$this->connect();
return $this->pdo;
}
}
private function connect() {
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
$this->config['host'],
$this->config['port'],
$this->config['name'],
$this->config['charset']
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_TIMEOUT => $this->config['timeout'],
PDO::ATTR_PERSISTENT => false, // No usar conexiones persistentes para evitar problemas
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
];
$attempts = 0;
$lastException = null;
while ($attempts < $this->config['reconnect_attempts']) {
try {
$this->pdo = new PDO(
$dsn,
$this->config['user'],
$this->config['pass'],
$options
);
// Configuración adicional de la conexión
$this->pdo->exec("SET time_zone = '-06:00';");
$this->pdo->exec("SET SESSION wait_timeout=28800;"); // 8 horas
$this->pdo->exec("SET SESSION interactive_timeout=28800;"); // 8 horas
error_log("Conexión a la base de datos establecida correctamente.");
return;
} catch (PDOException $e) {
$lastException = $e;
$attempts++;
error_log(sprintf(
"Intento de conexión %d fallido: %s. Reintentando en %d segundos...",
$attempts,
$e->getMessage(),
$this->config['reconnect_delay']
));
if ($attempts < $this->config['reconnect_attempts']) {
sleep($this->config['reconnect_delay']);
}
}
}
// Si llegamos aquí, todos los intentos fallaron
error_log("No se pudo establecer la conexión después de {$this->config['reconnect_attempts']} intentos.");
throw $lastException;
}
}
// Crear una instancia de la conexión
try {
$pdo = DatabaseConnection::getInstance()->getConnection();
} catch (PDOException $e) {
error_log("Error crítico de conexión a la base de datos: " . $e->getMessage());
// No usar die() aquí porque mata a los workers - dejar que el código maneje el error
// Para scripts web, el error se mostrará en el log y el script continuará
// Para workers, pueden manejar la excepción y reintentar
if (php_sapi_name() !== 'cli') {
// Solo para contexto web
die("Error de conexión a la base de datos. Por favor, inténtalo de nuevo más tarde.");
}
// Para CLI (workers), lanzar la excepción para que el worker la maneje
throw $e;
}
?>

72
includes/discord_actions.php Executable file
View File

@@ -0,0 +1,72 @@
<?php
require_once __DIR__ . '/session_check.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/../src/DiscordSender.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['action'])) {
header('Location: ../sent_messages.php');
exit();
}
$action = $_POST['action'];
if ($action === 'delete_message') {
if (!isset($_POST['sent_message_id'], $_POST['platform_message_id'], $_POST['channel_id'])) {
header('Location: ../sent_messages.php?error=missing_data');
exit();
}
$sentMessageId = $_POST['sent_message_id'];
$discordMessageIdsJson = $_POST['platform_message_id'];
$channelId = $_POST['channel_id']; // The channel where the message was sent
$discordMessageIds = json_decode($discordMessageIdsJson, true);
// If decoding fails or the result is not an array, treat it as a single ID
if (json_last_error() !== JSON_ERROR_NONE || !is_array($discordMessageIds)) {
$discordMessageIds = [$discordMessageIdsJson];
}
$discordSender = new DiscordSender(DISCORD_BOT_TOKEN);
try {
// 1. Attempt to delete from Discord
$all_deleted = true;
foreach ($discordMessageIds as $discordMessageId) {
// Skip if the ID is empty or invalid
if (empty($discordMessageId) || !is_numeric($discordMessageId)) {
error_log("Skipping invalid message chunk ID: " . var_export($discordMessageId, true));
continue;
}
try {
error_log("Attempting to delete message chunk ID: {$discordMessageId} in channel {$channelId}");
$discordSender->deleteMessage($channelId, $discordMessageId);
usleep(500000); // Wait 500ms to avoid rate limiting
} catch (Exception $e) {
error_log("Failed to delete message chunk {$discordMessageId}: " . $e->getMessage());
$all_deleted = false; // Mark that at least one failed
}
}
// 2. If all chunks were deleted (or if there was only one), delete from our database
if ($all_deleted) {
$stmt = $pdo->prepare("DELETE FROM sent_messages WHERE id = ?");
$stmt->execute([$sentMessageId]);
header('Location: ../sent_messages.php?success=deleted&platform=Discord');
} else {
// If some failed, we don't delete the entry, so it can be retried.
// We could also add more sophisticated logic here, like storing partial success.
header('Location: ../sent_messages.php?error=delete_failed_partial&platform=Discord');
}
exit();
} catch (Exception $e) {
error_log("Discord message deletion failed: " . $e->getMessage());
header('Location: ../sent_messages.php?error=delete_failed&platform=Discord&message=' . urlencode($e->getMessage()));
exit();
}
}
// Fallback redirect
header('Location: ../sent_messages.php');
?>

16
includes/emojis.php Executable file
View File

@@ -0,0 +1,16 @@
<?php
/**
* ARCHIVO BRIDGE - Migración Progresiva
*
* Este archivo mantiene compatibilidad con código existente
* mientras redirige a la nueva ubicación del helper.
*
* Nueva ubicación: /common/helpers/emojis.php
* Fecha de migración: 2025-11-25
*/
// Cargar el archivo desde la nueva ubicación
require_once __DIR__ . '/../common/helpers/emojis.php';
// Este archivo puede ser eliminado cuando toda la migración esté completa
// y todas las referencias apunten directamente a common/helpers/emojis.php

56
includes/error_handler.php Executable file
View File

@@ -0,0 +1,56 @@
<?php
// Configuración de manejo de errores
function customErrorHandler($errno, $errstr, $errfile, $errline) {
$logFile = __DIR__ . '/../logs/php_errors.log';
// Definir los tipos de error que queremos registrar
$error_types = [
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_PARSE => 'PARSING ERROR',
E_NOTICE => 'NOTICE',
E_CORE_ERROR => 'CORE ERROR',
E_CORE_WARNING => 'CORE WARNING',
E_COMPILE_ERROR => 'COMPILE ERROR',
E_COMPILE_WARNING => 'COMPILE WARNING',
E_USER_ERROR => 'USER ERROR',
E_USER_WARNING => 'USER WARNING',
E_USER_NOTICE => 'USER NOTICE',
E_STRICT => 'STRICT NOTICE',
E_RECOVERABLE_ERROR => 'RECOVERABLE ERROR',
E_DEPRECATED => 'DEPRECATED',
E_USER_DEPRECATED => 'USER DEPRECATED'
];
$error_type = $error_types[$errno] ?? 'UNKNOWN';
$error_message = "[" . date('Y-m-d H:i:s') . "] $error_type: $errstr in $errfile on line $errline\n";
// Escribir en el archivo de log
error_log($error_message, 3, $logFile);
// No ejecutar el gestor de errores interno de PHP
return true;
}
// Función para registrar excepciones no capturadas
function customExceptionHandler($exception) {
$logFile = __DIR__ . '/../logs/php_errors.log';
$error_message = "[" . date('Y-m-d H:i:s') . "] EXCEPTION: " . $exception->getMessage() . " in " .
$exception->getFile() . " on line " . $exception->getLine() . "\n";
error_log($error_message, 3, $logFile);
// Mostrar un mensaje genérico al usuario
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
echo "Ha ocurrido un error inesperado. El administrador ha sido notificado.";
}
// Establecer manejadores de errores
set_error_handler('customErrorHandler');
set_exception_handler('customExceptionHandler');
// Mostrar errores en pantalla solo en entorno de desarrollo
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/../logs/php_errors.log');

134
includes/get_gallery.php Executable file
View File

@@ -0,0 +1,134 @@
<?php
require_once __DIR__ . '/session_check.php';
require_once __DIR__ . '/db.php';
header('Content-Type: application/json');
// Directorio donde se almacenan las imágenes
$uploadDir = __DIR__ . '/../uploads/';
$baseUrl = '/bot/uploads/';
$response = [
'success' => false,
'message' => '',
'images' => []
];
try {
// Verificar si el directorio existe
if (!is_dir($uploadDir)) {
throw new Exception('El directorio de imágenes no existe');
}
// Obtener archivos del directorio
$files = scandir($uploadDir);
$imageFiles = array_filter($files, function($file) use ($uploadDir) {
// Solo archivos de imagen
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
return in_array($ext, $imageExtensions) && is_file($uploadDir . $file);
});
// Construir la respuesta con las imágenes
foreach ($imageFiles as $file) {
$filePath = $uploadDir . $file;
$fileUrl = $baseUrl . $file;
// Crear miniatura si no existe
$thumbnailPath = $uploadDir . 'thumbs/' . $file;
$thumbnailUrl = $baseUrl . 'thumbs/' . $file;
if (!file_exists($thumbnailPath)) {
// Crear directorio de miniaturas si no existe
if (!is_dir(dirname($thumbnailPath))) {
mkdir(dirname($thumbnailPath), 0755, true);
}
// Crear miniatura
createThumbnail($filePath, $thumbnailPath, 200, 200);
}
$response['images'][] = [
'name' => $file,
'url' => $fileUrl,
'thumbnail' => file_exists($thumbnailPath) ? $thumbnailUrl : $fileUrl,
'size' => filesize($filePath),
'mtime' => filemtime($filePath)
];
}
$response['success'] = true;
$response['message'] = count($response['images']) . ' imágenes encontradas';
} catch (Exception $e) {
$response['message'] = 'Error al cargar la galería: ' . $e->getMessage();
}
echo json_encode($response);
/**
* Crea una miniatura de una imagen
*/
function createThumbnail($sourcePath, $destPath, $maxWidth, $maxHeight) {
$sourceInfo = getimagesize($sourcePath);
if (!$sourceInfo) return false;
list($origWidth, $origHeight, $type) = $sourceInfo;
// Calcular nuevas dimensiones manteniendo la relación de aspecto
$ratio = $origWidth / $origHeight;
if ($maxWidth / $maxHeight > $ratio) {
$maxWidth = $maxHeight * $ratio;
} else {
$maxHeight = $maxWidth / $ratio;
}
// Crear imagen de origen
switch ($type) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$source = imagecreatefromgif($sourcePath);
break;
default:
return false;
}
if (!$source) return false;
// Crear imagen de destino
$thumb = imagecreatetruecolor($maxWidth, $maxHeight);
// Preservar transparencia para PNG y GIF
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF) {
imagecolortransparent($thumb, imagecolorallocatealpha($thumb, 0, 0, 0, 127));
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
}
// Redimensionar
imagecopyresampled($thumb, $source, 0, 0, 0, 0, $maxWidth, $maxHeight, $origWidth, $origHeight);
// Guardar miniatura
switch ($type) {
case IMAGETYPE_JPEG:
imagejpeg($thumb, $destPath, 90);
break;
case IMAGETYPE_PNG:
imagepng($thumb, $destPath, 9);
break;
case IMAGETYPE_GIF:
imagegif($thumb, $destPath);
break;
}
// Liberar memoria
imagedestroy($source);
imagedestroy($thumb);
return true;
}

10
includes/logger.php Executable file
View File

@@ -0,0 +1,10 @@
<?php
// Función de depuración personalizada
if (!function_exists('custom_log')) {
function custom_log($message) {
$logFile = __DIR__ . '/../logs/custom_debug.log'; // Adjust path as needed
$timestamp = date('Y-m-d H:i:s');
file_put_contents('php://stderr', "[$timestamp] $message\n", FILE_APPEND);
}
}
?>

361
includes/message_handler.php Executable file
View File

@@ -0,0 +1,361 @@
<?php
// Configurar zona horaria
date_default_timezone_set('America/Mexico_City');
// Incluir el helper de programación
require_once __DIR__ . '/schedule_helpers.php';
// Configuración de logs
$logFile = dirname(__DIR__) . '/logs/discord_api.log';
// Crear el directorio de logs si no existe
if (!file_exists(dirname($logFile))) {
mkdir(dirname($logFile), 0755, true);
}
// Incluir archivos necesarios
require_once __DIR__ . '/session_check.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/activity_logger.php';
$submitAction = $_POST['submit'] ?? '';
// --- Special handler for sending from a template ---
if ($submitAction === 'send_from_template') {
$template_id = $_POST['recurrent_message_id'] ?? 0;
$user_ids = $_POST['recipientId_user'] ?? [];
$scheduleType = $_POST['scheduleType'] ?? 'now';
$scheduleDateTime = $_POST['scheduleDateTime'] ?? null;
$userId = $_SESSION['user_id'];
if (empty($template_id) || empty($user_ids)) {
header("Location: ../enviar_plantilla.php?error=missing_fields");
exit();
}
$pdo->beginTransaction();
try {
// Fetch the message content from the template
$stmt = $pdo->prepare("SELECT message_content FROM recurrent_messages WHERE id = ?");
$stmt->execute([$template_id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("La plantilla seleccionada no existe.");
}
$messageContent = $template['message_content'];
// 1. Insert the main message content into the messages table
$stmt = $pdo->prepare("INSERT INTO messages (user_id, content) VALUES (?, ?)");
$stmt->execute([$userId, $messageContent]);
$messageId = $pdo->lastInsertId();
// 2. Determine the send time
$sendTime = ($scheduleType === 'later')
? (new DateTime($scheduleDateTime, new DateTimeZone('America/Mexico_City')))->format('Y-m-d H:i:s')
: date('Y-m-d H:i:s');
// 3. Create a schedule for each selected user
$stmt = $pdo->prepare(
"INSERT INTO schedules (message_id, recipient_id, send_time, status, is_recurring, recurring_days, recurring_time)
VALUES (?, ?, ?, 'pending', 0, NULL, NULL)"
);
foreach ($user_ids as $user_id) {
$stmt->execute([$messageId, $user_id, $sendTime]);
log_activity($userId, 'Message Scheduled from Template', 'Schedule for user ID: ' . $user_id);
}
$pdo->commit();
// If sending now, trigger the queue processor
if ($scheduleType === 'now') {
$phpPath = PHP_BINARY ?: 'php';
$scriptPath = dirname(__DIR__) . '/process_queue.php';
$logPath = dirname(__DIR__) . '/logs/process_queue_manual.log';
$command = sprintf('%s %s >> %s 2>&1 &', escapeshellarg($phpPath), escapeshellarg($scriptPath), escapeshellarg($logPath));
shell_exec($command);
}
header("Location: ../scheduled_messages.php?success=template_sent");
exit();
} catch (Exception $e) {
$pdo->rollBack();
error_log("TemplateHandler Error: " . $e->getMessage() . "\n", 3, dirname(__DIR__) . '/logs/discord_api.log');
header("Location: ../enviar_plantilla.php?error=dberror");
exit();
}
}
// Regular message handling starts here
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../index.php');
exit();
}
$userId = $_SESSION['user_id'];
$content = $_POST['messageContent'] ?? '';
error_log("[DEBUG] Message content in message_handler.php: " . substr($content, 0, 500)); // Log first 500 chars
$platform = $_POST['platform'] ?? ''; // 'discord' or 'telegram'
// Obtener canales y usuarios seleccionados (pueden ser arrays JSON)
$channelIds = json_decode($_POST['channelIds'] ?? '[]', true);
$userIds = json_decode($_POST['userIds'] ?? '[]', true);
// Si no hay canales ni usuarios seleccionados, verificar el campo legacy recipientId
if (empty($channelIds) && empty($userIds) && !empty($_POST['recipientId'])) {
// Para compatibilidad con el código existente
$channelIds = [$_POST['recipientId']];
}
// Combinar todos los IDs de destinatarios y eliminar valores vacíos
$allRecipientIds = array_filter(array_merge($channelIds, $userIds));
// Debug: Registrar los destinatarios recibidos
error_log("Channel IDs: " . print_r($channelIds, true));
error_log("User IDs: " . print_r($userIds, true));
error_log("All Recipient IDs: " . print_r($allRecipientIds, true));
// Validar que haya al menos un destinatario
if (empty($allRecipientIds)) {
header('Location: ../create_message.php?error=no_recipients');
exit();
}
$scheduleId = $_POST['schedule_id'] ?? null;
$submitAction = $_POST['submit'] ?? 'send';
$isEditing = ($submitAction === 'update' && !empty($scheduleId));
// Basic validation
if (empty($content) || empty($platform) || empty(array_filter($allRecipientIds))) {
$error_param = 'missing_fields';
if (empty($platform)) $error_param = 'missing_platform';
if (empty(array_filter($allRecipientIds))) $error_param = 'missing_recipient';
$error_url = $isEditing
? "../create_message.php?action=edit&schedule_id={$scheduleId}&error={$error_param}"
: "../create_message.php?error={$error_param}";
header("Location: {$error_url}");
exit();
}
$scheduleType = $_POST['scheduleType'] ?? 'now';
$pdo->beginTransaction();
try {
// --- DEBUG LOGGING ---
$debugLogFile = dirname(__DIR__) . '/logs/schedule_debug.log';
$logEntry = "[" . date('Y-m-d H:i:s') . "] --- INICIO DE PETICIÓN ---\n";
$logEntry .= "ACTION: {$submitAction}, SCHEDULE_ID: " . ($scheduleId ?? 'null') . "\n";
$logEntry .= "PLATFORM: {$platform}\n";
$logEntry .= "CHANNEL_IDS: " . (is_array($channelIds) ? implode(', ', $channelIds) : $channelIds) . "\n";
$logEntry .= "USER_IDS: " . (is_array($userIds) ? implode(', ', $userIds) : $userIds) . "\n";
$logEntry .= "CONTENT LENGTH: " . strlen($content) . "\n";
$logEntry .= "POST DATA: " . json_encode($_POST, JSON_PRETTY_PRINT) . "\n";
file_put_contents($debugLogFile, $logEntry, FILE_APPEND);
// --- FIN DEBUG LOGGING ---
// Validar que haya contenido y destinatarios
if (empty(trim($content))) {
throw new Exception("El contenido del mensaje no puede estar vacío.");
}
if (empty($channelIds) && empty($userIds)) {
throw new Exception("Debes seleccionar al menos un canal o usuario como destinatario.");
}
// Combinar todos los IDs de destinatarios
$allRecipientIds = array_merge(
is_array($channelIds) ? $channelIds : [],
is_array($userIds) ? $userIds : []
);
// Validar que todos los destinatarios existan
if (!empty($allRecipientIds)) {
$placeholders = rtrim(str_repeat('?,', count($allRecipientIds)), ',');
$stmt = $pdo->prepare("SELECT COUNT(*) FROM recipients WHERE id IN ($placeholders)");
$stmt->execute($allRecipientIds);
$validRecipients = (int)$stmt->fetchColumn();
if ($validRecipients !== count($allRecipientIds)) {
throw new Exception("Uno o más destinatarios no son válidos.");
}
}
if ($submitAction === 'update' && !empty($scheduleId)) {
// UPDATE LOGIC
$stmt = $pdo->prepare("SELECT message_id FROM schedules WHERE id = ?");
$stmt->execute([$scheduleId]);
$originalMessage = $stmt->fetch();
if (!$originalMessage) {
throw new Exception("No se encontró la programación original.");
}
$messageId = $originalMessage['message_id'];
$stmt = $pdo->prepare("UPDATE messages SET content = ? WHERE id = ?");
$stmt->execute([$content, $messageId]);
$sendTime = null;
$isRecurring = 0;
$recurringDaysStr = null;
$recurringTime = null;
$status = 'pending';
$stmt = $pdo->prepare("DELETE FROM schedules WHERE message_id = ?");
$stmt->execute([$originalMessage['message_id']]);
// Obtener la configuración de programación
$sendTime = null;
$status = 'pending';
$isRecurring = 0;
$recurringDaysStr = null;
$recurringTime = null;
if ($scheduleType === 'later') {
$user_timezone = new DateTimeZone('America/Mexico_City');
$schedule_dt = DateTime::createFromFormat('Y-m-d\TH:i', $_POST['scheduleDateTime'], $user_timezone);
if (!$schedule_dt) {
throw new Exception("Formato de fecha/hora de programación no válido.");
}
$sendTime = $schedule_dt->format('Y-m-d H:i:s');
} elseif ($scheduleType === 'recurring') {
$isRecurring = 1;
$recurringDays = $_POST['recurringDays'] ?? [];
sort($recurringDays);
$recurringDaysStr = !empty($recurringDays) ? implode(',', $recurringDays) : null;
$recurringTime = $_POST['recurringTime'] ?? '00:00';
$sendTime = calculateNextSendTime($recurringDays, $recurringTime);
} else { // 'now'
$sendTime = date('Y-m-d H:i:s');
}
// Create new schedules for each recipient
$stmt = $pdo->prepare(
"INSERT INTO schedules (message_id, recipient_id, send_time, status, is_recurring, recurring_days, recurring_time) VALUES (?, ?, ?, ?, ?, ?, ?)"
);
foreach ($allRecipientIds as $recipientId) {
$stmt->execute([
$originalMessage['message_id'],
$recipientId,
$sendTime,
$status,
$isRecurring,
$recurringDaysStr,
$recurringTime
]);
log_activity($userId, 'Schedule Recipient Updated', 'Schedule for Message ID: ' . $originalMessage['message_id'] . ' to Recipient ID: ' . $recipientId);
}
// --- DEBUG LOGGING ---
$logEntry = "Calculated sendTime for UPDATE: {$sendTime} (Timezone: " . date_default_timezone_get() . ")\n";
file_put_contents($debugLogFile, $logEntry, FILE_APPEND);
// --- FIN DEBUG LOGGING ---
$success_param = 'updated';
log_activity($userId, 'Message Updated', 'Schedule ID: ' . $scheduleId);
} else {
// CREATE LOGIC
$sendTime = null;
$status = 'pending';
$isRecurring = 0;
$recurringDaysStr = null;
$recurringTime = null;
// Determinar la configuración de programación
if ($scheduleType === 'later') {
$user_timezone = new DateTimeZone('America/Mexico_City');
$schedule_dt = DateTime::createFromFormat('Y-m-d\TH:i', $_POST['scheduleDateTime'], $user_timezone);
if (!$schedule_dt) {
throw new Exception("Formato de fecha/hora de programación no válido.");
}
$sendTime = $schedule_dt->format('Y-m-d H:i:s');
} elseif ($scheduleType === 'recurring') {
$isRecurring = 1;
$recurringDays = $_POST['recurringDays'] ?? [];
sort($recurringDays);
$recurringDaysStr = !empty($recurringDays) ? implode(',', $recurringDays) : null;
$recurringTime = $_POST['recurringTime'] ?? '00:00';
$sendTime = calculateNextSendTime($recurringDays, $recurringTime);
} else { // 'now'
$sendTime = date('Y-m-d H:i:s');
}
// Insertar un solo mensaje
$stmt = $pdo->prepare("INSERT INTO messages (user_id, content) VALUES (?, ?)");
$stmt->execute([$userId, $content]);
$messageId = $pdo->lastInsertId();
// Crear programaciones para cada destinatario
$stmt = $pdo->prepare(
"INSERT INTO schedules (message_id, recipient_id, send_time, status, is_recurring, recurring_days, recurring_time) " .
"VALUES (?, ?, ?, ?, ?, ?, ?)"
);
// Combinar canales y usuarios en un solo array
$allRecipients = array_merge(
is_array($channelIds) ? $channelIds : [],
is_array($userIds) ? $userIds : []
);
// Crear una programación para cada destinatario
foreach ($allRecipients as $recipientId) {
$stmt->execute([
$messageId,
$recipientId,
$sendTime,
$status,
$isRecurring,
$recurringDaysStr,
$recurringTime
]);
$scheduleId = $pdo->lastInsertId();
log_activity($userId, 'Message Created', 'Schedule ID: ' . $scheduleId . ' for Recipient ID: ' . $recipientId);
}
$success_param = 'message_created';
// --- DEBUG LOGGING ---
$logEntry = "Calculated sendTime for CREATE: {$sendTime} (Timezone: " . date_default_timezone_get() . ")\n-- FIN DE PETICIÓN --\n\n";
file_put_contents($debugLogFile, $logEntry, FILE_APPEND);
// --- FIN DEBUG LOGGING ---
}
$pdo->commit();
if ($scheduleType === 'now') {
$phpPath = PHP_BINARY ?: 'php';
$scriptPath = dirname(__DIR__) . '/process_queue.php';
$logPath = dirname(__DIR__) . '/logs/process_queue_manual.log';
// Asegurarse de que se use el entorno correcto
$command = sprintf(
'APP_ENVIRONMENT=pruebas %s %s >> %s 2>&1 &',
escapeshellarg($phpPath),
escapeshellarg($scriptPath),
escapeshellarg($logPath)
);
// Registrar el comando que se va a ejecutar para depuración
error_log("Ejecutando comando: " . $command . "\n", 3, $logFile);
// Ejecutar el comando
shell_exec($command);
// Registrar que se inició el procesamiento
error_log("Procesamiento en segundo plano iniciado para schedule_id: " . ($scheduleId ?? 'nuevo') . "\n", 3, $logFile);
}
header("Location: ../scheduled_messages.php?success={$success_param}");
exit();
} catch (Exception $e) {
$pdo->rollBack();
error_log("MessageHandler Error: " . $e->getMessage() . "\n", 3, $logFile);
$error_url = ($submitAction === 'update') ? "../create_message.php?action=edit&schedule_id={$scheduleId}&error=dberror" : '../create_message.php?error=dberror';
header("Location: {$error_url}");
exit();
}
?>

View File

@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . '/session_check.php';
require_once __DIR__ . '/db.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../scheduled_messages.php');
exit();
}
// Basic validation
if (empty($_POST['messageContent']) || empty($_POST['recipientId']) || empty($_POST['schedule_id']) || empty($_POST['message_id'])) {
header('Location: ../edit_message.php?schedule_id=' . $_POST['schedule_id'] . '&error=missing_fields');
exit();
}
$scheduleId = $_POST['schedule_id'];
$messageId = $_POST['message_id'];
$content = $_POST['messageContent'];
$recipientId = $_POST['recipientId'];
$scheduleDateTime = $_POST['scheduleDateTime'] ?? date('Y-m-d H:i:s');
$pdo->beginTransaction();
try {
// 1. Update the message content
$stmt = $pdo->prepare("UPDATE messages SET content = ? WHERE id = ?");
$stmt->execute([$content, $messageId]);
// 2. Update the schedule
// When editing, we reset its status to 'pending' so the worker can pick it up again.
$stmt = $pdo->prepare(
"UPDATE schedules SET recipient_id = ?, send_time = ?, status = 'pending' WHERE id = ?"
);
$stmt->execute([$recipientId, $scheduleDateTime, $scheduleId]);
$pdo->commit();
// Redirect to a success page
header('Location: ../scheduled_messages.php?success=updated');
exit();
} catch (Exception $e) {
$pdo->rollBack();
error_log("Message update failed: " . $e->getMessage());
header('Location: ../edit_message.php?schedule_id=' . $scheduleId . '&error=dberror');
exit();
}
?>

View File

@@ -0,0 +1,97 @@
<?php
require_once __DIR__ . '/session_check.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/activity_logger.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../recurrentes.php');
exit;
}
$action = $_POST['action'] ?? '';
$userId = $_SESSION['user_id'] ?? 0;
$username = $_SESSION['username'] ?? 'Unknown';
// Function to redirect with a status message
function redirect_with_status($status, $message) {
header("Location: ../recurrentes.php?status=$status&message=" . urlencode($message));
exit;
}
try {
switch ($action) {
case 'create':
$name = trim($_POST['messageName'] ?? '');
$content = trim($_POST['messageContent'] ?? '');
$telegram_command = trim($_POST['telegram_command'] ?? '');
if (empty($name) || empty($content)) {
redirect_with_status('error', 'El nombre y el contenido del mensaje son obligatorios.');
}
// Si el comando no está vacío, asegúrate de que no contenga el '#'
if (!empty($telegram_command)) {
$telegram_command = ltrim($telegram_command, '#');
}
$stmt = $pdo->prepare(
"INSERT INTO recurrent_messages (name, telegram_command, message_content)
VALUES (?, ?, ?)"
);
$stmt->execute([$name, $telegram_command, $content]);
log_activity($userId, 'Recurrent Message Created', 'User ' . $username . ' created recurrent message: ' . $name);
redirect_with_status('success', 'Mensaje guardado exitosamente.');
break;
case 'activate':
case 'deactivate':
$id = $_POST['id'] ?? 0;
$is_active = ($action === 'activate') ? 1 : 0;
$stmt = $pdo->prepare("UPDATE recurrent_messages SET is_active = ? WHERE id = ?");
$stmt->execute([$is_active, $id]);
$status_message = ($action === 'activate') ? 'activado' : 'desactivado';
log_activity($userId, 'Recurrent Message Status Change', 'User ' . $username . ' ' . $status_message . ' recurrent message ID: ' . $id);
redirect_with_status('success', "Mensaje recurrente $status_message.");
break;
case 'update':
$id = $_POST['id'] ?? 0;
$name = trim($_POST['messageName'] ?? '');
$content = trim($_POST['messageContent'] ?? '');
$telegram_command = trim($_POST['telegram_command'] ?? '');
if (empty($id) || empty($name) || empty($content)) {
redirect_with_status('error', 'ID, nombre y contenido del mensaje son obligatorios para actualizar.');
}
// Si el comando no está vacío, asegúrate de que no contenga el '#'
if (!empty($telegram_command)) {
$telegram_command = ltrim($telegram_command, '#');
}
$stmt = $pdo->prepare(
"UPDATE recurrent_messages SET name = ?, telegram_command = ?, message_content = ? WHERE id = ?"
);
$stmt->execute([$name, $telegram_command, $content, $id]);
log_activity($userId, 'Recurrent Message Updated', 'User ' . $username . ' updated recurrent message ID: ' . $id . ' with name: ' . $name);
redirect_with_status('success', 'Mensaje recurrente actualizado exitosamente.');
break;
case 'delete':
$id = $_POST['id'] ?? 0;
$stmt = $pdo->prepare("DELETE FROM recurrent_messages WHERE id = ?");
$stmt->execute([$id]);
log_activity($userId, 'Recurrent Message Deleted', 'User ' . $username . ' deleted recurrent message ID: ' . $id);
redirect_with_status('success', 'Mensaje recurrente eliminado.');
break;
default:
redirect_with_status('error', 'Acción no válida.');
break;
}
} catch (PDOException $e) {
// Log the error in a real application
error_log("Error in recurrent_message_handler: " . $e->getMessage());
redirect_with_status('error', 'Error en la base de datos: ' . $e->getMessage());
}

70
includes/schedule_actions.php Executable file
View File

@@ -0,0 +1,70 @@
<?php
require_once __DIR__ . '/session_check.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/activity_logger.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../scheduled_messages.php');
exit();
}
$scheduleId = $_POST['schedule_id'] ?? null;
$action = $_POST['action'] ?? null;
$userId = $_SESSION['user_id'];
if (!$scheduleId || !$action) {
header('Location: ../scheduled_messages.php?error=missing_data');
exit();
}
$pdo->beginTransaction();
try {
$newStatus = '';
$successMessage = '';
switch ($action) {
case 'disable':
$newStatus = 'disabled';
$successMessage = 'disabled';
log_activity($userId, 'Message Disabled', 'Schedule ID: ' . $scheduleId);
break;
case 'enable':
$newStatus = 'pending';
$successMessage = 'enabled';
log_activity($userId, 'Message Enabled', 'Schedule ID: ' . $scheduleId);
break;
case 'cancel':
$newStatus = 'cancelled';
$successMessage = 'cancelled';
log_activity($userId, 'Message Sending Cancelled', 'Schedule ID: ' . $scheduleId);
break;
case 'retry':
$newStatus = 'pending';
$successMessage = 'retried';
log_activity($userId, 'Message Retried', 'Schedule ID: ' . $scheduleId);
break;
case 'delete':
$stmt = $pdo->prepare("DELETE FROM schedules WHERE id = ?");
$stmt->execute([$scheduleId]);
$successMessage = 'deleted';
log_activity($userId, 'Message Deleted', 'Schedule ID: ' . $scheduleId);
break;
default:
throw new Exception('Invalid action specified.');
}
if ($newStatus) {
$stmt = $pdo->prepare("UPDATE schedules SET status = ? WHERE id = ?");
$stmt->execute([$newStatus, $scheduleId]);
}
$pdo->commit();
header("Location: ../scheduled_messages.php?success={$successMessage}");
exit();
} catch (Exception $e) {
$pdo->rollBack();
error_log("ScheduleActions Error: " . $e->getMessage());
header('Location: ../scheduled_messages.php?error=dberror');
exit();
}

16
includes/schedule_helpers.php Executable file
View File

@@ -0,0 +1,16 @@
<?php
/**
* ARCHIVO BRIDGE - Migración Progresiva
*
* Este archivo mantiene compatibilidad con código existente
* mientras redirige a la nueva ubicación del helper.
*
* Nueva ubicación: /common/helpers/schedule_helpers.php
* Fecha de migración: 2025-11-25
*/
// Cargar el archivo desde la nueva ubicación
require_once __DIR__ . '/../common/helpers/schedule_helpers.php';
// Este archivo puede ser eliminado cuando toda la migración esté completa
// y todas las referencias apunten directamente a common/helpers/schedule_helpers.php

View File

@@ -0,0 +1,136 @@
<?php
// Este archivo contiene solo la lógica para renderizar el cuerpo de la tabla de mensajes programados.
// Se espera que sea incluido por scheduled_messages.php o llamado vía AJAX.
// Asegurarse de que las dependencias necesarias estén cargadas si se llama directamente vía AJAX
if (!defined('SCHEDULED_MESSAGES_LOADED')) {
require_once __DIR__ . '/db.php';
// La función getDayNames se define aquí para auto-contención
function getDayNames($daysString) {
if (empty($daysString)) return '<span data-translate="true">No especificado</span>';
$dayMap = [
0 => '<span data-translate="true">Domingo</span>',
1 => '<span data-translate="true">Lunes</span>',
2 => '<span data-translate="true">Martes</span>',
3 => '<span data-translate="true">Miércoles</span>',
4 => '<span data-translate="true">Jueves</span>',
5 => '<span data-translate="true">Viernes</span>',
6 => '<span data-translate="true">Sábado</span>'
];
$days = explode(',', $daysString);
$names = array_map(fn($day) => $dayMap[(int)$day] ?? '', $days);
return implode(', ', array_filter($names));
}
// Si no se ha cargado $messages, cargarla aquí (para llamadas AJAX directas)
if (!isset($messages)) {
$stmt = $pdo->prepare(
"SELECT
s.id as schedule_id,
s.send_time,
s.status,
s.is_recurring,
s.recurring_days,
s.recurring_time,
m.id as message_id,
m.content,
r.name as recipient_name,
r.type as recipient_type,
r.platform,
u.username as creator_username
FROM schedules s
JOIN messages m ON s.message_id = m.id
LEFT JOIN recipients r ON s.recipient_id = r.id
JOIN users u ON m.user_id = u.id
WHERE s.status IN ('draft', 'pending', 'failed', 'processing', 'disabled')
ORDER BY s.created_at DESC"
);
$stmt->execute();
$messages = $stmt->fetchAll();
}
}
?>
<?php if (empty($messages)):
?>
<tr>
<td colspan="7" class="text-center text-muted" data-translate="true">No hay mensajes aquí.</td>
</tr>
<?php else:
?>
<?php foreach ($messages as $msg):
?>
<tr>
<td>
<?php if (!empty($msg['platform'])): ?>
<span class="badge <?= $msg['platform'] === 'discord' ? 'bg-primary' : 'bg-info text-dark' ?>">
<i class="bi bi-<?= $msg['platform'] === 'discord' ? 'discord' : 'telegram' ?>"></i>
<?= htmlspecialchars(ucfirst($msg['platform'])) ?>
</span>
<?php else: ?>
<span class="badge bg-secondary" data-translate="true">N/A</span>
<?php endif; ?>
</td>
<td><?= !empty($msg['recipient_name']) ? htmlspecialchars($msg['recipient_name']) . ' <span class="text-muted" data-translate="true">(' . $msg['recipient_type'] . ')</span>' : '<span class="text-muted" data-translate="true">No asignado</span>' ?></td>
<td>
<div class="message-preview">
<?= substr(strip_tags($msg['content']), 0, 100); ?>...
</div>
</td>
<td>
<?php if ($msg['is_recurring'] == 1):
echo '<span data-translate="true">Semanal:</span> ' . getDayNames($msg['recurring_days']) . ' <span data-translate="true">a las</span> ' . substr($msg['recurring_time'], 0, 5);
else:
echo $msg['send_time'] ? date('d/m/Y H:i', strtotime($msg['send_time'])) : '-';
endif; ?>
</td>
<td>
<?php
$statusBadges = [
'pending' => 'info text-dark', 'draft' => 'secondary', 'failed' => 'danger',
'processing' => 'warning text-dark', 'disabled' => 'light text-dark'
];
$badgeClass = $statusBadges[$msg['status']] ?? 'light';
?>
<span class="badge bg-<?= $badgeClass ?>"><?= ucfirst($msg['status']) ?></span>
</td>
<td><?= htmlspecialchars($msg['creator_username']) ?></td>
<td class="text-center">
<div class="d-flex justify-content-center gap-1">
<a href="preview_message.php?id=<?= $msg['message_id'] ?>" target="_blank" class="btn btn-sm btn-info" title="Previsualizar" data-translate-title="true">
<i class="bi bi-eye"></i>
</a>
<a href="create_message.php?schedule_id=<?= $msg['schedule_id'] ?>&action=edit" class="btn btn-sm btn-primary" title="Editar" data-translate-title="true">
<i class="bi bi-pencil-square"></i>
</a>
<form action="includes/schedule_actions.php" method="POST" class="d-inline">
<input type="hidden" name="schedule_id" value="<?= $msg['schedule_id'] ?>">
<input type="hidden" name="message_id" value="<?= $msg['message_id'] ?>">
<?php if (in_array($msg['status'], ['pending', 'disabled'])): ?>
<button type="submit" name="action" value="<?= $msg['status'] === 'pending' ? 'disable' : 'enable' ?>" class="btn btn-sm btn-secondary" data-translate-title="true" title="<?= $msg['status'] === 'pending' ? 'Deshabilitar' : 'Habilitar' ?>">
<i class="bi <?= $msg['status'] === 'pending' ? 'bi-pause-circle' : 'bi-play-circle' ?>"></i>
</button>
<?php endif; ?>
<?php if ($msg['status'] === 'failed'): ?>
<button type="submit" name="action" value="retry" class="btn btn-sm btn-warning text-dark" title="Reintentar Envío" data-translate-title="true">
<i class="bi bi-arrow-repeat"></i>
</button>
<?php endif; ?>
<?php if ($msg['status'] === 'processing'): ?>
<button type="submit" name="action" value="cancel" class="btn btn-sm btn-warning" title="Cancelar Envío" data-translate-title="true" data-confirm-message="¿Estás seguro de que quieres cancelar este envío?" data-translate-confirm="true" onclick="return confirm(this.getAttribute('data-confirm-message'));">
<i class="bi bi-x-circle"></i>
</button>
<?php endif; ?>
<button type="submit" name="action" value="delete" class="btn btn-sm btn-danger" title="Borrar" data-translate-title="true" data-confirm-message="¿Estás seguro de que quieres borrar este mensaje?" data-translate-confirm="true" onclick="return confirm(this.getAttribute('data-confirm-message'));">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>

25
includes/session_check.php Executable file
View File

@@ -0,0 +1,25 @@
<?php
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_httponly', 1);
// Must be the very first thing on any protected page
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Incluir el helper de URLs y establecer las cabeceras de seguridad
require_once __DIR__ . '/../includes/url_helper.php';
require_once __DIR__ . '/db.php'; // Asegura que $pdo esté disponible
require_once __DIR__ . '/activity_logger.php'; // Asegura que log_activity esté disponible
set_security_headers();
// Generar token CSRF si no existe
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Verificar si el usuario ha iniciado sesión
if (!isset($_SESSION['user_id'])) {
header('Location: ' . site_url('login.php'));
exit();
}
?>

75
includes/telegram_actions.php Executable file
View File

@@ -0,0 +1,75 @@
<?php
require_once __DIR__ . '/session_check.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/../src/TelegramSender.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['action'])) {
header('Location: ../sent_messages.php');
exit();
}
$action = $_POST['action'];
if ($action === 'delete_message') {
if (!isset($_POST['sent_message_id'], $_POST['platform_message_id'], $_POST['chat_id'])) {
header('Location: ../sent_messages.php?error=missing_data');
exit();
}
$sentMessageId = $_POST['sent_message_id'];
$telegramMessageIdsJson = $_POST['platform_message_id'];
$chatId = $_POST['chat_id'];
$telegramMessageIds = json_decode($telegramMessageIdsJson, true);
// If decoding fails or the result is not an array, treat it as a single ID
if (json_last_error() !== JSON_ERROR_NONE || !is_array($telegramMessageIds)) {
$telegramMessageIds = [$telegramMessageIdsJson];
}
$telegramSender = new TelegramSender(TELEGRAM_BOT_TOKEN);
$allDeleted = true;
$error_messages = [];
try {
foreach ($telegramMessageIds as $messageId) {
// Skip if the ID is empty or invalid
if (empty($messageId) || !is_numeric($messageId)) {
error_log("Skipping invalid Telegram message ID: " . var_export($messageId, true));
continue;
}
try {
$telegramSender->deleteMessage($chatId, $messageId);
usleep(300000); // 300ms pause to avoid rate limits
} catch (Exception $e) {
$allDeleted = false;
$error_messages[] = "Failed to delete message ID {$messageId}: " . $e->getMessage();
error_log(end($error_messages)); // Log the last error
}
}
if ($allDeleted) {
// If all messages were deleted successfully, remove the record from DB
$stmt = $pdo->prepare("DELETE FROM sent_messages WHERE id = ?");
$stmt->execute([$sentMessageId]);
header('Location: ../sent_messages.php?success=deleted&platform=Telegram');
} else {
// If some deletions failed, redirect with an error message
$errorMessage = implode('; ', $error_messages);
header('Location: ../sent_messages.php?error=delete_failed&platform=Telegram&message=' . urlencode($errorMessage));
}
exit();
} catch (Exception $e) {
// Catch any other unexpected errors during the process
$errorMessage = "An unexpected error occurred: " . $e->getMessage();
error_log($errorMessage);
header('Location: ../sent_messages.php?error=unexpected_error&platform=Telegram&message=' . urlencode($errorMessage));
exit();
}
} // Closing brace for "if ($action === 'delete_message')"
// Fallback redirect if action is not 'delete_message'
header('Location: ../sent_messages.php');
exit();

210
includes/translation_helper.php Executable file
View File

@@ -0,0 +1,210 @@
<?php
/**
* Helper para manejar traducciones en la aplicación
*
* Este archivo proporciona funciones para manejar traducciones de manera consistente
* en toda la aplicación, incluyendo soporte para múltiples idiomas y caché de traducciones.
*/
// Evitar acceso directo
defined('ROOT_PATH') || define('ROOT_PATH', dirname(__DIR__));
// Inicializar el array de caché de traducciones
$GLOBALS['_translations_cache'] = [];
/**
* Obtiene una traducción para la clave dada en el idioma actual del usuario
*
* @param string $key Clave de traducción
* @param array $params Parámetros para reemplazar en la cadena de traducción
* @param string|null $language Código de idioma (opcional, por defecto usa el idioma de sesión)
* @return string Texto traducido o la clave si no se encuentra la traducción
*/
function __($key, $params = [], $language = null) {
global $pdo; // Asumiendo que $pdo está disponible globalmente
// Si no se proporciona un idioma, usar el de la sesión o el predeterminado
if ($language === null) {
$language = $_SESSION['language'] ?? 'es';
}
// Clave para el caché
$cache_key = $language . '_' . $key;
// Verificar si la traducción está en caché
if (isset($GLOBALS['_translations_cache'][$cache_key])) {
$translation = $GLOBALS['_translations_cache'][$cache_key];
} else {
// Si no está en caché, buscarla en la base de datos
try {
$stmt = $pdo->prepare(
"SELECT `value` FROM translations
WHERE `key` = :key AND language_code = :language
LIMIT 1"
);
$stmt->execute([
':key' => $key,
':language' => $language
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$translation = $result ? $result['value'] : '';
// Almacenar en caché para futuras solicitudes
$GLOBALS['_translations_cache'][$cache_key] = $translation;
} catch (PDOException $e) {
// En caso de error, devolver la clave como último recurso
error_log("Error al obtener traducción: " . $e->getMessage());
return $key;
}
}
// Si no se encontró la traducción, devolver la clave
if (empty($translation)) {
// Opcional: Registrar claves faltantes para facilitar la localización
log_missing_translation($key, $language);
return $key;
}
// Reemplazar parámetros si se proporcionan
if (!empty($params) && is_array($params)) {
foreach ($params as $param => $value) {
$translation = str_replace(":$param", $value, $translation);
}
}
return $translation;
}
/**
* Registra claves de traducción faltantes para facilitar la localización
*
* @param string $key Clave de traducción faltante
* @param string $language Código de idioma
*/
function log_missing_translation($key, $language) {
$log_file = ROOT_PATH . '/logs/missing_translations.log';
$log_entry = sprintf(
"[%s] Missing translation: %s (Language: %s)\n",
date('Y-m-d H:i:s'),
$key,
$language
);
// Asegurarse de que el directorio de logs existe
if (!is_dir(dirname($log_file))) {
@mkdir(dirname($log_file), 0755, true);
}
// Registrar en el archivo de log
@file_put_contents($log_file, $log_entry, FILE_APPEND);
}
/**
* Obtiene la lista de idiomas disponibles
*
* @param bool $only_active Si es true, solo devuelve los idiomas activos
* @return array Lista de idiomas
*/
function get_available_languages($only_active = true) {
global $pdo;
try {
$sql = "SELECT code, name, native_name, flag_emoji, is_active
FROM languages";
if ($only_active) {
$sql .= " WHERE is_active = 1";
}
$sql .= " ORDER BY name";
$stmt = $pdo->query($sql);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Error al obtener idiomas: " . $e->getMessage());
return [
['code' => 'es', 'name' => 'Spanish', 'native_name' => 'Español', 'flag_emoji' => '🇪🇸', 'is_active' => 1],
['code' => 'en', 'name' => 'English', 'native_name' => 'English', 'flag_emoji' => '🇬🇧', 'is_active' => 1]
];
}
}
/**
* Inicializa el sistema de traducciones en las plantillas
* Inyecta las traducciones necesarias para JavaScript
*
* @return string Código JavaScript con las traducciones (sin etiquetas <script>)
*/
function init_translations_for_js() {
global $pdo;
// Obtener el idioma actual
$language = $_SESSION['language'] ?? 'es';
// Obtener todas las traducciones para el idioma actual
try {
$stmt = $pdo->prepare(
"SELECT `key`, `value` FROM translations
WHERE language_code = :language"
);
$stmt->execute([':language' => $language]);
$translations = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
} catch (PDOException $e) {
error_log("Error al cargar traducciones para JS: " . $e->getMessage());
$translations = [];
}
// Devolver solo el código JavaScript (sin etiquetas <script>)
return "// Traducciones cargadas dinámicamente\n" .
"window.translations = " . json_encode($translations, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE) . ";\n";
}
/**
* Función para traducir texto en JavaScript
* Se debe llamar después de incluir el helper en el HTML
*/
function js_translation_function() {
echo <<<JS
<script>
/**
* Función para traducir texto en JavaScript
* @param {string} key - Clave de traducción
* @param {Object} params - Parámetros para reemplazar en la cadena
* @return {string} Texto traducido o la clave si no se encuentra
*/
function __(key, params = {}) {
let text = translations[key] || key;
// Reemplazar parámetros
Object.keys(params).forEach(param => {
text = text.replace(new RegExp(`:${param}`, 'g'), params[param]);
});
return text;
}
// Traducir elementos con el atributo data-translate
document.addEventListener('DOMContentLoaded', function() {
const elements = document.querySelectorAll('[data-translate]');
elements.forEach(element => {
const key = element.getAttribute('data-translate');
if (translations[key]) {
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
element.placeholder = translations[key];
} else if (element.tagName === 'BUTTON' || element.tagName === 'A') {
element.textContent = translations[key];
} else {
element.textContent = translations[key];
}
}
});
});
</script>
JS;
}

79
includes/tren_handler.php Executable file
View File

@@ -0,0 +1,79 @@
<?php
require_once __DIR__ . '/session_check.php';
require_once __DIR__ . '/db.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../tren.php');
exit();
}
$userId = $_SESSION['user_id'];
$content = $_POST['messageContent'] ?? '';
$recipientId = $_POST['recipientId'] ?? '';
$scheduleType = $_POST['scheduleType'] ?? 'now';
// Validation
if (empty($recipientId) || empty($content)) {
header('Location: ../tren.php?error=missing_fields');
exit();
}
$pdo->beginTransaction();
try {
// 1. Save the message content
$stmt = $pdo->prepare("INSERT INTO messages (user_id, content) VALUES (?, ?)");
$stmt->execute([$userId, $content]);
$messageId = $pdo->lastInsertId();
// 2. Schedule the message
$sendTime = null;
$status = 'pending';
if ($scheduleType === 'later') {
if (empty($_POST['scheduleDateTime'])) {
throw new Exception('La fecha y hora de envío son requeridas para programar.');
}
$sendTime = (new DateTime($_POST['scheduleDateTime'], new DateTimeZone('America/Mexico_City')))->format('Y-m-d H:i:s');
} else { // 'now'
$sendTime = date('Y-m-d H:i:s');
}
$stmt = $pdo->prepare(
"INSERT INTO schedules (message_id, recipient_id, send_time, status, is_recurring, recurring_days, recurring_time) VALUES (?, ?, ?, ?, 0, NULL, NULL)"
);
$stmt->execute([$messageId, $recipientId, $sendTime, $status]);
$pdo->commit();
// If it's an immediate send, trigger processing
if ($scheduleType === 'now') {
$phpPath = PHP_BINARY ?: 'php';
$scriptPath = dirname(__DIR__) . '/process_queue.php';
$logPath = dirname(__DIR__) . '/logs/process_queue_manual.log';
// Asegurarse de que se use el entorno correcto
$command = sprintf(
'APP_ENVIRONMENT=pruebas %s %s >> %s 2>&1 &',
escapeshellarg($phpPath),
escapeshellarg($scriptPath),
escapeshellarg($logPath)
);
// Registrar el comando que se va a ejecutar para depuración
error_log("Ejecutando comando desde tren_handler.php: " . $command . "\n", 3, dirname(__DIR__) . '/logs/tren_handler.log');
// Ejecutar el comando
shell_exec($command);
}
header('Location: ../scheduled_messages.php?success=message_created');
exit();
} catch (Exception $e) {
$pdo->rollBack();
// Log the error properly in a real application
header('Location: ../tren.php?error=dberror');
exit();
}
?>

16
includes/url_helper.php Executable file
View File

@@ -0,0 +1,16 @@
<?php
/**
* ARCHIVO BRIDGE - Migración Progresiva
*
* Este archivo mantiene compatibilidad con código existente
* mientras redirige a la nueva ubicación del helper.
*
* Nueva ubicación: /common/helpers/url_helper.php
* Fecha de migración: 2025-11-25
*/
// Cargar el archivo desde la nueva ubicación
require_once __DIR__ . '/../common/helpers/url_helper.php';
// Este archivo puede ser eliminado cuando toda la migración esté completa
// y todas las referencias apunten directamente a common/helpers/url_helper.php