Compare commits

..

2 Commits

Author SHA1 Message Date
Flatlogic Bot
d2f005da56 nu norm 2025-10-27 13:55:31 +00:00
Flatlogic Bot
74e14f325c wtf 2025-10-27 12:24:10 +00:00
13 changed files with 1293 additions and 146 deletions

82
add_channel.php Normal file
View File

@ -0,0 +1,82 @@
<?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 Normal file
View File

@ -0,0 +1,129 @@
<?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';
?>

141
assets/css/custom.css Normal file
View File

@ -0,0 +1,141 @@
: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);
}

3
assets/js/main.js Normal file
View File

@ -0,0 +1,3 @@
// Empty for now, can be used for more complex interactions later.
console.log("main.js loaded");

94
channels.php Normal file
View File

@ -0,0 +1,94 @@
<?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
View File

@ -1,150 +1,58 @@
<?php <?php
declare(strict_types=1); require_once __DIR__ . '/db/config.php';
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; // Simple migration runner
$now = date('Y-m-d H:i:s'); try {
$pdo = db();
// Main Tables
$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);");
$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);");
$pdo->exec("CREATE TABLE IF NOT EXISTS settings (setting_key VARCHAR(255) PRIMARY KEY, setting_value TEXT);");
$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));");
// Simple migration to rename telegram_id to channel_identifier for backwards compatibility
$checkColumn = $pdo->query("SHOW COLUMNS FROM `channels` LIKE 'telegram_id'");
if ($checkColumn->rowCount() > 0) {
$pdo->exec("ALTER TABLE `channels` CHANGE `telegram_id` `channel_identifier` VARCHAR(255) NOT NULL UNIQUE;");
}
} catch (PDOException $e) {
// In a real app, log this error. For now, we die.
die("Database migration failed: " . $e->getMessage());
}
// Fetch channel count
$stmt = $pdo->query("SELECT COUNT(*) FROM channels");
$channel_count = $stmt->fetchColumn();
require_once __DIR__ . '/layout_header.php';
?> ?>
<!doctype html>
<html lang="en"> <div class="container-fluid">
<head> <h1 class="h2 mb-4">Dashboard</h1>
<meta charset="utf-8" /> <div class="row">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <div class="col-md-4">
<title>New Style</title> <div class="card text-white">
<?php <div class="card-body">
// Read project preview data from environment <div class="d-flex justify-content-between align-items-center">
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; <div>
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; <h5 class="card-title">Total Channels</h5>
?> <p class="card-text fs-2 fw-bold"><?php echo $channel_count; ?></p>
<?php if ($projectDescription): ?> </div>
<!-- Meta description --> <div class="fs-1 text-primary">
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> <i data-feather="tv"></i>
<!-- Open Graph meta tags --> </div>
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" /> </div>
<!-- Twitter meta tags --> <a href="add_channel.php" class="btn btn-primary mt-3">
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" /> <i data-feather="plus" class="me-1" style="width:16px; height:16px;"></i>
<?php endif; ?> Add Channel
<?php if ($projectImageUrl): ?> </a>
<!-- Open Graph image --> </div>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" /> </div>
<!-- Twitter image --> </div>
<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 {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
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>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
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>
</main> </div>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <?php
</footer> require_once __DIR__ . '/layout_footer.php';
</body> ?>
</html>

20
layout_footer.php Normal file
View File

@ -0,0 +1,20 @@
</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>

70
layout_header.php Normal file
View File

@ -0,0 +1,70 @@
<?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 Normal file
View File

@ -0,0 +1,135 @@
[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 Normal file
View File

@ -0,0 +1,243 @@
<?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 Normal file
View File

@ -0,0 +1,147 @@
<?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';
?>

93
webhook_docs.php Normal file
View File

@ -0,0 +1,93 @@
<?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. Heres 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'; ?>

82
webhook_receiver.php Normal file
View File

@ -0,0 +1,82 @@
<?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()]);
}