nu norm
This commit is contained in:
parent
74e14f325c
commit
d2f005da56
@ -3,37 +3,37 @@
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$error = '';
|
||||
$telegram_id = '';
|
||||
$channel_identifier = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$telegram_id = trim($_POST['telegram_id'] ?? '');
|
||||
$channel_identifier = trim($_POST['channel_identifier'] ?? '');
|
||||
|
||||
if (empty($telegram_id)) {
|
||||
if (empty($channel_identifier)) {
|
||||
$error = 'Telegram Channel ID or URL is required.';
|
||||
} else {
|
||||
// Basic parsing to get ID from URL
|
||||
if (filter_var($telegram_id, FILTER_VALIDATE_URL)) {
|
||||
$path = parse_url($telegram_id, PHP_URL_PATH);
|
||||
$telegram_id = basename($path);
|
||||
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($telegram_id, '@') !== 0 && !is_numeric($telegram_id)) {
|
||||
$telegram_id = '@' . $telegram_id;
|
||||
if (strpos($channel_identifier, '@') !== 0 && !is_numeric($channel_identifier)) {
|
||||
$channel_identifier = '@' . $channel_identifier;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("INSERT INTO channels (telegram_id) VALUES (:telegram_id)");
|
||||
$stmt->execute([':telegram_id' => $telegram_id]);
|
||||
$stmt = $pdo->prepare("INSERT INTO channels (channel_identifier) VALUES (:channel_identifier)");
|
||||
$stmt->execute([':channel_identifier' => $channel_identifier]);
|
||||
|
||||
session_start();
|
||||
$_SESSION['flash_message'] = "Channel '{$telegram_id}' added successfully!";
|
||||
$_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 '{$telegram_id}' already exists.";
|
||||
$error = "Error: Channel '{$channel_identifier}' already exists.";
|
||||
} else {
|
||||
$error = "Database error: " . $e->getMessage();
|
||||
}
|
||||
@ -57,10 +57,10 @@ require_once __DIR__ . '/layout_header.php';
|
||||
|
||||
<form action="add_channel.php" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="telegram_id" class="form-label">Telegram Channel ID or URL</label>
|
||||
<input type="text" class="form-control" id="telegram_id" name="telegram_id"
|
||||
<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($telegram_id); ?>" required>
|
||||
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>
|
||||
|
||||
129
analytics.php
Normal file
129
analytics.php
Normal 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';
|
||||
?>
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--surface-color: #1E1E1E;
|
||||
@ -133,3 +132,10 @@ body {
|
||||
.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);
|
||||
}
|
||||
94
channels.php
Normal file
94
channels.php
Normal 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';
|
||||
?>
|
||||
13
index.php
13
index.php
@ -4,7 +4,18 @@ require_once __DIR__ . '/db/config.php';
|
||||
// Simple migration runner
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS channels (id INT AUTO_INCREMENT PRIMARY KEY, telegram_id VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255), avatar_url VARCHAR(255), description TEXT, subscribers INT, added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);");
|
||||
// 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());
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
<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();
|
||||
|
||||
|
||||
@ -32,37 +32,25 @@ session_start();
|
||||
<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 active" href="index.php">
|
||||
<i data-feather="home"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<i data-feather="tv"></i>
|
||||
<span>Channels</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<i data-feather="calendar"></i>
|
||||
<span>Scheduler</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<i data-feather="bar-chart-2"></i>
|
||||
<span>Analytics</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<i data-feather="settings"></i>
|
||||
<span>Settings</span>
|
||||
<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">
|
||||
|
||||
135
logs/webhook.log
Normal file
135
logs/webhook.log
Normal 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
243
scheduler.php
Normal 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
147
settings.php
Normal 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
93
webhook_docs.php
Normal 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. 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'; ?>
|
||||
82
webhook_receiver.php
Normal file
82
webhook_receiver.php
Normal 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()]);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user