-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1455f6f
Showing
5 changed files
with
382 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
?> |