-
-
Telegram Channel ID or URL
+
+ value="" required>
Provide the public channel username (e.g., @durov) or full URL.
diff --git a/analytics.php b/analytics.php
new file mode 100644
index 0000000..1029f0e
--- /dev/null
+++ b/analytics.php
@@ -0,0 +1,129 @@
+ 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
+}
+
+?>
+
+
+
Analytics
+
This page provides an overview of your post scheduling and publishing activity, based on status updates received from your n8n workflows.
+
+
+
+
+
+
+
Total Scheduled
+
+
+
+
+
+
+
+
Successfully Published
+
+
+
+
+
+
+
+
Failed to Publish
+
+
+
+
+
+
+
+
Pending / In Progress
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Post ID |
+ Status |
+ Message |
+ Received At |
+
+
+
+
+
+ | No recent webhook events found. |
+
+
+
+
+ | # |
+
+
+
+ |
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/css/custom.css b/assets/css/custom.css
index 3fe7005..2b58c78 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -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);
+}
\ No newline at end of file
diff --git a/channels.php b/channels.php
new file mode 100644
index 0000000..8d2194c
--- /dev/null
+++ b/channels.php
@@ -0,0 +1,94 @@
+ 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 = [];
+}
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No channels found. Get started by adding one.
+
+
+
+
+
+
+ | ID |
+ Channel ID/URL |
+ Added On |
+ Actions |
+
+
+
+
+
+ |
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.php b/index.php
index 62cfb8d..e386754 100644
--- a/index.php
+++ b/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());
diff --git a/layout_footer.php b/layout_footer.php
index ac7605e..7efbefd 100644
--- a/layout_footer.php
+++ b/layout_footer.php
@@ -4,6 +4,7 @@
+
+
+
diff --git a/webhook_docs.php b/webhook_docs.php
new file mode 100644
index 0000000..d1f7262
--- /dev/null
+++ b/webhook_docs.php
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
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.
+
+
+
+
Outgoing Webhooks
+
Your application sends these webhooks when certain events happen. You can set the destination URL in the Settings page.
+
+
+
+
+
This webhook is sent every time you schedule a new post in the Scheduler. It tells your external service (e.g., n8n) that a new post needs to be generated and published.
+
Method: POST
+
Data Format: application/json
+
Example 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
+}
+
Field Descriptions:
+
+ event: The name of the event (always "new_post_scheduled").
+ channel_identifier: The public URL or @name of the target channel (e.g., "@my_channel" or "t.me/joinchat/...").
+ post_text_idea: The initial text or idea you entered in the scheduler. Your n8n workflow should use this as a prompt for AI text generation.
+ scheduled_at_utc: The planned publication time in UTC.
+ post_id: The unique ID of the post in this CRM, which can be used for status updates.
+
+
+
+
+
+
+
Incoming Webhooks
+
Your application can also receive webhooks from other services. This is how your n8n workflow reports back on the result of a publication.
+
The unique and secure URL for incoming webhooks is available on the Settings page.
+
+
+ Important: The payload for incoming webhooks is different from the outgoing ones. You must send a status update, not the original event data.
+
+
+
+
+
+
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".
+
Method: POST
+
Data Format: application/json
+
+
Required Fields:
+
+ post_id (integer): The ID of the post this update refers to. This is mandatory.
+ status (string): The outcome. Use published for success or failed for an error.
+
+
+
Example Payload (Success):
+
This is the minimal payload you need to send for a successful publication.
+
{
+ "post_id": 123,
+ "status": "published"
+}
+
+
Example Payload (Error):
+
If something goes wrong, send this payload. You can include an optional details field for error messages.
+
{
+ "post_id": 123,
+ "status": "failed",
+ "details": "Telegram API error: Chat not found."
+}
+
+
+
+
+
+
+
+
diff --git a/webhook_receiver.php b/webhook_receiver.php
new file mode 100644
index 0000000..cd90f6c
--- /dev/null
+++ b/webhook_receiver.php
@@ -0,0 +1,82 @@
+ '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()]);
+}
+