diff --git a/add_channel.php b/add_channel.php index 7113153..7cde419 100644 --- a/add_channel.php +++ b/add_channel.php @@ -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';
- - 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
+

+
+
+
+
+ + +
+
+
Recent Activity (from Webhooks)
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Post IDStatusMessageReceived 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 = []; +} + +?> + +
+
+

Channels

+ + + Add Channel + +
+ + + + + +
+
+ +
+

No channels found. Get started by adding one.

+
+ +
+ + + + + + + + + + + + + + + + + + + +
IDChannel ID/URLAdded OnActions
+ + + + + +
+
+ +
+
+
+ + \ 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 @@ + + +
+
+
+

Webhook Documentation

+

Everything you need to integrate with external services like n8n.

+
+
+

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.

+ +
+
+ Event: New Post Scheduled +
+
+

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.

+ + + +
+
+ Purpose: Report Publication Status +
+
+

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()]); +} +