diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ef5ed4 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Puck — A WordPress plugin for automating news feeds and mailing lists + +Puck is a WordPress plugin to generate news feeds from a webhook as well as notify subscribed users via email. It supports any number of feeds and subscribers and makes opting in and out of notifications easy for the user. + +## Features + +- **Multiple Webhooks**: Create and manage multiple feeds with different webhooks and API keys. Trigger the webhook with the content of a news item to generate an article. +- **Shortcode for subscribing**: Use the included shortcode to display a sign up form for users to subscribe to your feed. +- **Email Notifications**: Notify subscribers via email when a news item is created. +- **Unsubscribe Links**: Subscribers can opt out of further notifications with the included opt out link. + +## Project status + +This is currently an MVP - it does the minimum of what I wanted it to do for my use case, but it's rough around the edges. + +## Installation + +1. **Clone the Repository**: + ```sh + git clone https://github.com/andreas-hartmann/puck.git + ``` +2. **Upload to WordPress**: + Upload the `puck` directory to the `/wp-content/plugins/` directory of your WordPress installation. + +3. **Activate the Plugin**: + Navigate to the WordPress admin panel, go to Plugins, and activate the Puck plugin. + +## Usage + +### Admin Settings +Navigate to **Settings > Webhook Consumer** to manage your webhooks: + +- **Add a Webhook**: Click "Add Webhook", enter a name, and generate an API key. Don't forget to save. +- **Remove a Webhook**: Click the "Remove" button next to a webhook to delete it. + +### Shortcodes +Use the following shortcode to create a subscription form for a specific webhook: +```shortcode +[puck_subscribe webhook="example_webhook"] +``` +Replace `example_webhook` with the name of your webhook. + +### Webhook Call +To trigger a webhook, use the following example cURL command: +```sh +curl -X POST https://your-site.com/wp-json/webhook/v1/receive/your_webhook_name/ \ +-H "Content-Type: application/json" \ +-d '{ + "title": "Test Post", + "content": "This is the content of the post.", + "api_key": "your_api_key" +}' +``` +Replace `your_site.com` with your website URL, `your_webhook_name` with the webhook name, and `your_api_key` with the corresponding API key. +USE HTTPS! The API key is secret and could be abused if leaked. diff --git a/includes/class-admin-settings.php b/includes/class-admin-settings.php new file mode 100644 index 0000000..1bf8f23 --- /dev/null +++ b/includes/class-admin-settings.php @@ -0,0 +1,111 @@ + +
+ $webhook): ?> +
+ + + + +
+ +
+ + + +
+

Webhook Consumer

+ + +

How to Use

+

The following shortcodes and curl examples will help you utilize the webhooks:

+ +

Shortcodes

+

Use the following shortcode to create a subscription form for a specific webhook. Replace [webhook_name] with the name of the webhook.

+
[puck_subscribe webhook="[webhook_name]"]
+ +

cURL Example

+

You can use curl to send a POST request to the webhook URL. Replace [webhook_name] with the name of the webhook and [api_key] with the corresponding API key:

+

