Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,82 +0,0 @@
|
|||||||
|
|
||||||
<?php
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
|
|
||||||
$error = '';
|
|
||||||
$channel_identifier = '';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$channel_identifier = trim($_POST['channel_identifier'] ?? '');
|
|
||||||
|
|
||||||
if (empty($channel_identifier)) {
|
|
||||||
$error = 'Telegram Channel ID or URL is required.';
|
|
||||||
} else {
|
|
||||||
// Basic parsing to get ID from URL
|
|
||||||
if (filter_var($channel_identifier, FILTER_VALIDATE_URL)) {
|
|
||||||
$path = parse_url($channel_identifier, PHP_URL_PATH);
|
|
||||||
$channel_identifier = basename($path);
|
|
||||||
}
|
|
||||||
// Prepend '@' if it's a public username without it
|
|
||||||
if (strpos($channel_identifier, '@') !== 0 && !is_numeric($channel_identifier)) {
|
|
||||||
$channel_identifier = '@' . $channel_identifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO channels (channel_identifier) VALUES (:channel_identifier)");
|
|
||||||
$stmt->execute([':channel_identifier' => $channel_identifier]);
|
|
||||||
|
|
||||||
session_start();
|
|
||||||
$_SESSION['flash_message'] = "Channel '{$channel_identifier}' added successfully!";
|
|
||||||
header("Location: index.php");
|
|
||||||
exit;
|
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
if ($e->errorInfo[1] == 1062) { // Duplicate entry
|
|
||||||
$error = "Error: Channel '{$channel_identifier}' already exists.";
|
|
||||||
} else {
|
|
||||||
$error = "Database error: " . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once __DIR__ . '/layout_header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container-fluid">
|
|
||||||
<h1 class="h2 mb-4">Add New Channel</h1>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<?php if ($error): ?>
|
|
||||||
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<form action="add_channel.php" method="POST">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="channel_identifier" class="form-label">Telegram Channel ID or URL</label>
|
|
||||||
<input type="text" class="form-control" id="channel_identifier" name="channel_identifier"
|
|
||||||
placeholder="e.g., @channelname or https://t.me/channelname"
|
|
||||||
value="<?php echo htmlspecialchars($channel_identifier); ?>" required>
|
|
||||||
<div class="form-text text-secondary-color">
|
|
||||||
Provide the public channel username (e.g., @durov) or full URL.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i data-feather="plus" class="me-1" style="width:16px; height:16px;"></i>
|
|
||||||
Add Channel
|
|
||||||
</button>
|
|
||||||
<a href="index.php" class="btn btn-secondary">Cancel</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
require_once __DIR__ . '/layout_footer.php';
|
|
||||||
?>
|
|
||||||
129
analytics.php
129
analytics.php
@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
require_once 'layout_header.php';
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
// --- Fetch Key Metrics ---
|
|
||||||
$stats = [
|
|
||||||
'total' => 0,
|
|
||||||
'published' => 0,
|
|
||||||
'failed' => 0,
|
|
||||||
'pending' => 0
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT status, COUNT(*) as count FROM scheduled_posts GROUP BY status");
|
|
||||||
$results = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
||||||
|
|
||||||
$stats['published'] = $results['published'] ?? 0;
|
|
||||||
$stats['failed'] = $results['failed'] ?? 0;
|
|
||||||
$stats['pending'] = $results['pending'] ?? 0;
|
|
||||||
$stats['total'] = array_sum($stats);
|
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// Handle error, maybe show a message
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Fetch Recent Webhook Events ---
|
|
||||||
$recent_events = [];
|
|
||||||
try {
|
|
||||||
$event_stmt = $pdo->query("SELECT * FROM webhook_events ORDER BY received_at DESC LIMIT 15");
|
|
||||||
$recent_events = $event_stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container-fluid p-4">
|
|
||||||
<h1 class="h3 mb-4 text-white">Analytics</h1>
|
|
||||||
<p class="text-muted mb-4">This page provides an overview of your post scheduling and publishing activity, based on status updates received from your n8n workflows.</p>
|
|
||||||
|
|
||||||
<!-- Key Metrics -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-xl-3 col-md-6 mb-4">
|
|
||||||
<div class="card bg-dark-surface h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title text-muted text-uppercase">Total Scheduled</h6>
|
|
||||||
<h2 class="h1 fw-bold mb-0"><?php echo $stats['total']; ?></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-3 col-md-6 mb-4">
|
|
||||||
<div class="card bg-dark-surface h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title text-muted text-uppercase">Successfully Published</h6>
|
|
||||||
<h2 class="h1 fw-bold mb-0 text-success"><?php echo $stats['published']; ?></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-3 col-md-6 mb-4">
|
|
||||||
<div class="card bg-dark-surface h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title text-muted text-uppercase">Failed to Publish</h6>
|
|
||||||
<h2 class="h1 fw-bold mb-0 text-danger"><?php echo $stats['failed']; ?></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-3 col-md-6 mb-4">
|
|
||||||
<div class="card bg-dark-surface h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title text-muted text-uppercase">Pending / In Progress</h6>
|
|
||||||
<h2 class="h1 fw-bold mb-0 text-warning"><?php echo $stats['pending']; ?></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Recent Activity Log -->
|
|
||||||
<div class="card bg-dark-surface">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Recent Activity (from Webhooks)</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-dark">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Post ID</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Message</th>
|
|
||||||
<th>Received At</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($recent_events)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="text-center text-muted">No recent webhook events found.</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($recent_events as $event): ?>
|
|
||||||
<tr>
|
|
||||||
<td>#<?php echo htmlspecialchars($event['post_id']); ?></td>
|
|
||||||
<td>
|
|
||||||
<?php
|
|
||||||
$status_class = 'bg-secondary';
|
|
||||||
if (in_array($event['status'], ['success', 'published'])) {
|
|
||||||
$status_class = 'bg-success';
|
|
||||||
} elseif (in_array($event['status'], ['error', 'failed'])) {
|
|
||||||
$status_class = 'bg-danger';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<span class="badge <?php echo $status_class; ?>"><?php echo htmlspecialchars(ucfirst($event['status'])); ?></span>
|
|
||||||
</td>
|
|
||||||
<td><?php echo htmlspecialchars($event['message']); ?></td>
|
|
||||||
<td><?php echo date("F j, Y, g:i a", strtotime($event['received_at'])); ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
require_once 'layout_footer.php';
|
|
||||||
?>
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
:root {
|
|
||||||
--bg-color: #121212;
|
|
||||||
--surface-color: #1E1E1E;
|
|
||||||
--primary-color: #00A8FF;
|
|
||||||
--secondary-color: #4CAF50;
|
|
||||||
--text-color: #FFFFFF;
|
|
||||||
--text-secondary-color: #B0B0B0;
|
|
||||||
--border-color: #2c2c2c;
|
|
||||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
--border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
font-family: var(--font-family);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
height: 100vh;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-brand .brand-icon {
|
|
||||||
color: var(--primary-color);
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav-link {
|
|
||||||
color: var(--text-secondary-color);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
transition: background-color 0.2s ease, color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav-link i {
|
|
||||||
margin-right: 1rem;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav-link:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav-link.active {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
margin-left: 250px;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
background-color: rgba(255, 255, 255, 0.03);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
font-weight: 600;
|
|
||||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: #0094e0;
|
|
||||||
border-color: #0094e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control, .form-select {
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
border-color: var(--border-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus, .form-select:focus {
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
box-shadow: 0 0 0 0.25rem rgba(0, 168, 255, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control::placeholder {
|
|
||||||
color: var(--text-secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-header {
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
color: var(--text-color);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
.btn-close {
|
|
||||||
filter: invert(1) grayscale(100%) brightness(200%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style for code blocks in documentation */
|
|
||||||
.code-block-dark {
|
|
||||||
background-color: #2d2d2d; /* A slightly lighter dark */
|
|
||||||
color: #e0e0e0; /* Light grey text */
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
// Empty for now, can be used for more complex interactions later.
|
|
||||||
console.log("main.js loaded");
|
|
||||||
94
channels.php
94
channels.php
@ -1,94 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
require_once 'layout_header.php';
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$message = '';
|
|
||||||
$type = '';
|
|
||||||
|
|
||||||
// Handle Delete action
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_channel') {
|
|
||||||
$channel_id_to_delete = $_POST['channel_id'] ?? 0;
|
|
||||||
if ($channel_id_to_delete > 0) {
|
|
||||||
try {
|
|
||||||
$delete_stmt = $pdo->prepare("DELETE FROM channels WHERE id = ?");
|
|
||||||
$delete_stmt->execute([$channel_id_to_delete]);
|
|
||||||
$message = 'Channel deleted successfully. All associated scheduled posts have also been removed.';
|
|
||||||
$type = 'success';
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$message = 'Error deleting channel: ' . $e->getMessage();
|
|
||||||
$type = 'danger';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, channel_identifier, created_at FROM channels ORDER BY created_at DESC");
|
|
||||||
$channels = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$message = "Error fetching channels: " . $e->getMessage();
|
|
||||||
$type = 'danger';
|
|
||||||
$channels = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container-fluid p-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h1 class="h3 mb-0 text-white">Channels</h1>
|
|
||||||
<a href="add_channel.php" class="btn btn-primary">
|
|
||||||
<i data-feather="plus" class="me-1"></i>
|
|
||||||
Add Channel
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($message): ?>
|
|
||||||
<div class="alert alert-<?php echo $type; ?> alert-dismissible fade show" role="alert">
|
|
||||||
<?php echo htmlspecialchars($message); ?>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="card bg-dark-surface">
|
|
||||||
<div class="card-body">
|
|
||||||
<?php if (empty($channels)): ?>
|
|
||||||
<div class="text-center p-4">
|
|
||||||
<p class="text-muted">No channels found. Get started by adding one.</p>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-dark table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">ID</th>
|
|
||||||
<th scope="col">Channel ID/URL</th>
|
|
||||||
<th scope="col">Added On</th>
|
|
||||||
<th scope="col">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($channels as $channel): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?php echo htmlspecialchars($channel['id']); ?></td>
|
|
||||||
<td><?php echo htmlspecialchars($channel['channel_identifier']); ?></td>
|
|
||||||
<td><?php echo date("F j, Y, g:i a", strtotime($channel['created_at'])); ?></td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" action="channels.php" onsubmit="return confirm('Are you sure you want to delete this channel? This will also delete all scheduled posts for this channel.');" style="display: inline;">
|
|
||||||
<input type="hidden" name="action" value="delete_channel">
|
|
||||||
<input type="hidden" name="channel_id" value="<?php echo $channel['id']; ?>">
|
|
||||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
require_once 'layout_footer.php';
|
|
||||||
?>
|
|
||||||
200
index.php
200
index.php
@ -1,58 +1,150 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/db/config.php';
|
declare(strict_types=1);
|
||||||
|
@ini_set('display_errors', '1');
|
||||||
|
@error_reporting(E_ALL);
|
||||||
|
@date_default_timezone_set('UTC');
|
||||||
|
|
||||||
// Simple migration runner
|
$phpVersion = PHP_VERSION;
|
||||||
try {
|
$now = date('Y-m-d H:i:s');
|
||||||
$pdo = db();
|
?>
|
||||||
// Main Tables
|
<!doctype html>
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS channels (id INT AUTO_INCREMENT PRIMARY KEY, channel_identifier VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL);");
|
<html lang="en">
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS scheduled_posts (id INT AUTO_INCREMENT PRIMARY KEY, channel_id INT NOT NULL, message TEXT NOT NULL, scheduled_at DATETIME NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE);");
|
<head>
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS settings (setting_key VARCHAR(255) PRIMARY KEY, setting_value TEXT);");
|
<meta charset="utf-8" />
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS webhook_events (id INT AUTO_INCREMENT PRIMARY KEY, post_id INT, status VARCHAR(50), message TEXT, raw_payload TEXT, received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY (post_id));");
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>New Style</title>
|
||||||
// Simple migration to rename telegram_id to channel_identifier for backwards compatibility
|
<?php
|
||||||
$checkColumn = $pdo->query("SHOW COLUMNS FROM `channels` LIKE 'telegram_id'");
|
// Read project preview data from environment
|
||||||
if ($checkColumn->rowCount() > 0) {
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
$pdo->exec("ALTER TABLE `channels` CHANGE `telegram_id` `channel_identifier` VARCHAR(255) NOT NULL UNIQUE;");
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
|
?>
|
||||||
|
<?php if ($projectDescription): ?>
|
||||||
|
<!-- Meta description -->
|
||||||
|
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||||
|
<!-- Open Graph meta tags -->
|
||||||
|
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
|
<!-- Twitter meta tags -->
|
||||||
|
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($projectImageUrl): ?>
|
||||||
|
<!-- Open Graph image -->
|
||||||
|
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
|
<!-- Twitter image -->
|
||||||
|
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color-start: #6a11cb;
|
||||||
|
--bg-color-end: #2575fc;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
body {
|
||||||
} catch (PDOException $e) {
|
margin: 0;
|
||||||
// In a real app, log this error. For now, we die.
|
font-family: 'Inter', sans-serif;
|
||||||
die("Database migration failed: " . $e->getMessage());
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
}
|
color: var(--text-color);
|
||||||
|
display: flex;
|
||||||
// Fetch channel count
|
justify-content: center;
|
||||||
$stmt = $pdo->query("SELECT COUNT(*) FROM channels");
|
align-items: center;
|
||||||
$channel_count = $stmt->fetchColumn();
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
require_once __DIR__ . '/layout_header.php';
|
overflow: hidden;
|
||||||
?>
|
position: relative;
|
||||||
|
}
|
||||||
<div class="container-fluid">
|
body::before {
|
||||||
<h1 class="h2 mb-4">Dashboard</h1>
|
content: '';
|
||||||
<div class="row">
|
position: absolute;
|
||||||
<div class="col-md-4">
|
top: 0;
|
||||||
<div class="card text-white">
|
left: 0;
|
||||||
<div class="card-body">
|
width: 100%;
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
height: 100%;
|
||||||
<div>
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||||
<h5 class="card-title">Total Channels</h5>
|
animation: bg-pan 20s linear infinite;
|
||||||
<p class="card-text fs-2 fw-bold"><?php echo $channel_count; ?></p>
|
z-index: -1;
|
||||||
</div>
|
}
|
||||||
<div class="fs-1 text-primary">
|
@keyframes bg-pan {
|
||||||
<i data-feather="tv"></i>
|
0% { background-position: 0% 0%; }
|
||||||
</div>
|
100% { background-position: 100% 100%; }
|
||||||
</div>
|
}
|
||||||
<a href="add_channel.php" class="btn btn-primary mt-3">
|
main {
|
||||||
<i data-feather="plus" class="me-1" style="width:16px; height:16px;"></i>
|
padding: 2rem;
|
||||||
Add Channel
|
}
|
||||||
</a>
|
.card {
|
||||||
</div>
|
background: var(--card-bg-color);
|
||||||
</div>
|
border: 1px solid var(--card-border-color);
|
||||||
</div>
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.loader {
|
||||||
|
margin: 1.25rem auto 1.25rem;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px; height: 1px;
|
||||||
|
padding: 0; margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap; border: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Analyzing your requirements and generating your website…</h1>
|
||||||
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
|
<span class="sr-only">Loading…</span>
|
||||||
|
</div>
|
||||||
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
|
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
<footer>
|
||||||
<?php
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
require_once __DIR__ . '/layout_footer.php';
|
</footer>
|
||||||
?>
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
|
|
||||||
<script>
|
|
||||||
feather.replace();
|
|
||||||
|
|
||||||
// Auto-hide toast
|
|
||||||
const flashToast = document.getElementById('flashToast');
|
|
||||||
if (flashToast) {
|
|
||||||
const toast = new bootstrap.Toast(flashToast, { delay: 5000 });
|
|
||||||
toast.show();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
|
|
||||||
<?php
|
|
||||||
session_start();
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>telegra</title>
|
|
||||||
<meta name="description" content="Custom CRM System for Managing Multiple Telegram Public Channels. Built with Flatlogic Generator.">
|
|
||||||
<meta name="keywords" content="telegram crm, channel management, post scheduler, telegram analytics, content automation, n8n integration, telegram pubs, social media tool, Built with Flatlogic Generator">
|
|
||||||
<meta property="og:title" content="telegra">
|
|
||||||
<meta property="og:description" content="Custom CRM System for Managing Multiple Telegram Public Channels. Built with Flatlogic Generator.">
|
|
||||||
<meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
|
||||||
<meta name="twitter:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
|
||||||
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="d-flex">
|
|
||||||
<nav class="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<a href="index.php" class="sidebar-brand">
|
|
||||||
<i data-feather="send" class="brand-icon"></i>
|
|
||||||
<span>telegra</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
$current_page = basename($_SERVER['PHP_SELF']);
|
|
||||||
$menu_items = [
|
|
||||||
'index.php' => ['icon' => 'home', 'label' => 'Dashboard'],
|
|
||||||
'channels.php' => ['icon' => 'tv', 'label' => 'Channels'],
|
|
||||||
'scheduler.php' => ['icon' => 'calendar', 'label' => 'Scheduler'],
|
|
||||||
'analytics.php' => ['icon' => 'bar-chart-2', 'label' => 'Analytics'],
|
|
||||||
'settings.php' => ['icon' => 'settings', 'label' => 'Settings'],
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
<ul class="nav flex-column">
|
|
||||||
<?php foreach ($menu_items as $url => $item): ?>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link <?php echo ($current_page == $url) ? 'active' : ''; ?>" href="<?php echo $url; ?>">
|
|
||||||
<i data-feather="<?php echo $item['icon']; ?>"></i>
|
|
||||||
<span><?php echo $item['label']; ?></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<main class="main-content flex-grow-1">
|
|
||||||
<?php if (isset($_SESSION['flash_message'])): ?>
|
|
||||||
<div class="toast-container position-fixed top-0 end-0 p-3">
|
|
||||||
<div id="flashToast" class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
|
|
||||||
<div class="toast-header">
|
|
||||||
<strong class="me-auto">Notification</strong>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body">
|
|
||||||
<?php echo $_SESSION['flash_message']; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php unset($_SESSION['flash_message']); ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
135
logs/webhook.log
135
logs/webhook.log
@ -1,135 +0,0 @@
|
|||||||
[2025-10-27 13:19:02] - Received webhook.
|
|
||||||
Headers: {
|
|
||||||
"Host": "telegra.dev.flatlogic.app",
|
|
||||||
"User-Agent": "Go-http-client\/2.0",
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Cdn-Loop": "cloudflare; loops=1; subreqs=1",
|
|
||||||
"Cf-Connecting-Ip": "2a06:98c0:3600::103",
|
|
||||||
"Cf-Ew-Via": "15",
|
|
||||||
"Cf-Ipcountry": "UA",
|
|
||||||
"Cf-Ray": "99527b98b5cac045-WAW",
|
|
||||||
"Cf-Visitor": "{\"scheme\":\"http\"}",
|
|
||||||
"Cf-Warp-Tag-Id": "2dc3820a-6205-48d9-b0ca-041c1b8c9e3e",
|
|
||||||
"Cf-Worker": "flatlogic.app",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"X-Forwarded-For": "2a06:98c0:3600::103",
|
|
||||||
"X-Forwarded-Proto": "http"
|
|
||||||
}
|
|
||||||
Payload:
|
|
||||||
|
|
||||||
[2025-10-27 13:19:03] - Received webhook.
|
|
||||||
Headers: {
|
|
||||||
"Host": "telegra.dev.flatlogic.app",
|
|
||||||
"User-Agent": "curl\/8.5.0",
|
|
||||||
"Content-Length": "149",
|
|
||||||
"Accept": "*\/*",
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Cdn-Loop": "cloudflare; loops=1; subreqs=1",
|
|
||||||
"Cf-Connecting-Ip": "37.53.100.68",
|
|
||||||
"Cf-Ew-Via": "15",
|
|
||||||
"Cf-Ipcountry": "UA",
|
|
||||||
"Cf-Ray": "99527b9b1622c045-WAW",
|
|
||||||
"Cf-Visitor": "{\"scheme\":\"http\"}",
|
|
||||||
"Cf-Warp-Tag-Id": "2dc3820a-6205-48d9-b0ca-041c1b8c9e3e",
|
|
||||||
"Cf-Worker": "flatlogic.app",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"Content-Type": "application\/json",
|
|
||||||
"X-Forwarded-For": "37.53.100.68",
|
|
||||||
"X-Forwarded-Proto": "http"
|
|
||||||
}
|
|
||||||
Payload: {"status":"error","order_id":"sub_348228662_20251021_8","amount":"149.00","currency":"UAH","description":"Internal error","email":"test@example.com"}
|
|
||||||
|
|
||||||
[2025-10-27 13:19:43] - Received webhook.
|
|
||||||
Headers: {
|
|
||||||
"Host": "telegra.dev.flatlogic.app",
|
|
||||||
"User-Agent": "Go-http-client\/2.0",
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Cdn-Loop": "cloudflare; loops=1; subreqs=1",
|
|
||||||
"Cf-Connecting-Ip": "2a06:98c0:3600::103",
|
|
||||||
"Cf-Ew-Via": "15",
|
|
||||||
"Cf-Ipcountry": "UA",
|
|
||||||
"Cf-Ray": "99527c98b2e9c045-WAW",
|
|
||||||
"Cf-Visitor": "{\"scheme\":\"http\"}",
|
|
||||||
"Cf-Warp-Tag-Id": "2dc3820a-6205-48d9-b0ca-041c1b8c9e3e",
|
|
||||||
"Cf-Worker": "flatlogic.app",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"X-Forwarded-For": "2a06:98c0:3600::103",
|
|
||||||
"X-Forwarded-Proto": "http"
|
|
||||||
}
|
|
||||||
Payload:
|
|
||||||
|
|
||||||
[2025-10-27 13:19:43] - Received webhook.
|
|
||||||
Headers: {
|
|
||||||
"Host": "telegra.dev.flatlogic.app",
|
|
||||||
"User-Agent": "curl\/8.5.0",
|
|
||||||
"Content-Length": "197",
|
|
||||||
"Accept": "*\/*",
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Cdn-Loop": "cloudflare; loops=1; subreqs=1",
|
|
||||||
"Cf-Connecting-Ip": "37.53.100.68",
|
|
||||||
"Cf-Ew-Via": "15",
|
|
||||||
"Cf-Ipcountry": "UA",
|
|
||||||
"Cf-Ray": "99527c999306c045-WAW",
|
|
||||||
"Cf-Visitor": "{\"scheme\":\"http\"}",
|
|
||||||
"Cf-Warp-Tag-Id": "2dc3820a-6205-48d9-b0ca-041c1b8c9e3e",
|
|
||||||
"Cf-Worker": "flatlogic.app",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"Content-Type": "application\/json",
|
|
||||||
"X-Forwarded-For": "37.53.100.68",
|
|
||||||
"X-Forwarded-Proto": "http"
|
|
||||||
}
|
|
||||||
Payload: {
|
|
||||||
"event": "new_post_scheduled",
|
|
||||||
"channel_identifier": "@my_telegram_channel",
|
|
||||||
"post_text_idea": "A post about morning coffee",
|
|
||||||
"scheduled_at_utc": "2025-10-28 14:30:00",
|
|
||||||
"post_id": 123
|
|
||||||
}
|
|
||||||
|
|
||||||
[2025-10-27 13:20:31] - Received webhook.
|
|
||||||
Headers: {
|
|
||||||
"Host": "telegra.dev.flatlogic.app",
|
|
||||||
"User-Agent": "Go-http-client\/2.0",
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Cdn-Loop": "cloudflare; loops=1; subreqs=1",
|
|
||||||
"Cf-Connecting-Ip": "2a06:98c0:3600::103",
|
|
||||||
"Cf-Ew-Via": "15",
|
|
||||||
"Cf-Ipcountry": "UA",
|
|
||||||
"Cf-Ray": "99527dc48185c045-WAW",
|
|
||||||
"Cf-Visitor": "{\"scheme\":\"http\"}",
|
|
||||||
"Cf-Warp-Tag-Id": "2dc3820a-6205-48d9-b0ca-041c1b8c9e3e",
|
|
||||||
"Cf-Worker": "flatlogic.app",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"X-Forwarded-For": "2a06:98c0:3600::103",
|
|
||||||
"X-Forwarded-Proto": "http"
|
|
||||||
}
|
|
||||||
Payload:
|
|
||||||
|
|
||||||
[2025-10-27 13:20:31] - Received webhook.
|
|
||||||
Headers: {
|
|
||||||
"Host": "telegra.dev.flatlogic.app",
|
|
||||||
"User-Agent": "curl\/8.5.0",
|
|
||||||
"Content-Length": "192",
|
|
||||||
"Accept": "*\/*",
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Cdn-Loop": "cloudflare; loops=1; subreqs=1",
|
|
||||||
"Cf-Connecting-Ip": "37.53.100.68",
|
|
||||||
"Cf-Ew-Via": "15",
|
|
||||||
"Cf-Ipcountry": "UA",
|
|
||||||
"Cf-Ray": "99527dc561b0c045-WAW",
|
|
||||||
"Cf-Visitor": "{\"scheme\":\"http\"}",
|
|
||||||
"Cf-Warp-Tag-Id": "2dc3820a-6205-48d9-b0ca-041c1b8c9e3e",
|
|
||||||
"Cf-Worker": "flatlogic.app",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"Content-Type": "application\/json",
|
|
||||||
"X-Forwarded-For": "37.53.100.68",
|
|
||||||
"X-Forwarded-Proto": "http"
|
|
||||||
}
|
|
||||||
Payload: {
|
|
||||||
"event": "new_post_scheduled",
|
|
||||||
"channel_identifier": "@bazhayu_zdorovya",
|
|
||||||
"post_text_idea": "A post about morning coffee",
|
|
||||||
"scheduled_at_utc": "2025-10-28 14:30:00",
|
|
||||||
"post_id": 1
|
|
||||||
}
|
|
||||||
|
|
||||||
243
scheduler.php
243
scheduler.php
@ -1,243 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
require_once 'layout_header.php';
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
// Create scheduled_posts table if it doesn't exist
|
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS scheduled_posts (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
channel_id INT NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
scheduled_at DATETIME NOT NULL,
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
|
|
||||||
);");
|
|
||||||
|
|
||||||
$message = '';
|
|
||||||
$type = '';
|
|
||||||
|
|
||||||
// Handle deletion of a scheduled post
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_post') {
|
|
||||||
$post_id_to_delete = $_POST['post_id'] ?? 0;
|
|
||||||
if ($post_id_to_delete > 0) {
|
|
||||||
try {
|
|
||||||
$delete_stmt = $pdo->prepare("DELETE FROM scheduled_posts WHERE id = ?");
|
|
||||||
$delete_stmt->execute([$post_id_to_delete]);
|
|
||||||
$message = 'Scheduled post cancelled successfully.';
|
|
||||||
$type = 'success';
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$message = 'Error cancelling post: ' . $e->getMessage();
|
|
||||||
$type = 'danger';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle form submission for bulk scheduling
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['schedule_post'])) {
|
|
||||||
$channel_id = $_POST['channel_id'];
|
|
||||||
$post_idea = $_POST['message'];
|
|
||||||
$schedule_date = $_POST['schedule_date'];
|
|
||||||
$post_count = isset($_POST['post_count']) ? (int)$_POST['post_count'] : 0;
|
|
||||||
|
|
||||||
if (empty($channel_id) || empty($post_idea) || empty($schedule_date) || $post_count <= 0) {
|
|
||||||
$message = 'Please fill out all fields and specify a valid number of posts.';
|
|
||||||
$type = 'danger';
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO scheduled_posts (channel_id, message, scheduled_at) VALUES (?, ?, ?)");
|
|
||||||
|
|
||||||
// Fetch webhook URL and channel identifier once before the loop
|
|
||||||
$webhook_stmt = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'webhook_new_post'");
|
|
||||||
$webhook_url = $webhook_stmt->fetchColumn();
|
|
||||||
$identifier_stmt = $pdo->prepare("SELECT channel_identifier FROM channels WHERE id = ?");
|
|
||||||
$identifier_stmt->execute([$channel_id]);
|
|
||||||
$channel_identifier = $identifier_stmt->fetchColumn();
|
|
||||||
|
|
||||||
$start_hour = 8; // 8 AM
|
|
||||||
$end_hour = 22; // 10 PM
|
|
||||||
$total_hours = $end_hour - $start_hour;
|
|
||||||
$interval_minutes = ($post_count > 1) ? floor(($total_hours * 60) / ($post_count - 1)) : 0;
|
|
||||||
|
|
||||||
$posts_scheduled = 0;
|
|
||||||
for ($i = 0; $i < $post_count; $i++) {
|
|
||||||
$minutes_to_add = $i * $interval_minutes;
|
|
||||||
$scheduled_datetime = new DateTime($schedule_date . ' ' . $start_hour . ':00');
|
|
||||||
$scheduled_datetime->add(new DateInterval('PT' . $minutes_to_add . 'M'));
|
|
||||||
$scheduled_at_str = $scheduled_datetime->format('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
$stmt->execute([$channel_id, $post_idea, $scheduled_at_str]);
|
|
||||||
$last_post_id = $pdo->lastInsertId();
|
|
||||||
|
|
||||||
// Trigger webhook for each post
|
|
||||||
if ($webhook_url && $channel_identifier) {
|
|
||||||
$post_data = [
|
|
||||||
'event' => 'new_post_scheduled',
|
|
||||||
'post_id' => $last_post_id,
|
|
||||||
'channel_identifier' => $channel_identifier,
|
|
||||||
'post_text_idea' => $post_idea,
|
|
||||||
'scheduled_at_utc' => gmdate('Y-m-d H:i:s', strtotime($scheduled_at_str))
|
|
||||||
];
|
|
||||||
|
|
||||||
$ch = curl_init($webhook_url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
||||||
curl_exec($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
}
|
|
||||||
$posts_scheduled++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
$message = "$posts_scheduled post(s) scheduled successfully!";
|
|
||||||
$type = 'success';
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
$message = 'Error scheduling posts: ' . $e->getMessage();
|
|
||||||
$type = 'danger';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch channels for the dropdown
|
|
||||||
try {
|
|
||||||
$channels_stmt = $pdo->query("SELECT id, channel_identifier FROM channels ORDER BY channel_identifier ASC");
|
|
||||||
$channels = $channels_stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$channels = [];
|
|
||||||
$message = 'Error fetching channels: ' . $e->getMessage();
|
|
||||||
$type = 'danger';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch scheduled posts for the table
|
|
||||||
try {
|
|
||||||
$posts_stmt = $pdo->query("
|
|
||||||
SELECT sp.id, c.channel_identifier, sp.message, sp.scheduled_at, sp.status
|
|
||||||
FROM scheduled_posts sp
|
|
||||||
JOIN channels c ON sp.channel_id = c.id
|
|
||||||
ORDER BY sp.scheduled_at ASC
|
|
||||||
");
|
|
||||||
$scheduled_posts = $posts_stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$scheduled_posts = [];
|
|
||||||
$message = 'Error fetching scheduled posts: ' . $e->getMessage();
|
|
||||||
$type = 'danger';
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container-fluid p-4">
|
|
||||||
<h1 class="h3 mb-4 text-white">Scheduler</h1>
|
|
||||||
|
|
||||||
<?php if ($message): ?>
|
|
||||||
<div class="alert alert-<?php echo $type; ?> alert-dismissible fade show" role="alert">
|
|
||||||
<?php echo htmlspecialchars($message); ?>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Schedule Posts Form -->
|
|
||||||
<div class="card bg-dark-surface mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Bulk Schedule Posts</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST" action="scheduler.php">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="channel_id" class="form-label">Channel</label>
|
|
||||||
<select class="form-select" id="channel_id" name="channel_id" required>
|
|
||||||
<option value="" disabled selected>Select a channel</option>
|
|
||||||
<?php foreach ($channels as $channel): ?>
|
|
||||||
<option value="<?php echo $channel['id']; ?>">
|
|
||||||
<?php echo htmlspecialchars($channel['channel_identifier']); ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<label for="schedule_date" class="form-label">Date</label>
|
|
||||||
<input type="date" class="form-control" id="schedule_date" name="schedule_date" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<label for="post_count" class="form-label">Number of Posts</label>
|
|
||||||
<input type="number" class="form-control" id="post_count" name="post_count" min="1" max="100" value="1" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="message" class="form-label">Post Idea / Prompt</label>
|
|
||||||
<textarea class="form-control" id="message" name="message" rows="3" required placeholder="e.g., A motivational quote about success"></textarea>
|
|
||||||
<div class="form-text">This text will be sent to your n8n workflow as a prompt for AI content generation.</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" name="schedule_post" class="btn btn-primary">Schedule Posts</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scheduled Posts Table -->
|
|
||||||
<div class="card bg-dark-surface">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Upcoming Posts</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-dark table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Channel</th>
|
|
||||||
<th>Message</th>
|
|
||||||
<th>Scheduled Time</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($scheduled_posts)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-center text-muted">No posts scheduled yet.</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php
|
|
||||||
$current_date = null;
|
|
||||||
foreach ($scheduled_posts as $post):
|
|
||||||
$post_date = date("F j, Y", strtotime($post['scheduled_at']));
|
|
||||||
if ($post_date !== $current_date):
|
|
||||||
$current_date = $post_date;
|
|
||||||
?>
|
|
||||||
<tr class="table-group-divider">
|
|
||||||
<td colspan="6" class="bg-secondary text-white"><strong><?php echo $current_date; ?></strong></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<tr>
|
|
||||||
<td><?php echo $post['id']; ?></td>
|
|
||||||
<td><?php echo htmlspecialchars($post['channel_identifier']); ?></td>
|
|
||||||
<td><?php echo nl2br(htmlspecialchars(substr($post['message'], 0, 80))) . (strlen($post['message']) > 80 ? '...' : ''); ?></td>
|
|
||||||
<td><?php echo date("g:i a", strtotime($post['scheduled_at'])); ?></td>
|
|
||||||
<td><span class="badge bg-warning text-dark"><?php echo htmlspecialchars(ucfirst($post['status'])); ?></span></td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" action="scheduler.php" onsubmit="return confirm('Are you sure you want to cancel this post?');" style="display: inline;">
|
|
||||||
<input type="hidden" name="action" value="delete_post">
|
|
||||||
<input type="hidden" name="post_id" value="<?php echo $post['id']; ?>">
|
|
||||||
<button type="submit" class="btn btn-danger btn-sm">Cancel</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
require_once 'layout_footer.php';
|
|
||||||
?>
|
|
||||||
147
settings.php
147
settings.php
@ -1,147 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
require_once 'layout_header.php';
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$success_message = '';
|
|
||||||
|
|
||||||
// List of recognized settings keys
|
|
||||||
$allowed_settings = [
|
|
||||||
'webhook_new_post',
|
|
||||||
'webhook_post_sent',
|
|
||||||
'webhook_new_channel',
|
|
||||||
'webhook_token' // Add the token to the allowed list
|
|
||||||
];
|
|
||||||
|
|
||||||
// Handle form submission for saving webhook URLs
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (:key, :value) ON DUPLICATE KEY UPDATE setting_value = :value");
|
|
||||||
|
|
||||||
foreach ($allowed_settings as $key) {
|
|
||||||
if (isset($_POST[$key])) {
|
|
||||||
$stmt->execute([':key' => $key, ':value' => $_POST[$key]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$success_message = "Settings saved successfully!";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle token regeneration
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['regenerate_token'])) {
|
|
||||||
$new_token = bin2hex(random_bytes(16));
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('webhook_token', :token) ON DUPLICATE KEY UPDATE setting_value = :token");
|
|
||||||
$stmt->execute([':token' => $new_token]);
|
|
||||||
$success_message = "New webhook token generated successfully!";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Fetch all settings from the database
|
|
||||||
$stmt = $pdo->query("SELECT * FROM settings");
|
|
||||||
$db_settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
||||||
|
|
||||||
// Merge with defaults to ensure all keys exist
|
|
||||||
$settings = array_merge(
|
|
||||||
array_fill_keys($allowed_settings, ''),
|
|
||||||
$db_settings
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate a token if it doesn't exist
|
|
||||||
if (empty($settings['webhook_token'])) {
|
|
||||||
$settings['webhook_token'] = bin2hex(random_bytes(16));
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('webhook_token', :token) ON DUPLICATE KEY UPDATE setting_value = :token");
|
|
||||||
$stmt->execute([':token' => $settings['webhook_token']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the full webhook URL
|
|
||||||
$scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http";
|
|
||||||
$host = $_SERVER['HTTP_HOST'];
|
|
||||||
$incoming_webhook_url = sprintf('%s://%s/webhook_receiver.php?token=%s', $scheme, $host, $settings['webhook_token']);
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container-fluid p-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h1 class="h3 text-white mb-0">Settings</h1>
|
|
||||||
<a href="webhook_docs.php" class="btn btn-info">View Webhook Documentation</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($success_message): ?>
|
|
||||||
<div class="alert alert-success"><?php echo $success_message; ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="card bg-dark-surface mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Outgoing Webhooks <a href="webhook_docs.php#outgoing" class="small">(See Docs)</a></h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text text-muted mb-4">
|
|
||||||
Configure webhooks to send notifications to external services based on events in the application.
|
|
||||||
Enter the full URL for each endpoint.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="POST">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="webhook_new_post" class="form-label">New Post Scheduled</label>
|
|
||||||
<input type="url" class="form-control" id="webhook_new_post" name="webhook_new_post"
|
|
||||||
placeholder="https://your-service.com/webhook/new-post"
|
|
||||||
value="<?php echo htmlspecialchars($settings['webhook_new_post']); ?>">
|
|
||||||
<div class="form-text">This webhook will be triggered when a new post is added to the scheduler.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="webhook_post_sent" class="form-label">Post Successfully Sent</label>
|
|
||||||
<input type="url" class="form-control" id="webhook_post_sent" name="webhook_post_sent"
|
|
||||||
placeholder="https://your-service.com/webhook/post-sent"
|
|
||||||
value="<?php echo htmlspecialchars($settings['webhook_post_sent']); ?>">
|
|
||||||
<div class="form-text">This webhook will be triggered when a scheduled post is successfully sent. (Not yet implemented)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="webhook_new_channel" class="form-label">New Channel Added</label>
|
|
||||||
<input type="url" class="form-control" id="webhook_new_channel" name="webhook_new_channel"
|
|
||||||
placeholder="https://your-service.com/webhook/new-channel"
|
|
||||||
value="<?php echo htmlspecialchars($settings['webhook_new_channel']); ?>">
|
|
||||||
<div class="form-text">This webhook will be triggered when a new channel is added to the system. (Not yet implemented)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" name="save_settings" class="btn btn-primary">Save Settings</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="card bg-dark-surface">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0">Incoming Webhook URL <a href="webhook_docs.php#incoming" class="small">(See Docs)</a></h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-muted">
|
|
||||||
Use this URL in your external services (like n8n) to send data back to this application.
|
|
||||||
</p>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input type="text" id="webhook-url-input" class="form-control" value="<?php echo htmlspecialchars($incoming_webhook_url); ?>" readonly>
|
|
||||||
<button class="btn btn-secondary" type="button" onclick="copyToClipboard()">Copy</button>
|
|
||||||
</div>
|
|
||||||
<form method="POST" class="d-inline">
|
|
||||||
<button type="submit" name="regenerate_token" class="btn btn-warning">Regenerate Token</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function copyToClipboard() {
|
|
||||||
var copyText = document.getElementById("webhook-url-input");
|
|
||||||
copyText.select();
|
|
||||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
|
||||||
document.execCommand("copy");
|
|
||||||
alert("Copied the URL: " + copyText.value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
require_once 'layout_footer.php';
|
|
||||||
?>
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
require_once 'layout_header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h1 class="card-title">Webhook Documentation</h1>
|
|
||||||
<p class="card-subtitle mb-2 text-muted">Everything you need to integrate with external services like n8n.</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Webhooks are a powerful way for your application to send and receive information from other services in real-time. Here’s how they work in your CRM.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h2 class="mt-4">Outgoing Webhooks</h2>
|
|
||||||
<p>Your application sends these webhooks when certain events happen. You can set the destination URL in the <a href="settings.php">Settings</a> page.</p>
|
|
||||||
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<strong>Event: New Post Scheduled</strong>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>This webhook is sent every time you schedule a new post in the <a href="scheduler.php">Scheduler</a>. It tells your external service (e.g., n8n) that a new post needs to be generated and published.</p>
|
|
||||||
<p><strong>Method:</strong> <code>POST</code></p>
|
|
||||||
<p><strong>Data Format:</strong> <code>application/json</code></p>
|
|
||||||
<h6>Example Payload:</h6>
|
|
||||||
<pre class="code-block-dark p-3 rounded"><code>{
|
|
||||||
"event": "new_post_scheduled",
|
|
||||||
"channel_identifier": "@my_telegram_channel",
|
|
||||||
"post_text_idea": "A post about morning coffee",
|
|
||||||
"scheduled_at_utc": "2025-10-28 14:30:00",
|
|
||||||
"post_id": 123
|
|
||||||
}</code></pre>
|
|
||||||
<h6>Field Descriptions:</h6>
|
|
||||||
<ul>
|
|
||||||
<li><code>event</code>: The name of the event (always "new_post_scheduled").</li>
|
|
||||||
<li><code>channel_identifier</code>: The public URL or @name of the target channel (e.g., "@my_channel" or "t.me/joinchat/...").</li>
|
|
||||||
<li><code>post_text_idea</code>: The initial text or idea you entered in the scheduler. Your n8n workflow should use this as a prompt for AI text generation.</li>
|
|
||||||
<li><code>scheduled_at_utc</code>: The planned publication time in UTC.</li>
|
|
||||||
<li><code>post_id</code>: The unique ID of the post in this CRM, which can be used for status updates.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-5">
|
|
||||||
|
|
||||||
<h2 class="mt-4">Incoming Webhooks</h2>
|
|
||||||
<p>Your application can also receive webhooks from other services. This is how your n8n workflow reports back on the result of a publication.</p>
|
|
||||||
<p>The unique and secure URL for incoming webhooks is available on the <a href="settings.php">Settings</a> page.</p>
|
|
||||||
|
|
||||||
<div class="alert alert-warning" role="alert">
|
|
||||||
<strong>Important:</strong> The payload for incoming webhooks is different from the outgoing ones. You must send a status update, not the original event data.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<strong>Purpose: Report Publication Status</strong>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>After your n8n workflow attempts to publish a post, it **must** send a webhook back to your CRM to report the outcome. This allows the system to update the post's status from "pending" to "published" or "failed".</p>
|
|
||||||
<p><strong>Method:</strong> <code>POST</code></p>
|
|
||||||
<p><strong>Data Format:</strong> <code>application/json</code></p>
|
|
||||||
|
|
||||||
<h6 class="mt-4">Required Fields:</h6>
|
|
||||||
<ul>
|
|
||||||
<li><code>post_id</code> (integer): The ID of the post this update refers to. <strong>This is mandatory.</strong></li>
|
|
||||||
<li><code>status</code> (string): The outcome. Use <code>published</code> for success or <code>failed</code> for an error.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h6 class="mt-4">Example Payload (Success):</h6>
|
|
||||||
<p>This is the minimal payload you need to send for a successful publication.</p>
|
|
||||||
<pre class="code-block-dark p-3 rounded"><code>{
|
|
||||||
"post_id": 123,
|
|
||||||
"status": "published"
|
|
||||||
}</code></pre>
|
|
||||||
|
|
||||||
<h6 class="mt-4">Example Payload (Error):</h6>
|
|
||||||
<p>If something goes wrong, send this payload. You can include an optional <code>details</code> field for error messages.</p>
|
|
||||||
<pre class="code-block-dark p-3 rounded"><code>{
|
|
||||||
"post_id": 123,
|
|
||||||
"status": "failed",
|
|
||||||
"details": "Telegram API error: Chat not found."
|
|
||||||
}</code></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once 'layout_footer.php'; ?>
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
// --- Security Check ---
|
|
||||||
$provided_token = $_GET['token'] ?? null;
|
|
||||||
if (!$provided_token) {
|
|
||||||
http_response_code(401);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Security token not provided.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = 'webhook_token'");
|
|
||||||
$stmt->execute();
|
|
||||||
$correct_token = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if (!$correct_token || !hash_equals($correct_token, $provided_token)) {
|
|
||||||
http_response_code(403);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid security token.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Payload Processing ---
|
|
||||||
$raw_payload = file_get_contents('php://input');
|
|
||||||
$payload = json_decode($raw_payload, true);
|
|
||||||
|
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid JSON payload.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Data Validation ---
|
|
||||||
$post_id = $payload['post_id'] ?? null;
|
|
||||||
$status = $payload['status'] ?? null; // e.g., 'success', 'error', 'published', 'failed'
|
|
||||||
$message = $payload['message'] ?? 'No message provided.';
|
|
||||||
|
|
||||||
if (!$post_id || !$status) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Required fields \'post_id\' and \'status\' are missing.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Database Update ---
|
|
||||||
try {
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
// 1. Log the incoming event
|
|
||||||
$log_stmt = $pdo->prepare(
|
|
||||||
"INSERT INTO webhook_events (post_id, status, message, raw_payload) VALUES (?, ?, ?, ?)"
|
|
||||||
);
|
|
||||||
$log_stmt->execute([$post_id, $status, $message, $raw_payload]);
|
|
||||||
|
|
||||||
// 2. Update the status of the scheduled post
|
|
||||||
// We map various possible statuses to a simplified set for our internal state.
|
|
||||||
$internal_status = 'pending';
|
|
||||||
if (in_array($status, ['success', 'published'])) {
|
|
||||||
$internal_status = 'published';
|
|
||||||
} elseif (in_array($status, ['error', 'failed'])) {
|
|
||||||
$internal_status = 'failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($internal_status !== 'pending') {
|
|
||||||
$update_stmt = $pdo->prepare("UPDATE scheduled_posts SET status = ? WHERE id = ?");
|
|
||||||
$update_stmt->execute([$internal_status, $post_id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
|
|
||||||
echo json_encode(['status' => 'success', 'message' => 'Webhook processed and status updated.']);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if ($pdo->inTransaction()) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
}
|
|
||||||
http_response_code(500);
|
|
||||||
// In a real app, you would log this error to a file instead of echoing it.
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user