Skip to content

Commit

Permalink
Initial publication.
Browse files Browse the repository at this point in the history
  • Loading branch information
andreas-hartmann committed Jul 24, 2024
0 parents commit 1455f6f
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 0 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
111 changes: 111 additions & 0 deletions includes/class-admin-settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
class Puck_Admin_Settings {
public function __construct() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'settings_init'));
}

// Add options page
public function add_admin_menu() {
add_options_page('Webhook Consumer', 'Webhook Consumer', 'manage_options', 'webhook_consumer', array($this, 'options_page'));
}

// Register settings
public function settings_init() {
register_setting('webhook_consumer_plugin', 'webhook_settings');

add_settings_section(
'webhook_consumer_section',
__('Webhook Consumer Settings', 'webhook_consumer'),
array($this, 'settings_section_callback'),
'webhook_consumer_plugin'
);

add_settings_field(
'webhook_list',
__('Webhooks', 'webhook_consumer'),
array($this, 'webhook_list_render'),
'webhook_consumer_plugin',
'webhook_consumer_section'
);
}

// Render the webhooks list
public function webhook_list_render() {
$webhooks = get_option('webhook_settings', array());
?>
<div id="webhook_list">
<?php foreach ($webhooks as $index => $webhook): ?>
<div class="webhook_item" id="webhook_item_<?php echo esc_attr($index); ?>">
<input type="text" name="webhook_settings[<?php echo esc_attr($index); ?>][name]" value="<?php echo esc_attr($webhook['name']); ?>" placeholder="Webhook Name">
<input type="text" name="webhook_settings[<?php echo esc_attr($index); ?>][api_key]" value="<?php echo esc_attr($webhook['api_key']); ?>" placeholder="API Key">
<button type="button" onclick="generateApiKey(<?php echo esc_attr($index); ?>)">Generate API Key</button>
<button type="button" onclick="removeWebhook(<?php echo esc_attr($index); ?>)">Remove</button>
</div>
<?php endforeach; ?>
</div>
<button type="button" onclick="addWebhook()">Add Webhook</button>
<script type="text/javascript">
function generateApiKey(index) {
document.querySelector(`[name="webhook_settings[${index}][api_key]"]`).value = '<?php echo wp_generate_password(32, false); ?>';
}
function addWebhook() {
let index = document.querySelectorAll('.webhook_item').length;
let div = document.createElement('div');
div.className = 'webhook_item';
div.id = 'webhook_item_' + index;
div.innerHTML = `
<input type="text" name="webhook_settings[${index}][name]" placeholder="Webhook Name">
<input type="text" name="webhook_settings[${index}][api_key]" placeholder="API Key">
<button type="button" onclick="generateApiKey(${index})">Generate API Key</button>
<button type="button" onclick="removeWebhook(${index})">Remove</button>
`;
document.getElementById('webhook_list').appendChild(div);
}
function removeWebhook(index) {
let div = document.getElementById('webhook_item_' + index);
div.remove();
}
</script>
<?php
}

// Section callback
public function settings_section_callback() {
echo __('Manage your webhooks and API keys here.', 'webhook_consumer');
}

// Options page output
public function options_page() {
?>
<form action='options.php' method='post'>
<h2>Webhook Consumer</h2>
<?php
settings_fields('webhook_consumer_plugin');
do_settings_sections('webhook_consumer_plugin');
submit_button();
?>

<h3>How to Use</h3>
<p>The following shortcodes and curl examples will help you utilize the webhooks:</p>

<h4>Shortcodes</h4>
<p>Use the following shortcode to create a subscription form for a specific webhook. Replace <code>[webhook_name]</code> with the name of the webhook.</p>
<pre><code>[puck_subscribe webhook="[webhook_name]"]</code></pre>

<h4>cURL Example</h4>
<p>You can use curl to send a POST request to the webhook URL. Replace <code>[webhook_name]</code> with the name of the webhook and <code>[api_key]</code> with the corresponding API key:</p>
<pre><code>
curl -X POST <?php echo esc_url(home_url('/wp-json/webhook/v1/receive/[webhook_name]/')); ?> \
-H "Content-Type: application/json" \
-d '{
"title": "Test Post",
"content": "This is the content of the post.",
"api_key": "[api_key]"
}'
</code></pre>
</form>
<?php
}
}
?>
32 changes: 32 additions & 0 deletions includes/class-utilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
class Puck_Utilities {
public static function activate() {
self::create_custom_tables();
puck_register_webhook_post_type();
flush_rewrite_rules();
}

public static function deactivate() {
flush_rewrite_rules();
}

public static function create_custom_tables() {
global $wpdb;

$table_name = $wpdb->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);
}
}
?>
77 changes: 77 additions & 0 deletions includes/class-webhook-handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
class Puck_Webhook_Handler {
public function __construct() {
add_action('rest_api_init', array($this, 'register_rest_routes'));
}

// Register public REST routes
public function register_rest_routes() {
register_rest_route('webhook/v1', '/receive/(?P<webhook>[\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);
}
}
?>
107 changes: 107 additions & 0 deletions puck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
/*
Plugin Name: Puck
Description: A WordPress plugin for automating news feeds and mailing lists.
Version: 0.5.1
Author: Andreas Hartmann
Author URI: https://ohok.org/
*/

if (!defined('ABSPATH')) {
exit; // Exit if accessed directly.
}

// Load required modules.
define('PUCK_PLUGIN_DIR', plugin_dir_path(__FILE__));
require_once PUCK_PLUGIN_DIR . 'includes/class-admin-settings.php';
require_once PUCK_PLUGIN_DIR . 'includes/class-webhook-handler.php';
require_once PUCK_PLUGIN_DIR . 'includes/class-utilities.php';

// Register Webhook Post Type Function
function puck_register_webhook_post_type() {
register_post_type('webhook_post', array(
'labels' => 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();
?>
<form method="post">
<input type="email" name="email" required>
<button type="submit">Subscribe</button>
</form>
<?php
return ob_get_clean();
}
?>

0 comments on commit 1455f6f

Please sign in to comment.