+curl -X POST  \
+-H "Content-Type: application/json" \
+-d '{
+  "title": "Test Post",
+  "content": "This is the content of the post.",
+  "api_key": "[api_key]"
+}'
+            
+
+ diff --git a/includes/class-utilities.php b/includes/class-utilities.php new file mode 100644 index 0000000..ee9386b --- /dev/null +++ b/includes/class-utilities.php @@ -0,0 +1,32 @@ +prefix . 'custom_emails'; + + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + email varchar(100) NOT NULL, + webhook_name varchar(100) NOT NULL, + disabled tinyint(1) DEFAULT 0 NOT NULL, + PRIMARY KEY (id) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + } +} +?> diff --git a/includes/class-webhook-handler.php b/includes/class-webhook-handler.php new file mode 100644 index 0000000..35f30a7 --- /dev/null +++ b/includes/class-webhook-handler.php @@ -0,0 +1,77 @@ +[\w-]+)/', array( + 'methods' => 'POST', + 'callback' => array($this, 'handle_webhook'), + 'permission_callback' => '__return_true', + )); + } + + public function handle_webhook($request) { + $webhook_name = $request->get_param('webhook'); + $body = $request->get_json_params(); + + error_log('Webhook: ' . esc_html($webhook_name)); + + $webhooks = get_option('webhook_settings', array()); + $api_key_defined = null; + foreach ($webhooks as $webhook) { + if ($webhook['name'] === $webhook_name) { + $api_key_defined = $webhook['api_key']; + error_log('Expected API Key: ' . esc_html($api_key_defined)); + break; + } + } + + $api_key_sent = isset($body['api_key']) ? sanitize_text_field($body['api_key']) : ''; + + if (!$api_key_defined || $api_key_sent !== $api_key_defined) { + error_log('API key validation failed.'); + return new WP_Error('authentication_failed', 'API key not valid', array('status' => 403)); + } + + $title = sanitize_text_field($body['title']); + $content = sanitize_textarea_field($body['content']); + + $post_id = wp_insert_post(array( + 'post_title' => $title, + 'post_content' => $content, + 'post_status' => 'publish', + 'post_type' => 'webhook_post' + )); + + if (is_wp_error($post_id)) { + return new WP_Error('post_creation_failed', 'Post creation failed', array('status' => 500)); + } + + // Fetch email addresses for the specific webhook + global $wpdb; + $table_name = $wpdb->prefix . 'custom_emails'; + $emails = $wpdb->get_col($wpdb->prepare("SELECT email FROM $table_name WHERE webhook_name = %s AND disabled <> 1", $webhook_name)); + + // Send email + $subject = "New Post Created: " . esc_html($title); + $message = "A new post has been created with the following content:\n\n"; + $message .= esc_html($content) . "\n\n"; + $unsubscribe_url = add_query_arg(array( + 'action' => 'unsubscribe', + 'email' => '__EMAIL__', + 'webhook' => esc_html($webhook_name) + ), home_url()); + $message .= "View the post: " . esc_url(get_permalink($post_id)) . "\n"; + $message .= "Unsubscribe: " . esc_url($unsubscribe_url); + + foreach ($emails as $to) { + wp_mail($to, $subject, str_replace('__EMAIL__', esc_html($to), $message)); + } + + return new WP_REST_Response('Webhook processed', 200); + } +} +?> diff --git a/puck.php b/puck.php new file mode 100644 index 0000000..84e3730 --- /dev/null +++ b/puck.php @@ -0,0 +1,107 @@ + array( + 'name' => __('Webhook Posts'), + 'singular_name' => __('Webhook Post'), + ), + 'public' => true, + 'publicly_queryable' => true, + 'rewrite' => array('slug' => 'webhook_post'), + 'show_ui' => true, + 'has_archive' => false, + 'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments'), + )); +} + +add_action('init', 'puck_register_webhook_post_type'); + +// Initialize Admin Settings +new Puck_Admin_Settings(); + +// Initialize Webhook Handler +new Puck_Webhook_Handler(); + +// Activation and Deactivation Hooks +register_activation_hook(__FILE__, array('Puck_Utilities', 'activate')); +register_deactivation_hook(__FILE__, array('Puck_Utilities', 'deactivate')); + +// Register Shortcode for Hooks Subscription +function register_hooks_subscribe_shortcode() { + add_shortcode('puck_subscribe', 'handle_hooks_subscribe_shortcode'); +} +add_action('init', 'register_hooks_subscribe_shortcode'); + +// Register Unsubscribe Query Vars +function register_unsubscribe_query_vars($vars) { + $vars[] = 'action'; + $vars[] = 'email'; + $vars[] = 'webhook'; + return $vars; +} +add_filter('query_vars', 'register_unsubscribe_query_vars'); + +// Handle Unsubscribe Action +function handle_unsubscribe_action() { + if (get_query_var('action') == 'unsubscribe' && get_query_var('email') && get_query_var('webhook')) { + $email = sanitize_email(get_query_var('email')); + $webhook = sanitize_text_field(get_query_var('webhook')); + + global $wpdb; + $table_name = $wpdb->prefix . 'custom_emails'; + $wpdb->update($table_name, array('disabled' => 1), array('email' => $email, 'webhook_name' => $webhook)); + + wp_redirect(home_url('/unsubscribe-success')); + exit; + } +} +add_action('template_redirect', 'handle_unsubscribe_action'); + +// User sign up shortcode handler +function handle_hooks_subscribe_shortcode($atts) { + $webhook = isset($atts['webhook']) ? sanitize_text_field($atts['webhook']) : ''; + + if (isset($_POST['email']) && is_email($_POST['email'])) { + $email = sanitize_email($_POST['email']); + global $wpdb; + $table_name = $wpdb->prefix . 'custom_emails'; + $existing = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE email = %s AND webhook_name = %s", $email, $webhook)); + + if ($existing) { + $wpdb->update($table_name, array('disabled' => 0), array('email' => $email, 'webhook_name' => $webhook)); + } else { + $wpdb->insert($table_name, array('email' => $email, 'webhook_name' => $webhook, 'disabled' => 0)); + } + + echo 'Thank you for subscribing!'; + } + + ob_start(); + ?> +
+ + +
+