Initial commit: PRAutoBlogger v0.1.0 with frontend posts widget

This commit is contained in:
Terence Ong
2026-04-10 23:33:42 +07:00
parent 68c6f67bcf
commit 88d674d679
65 changed files with 2178 additions and 1084 deletions

View File

@@ -4,12 +4,12 @@ declare(strict_types=1);
/**
* Displays admin notices for onboarding, errors, and budget warnings.
*
* Triggered by: Autoblogger::register_admin_hooks() on `admin_notices`.
* Dependencies: Autoblogger_Cost_Tracker (for budget warnings).
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_notices`.
* Dependencies: PRAutoBlogger_Cost_Tracker (for budget warnings).
*
* @see class-autoblogger.php — Registers the hook.
* @see class-prautoblogger.php — Registers the hook.
*/
class Autoblogger_Admin_Notices {
class PRAutoBlogger_Admin_Notices {
/**
* Display relevant admin notices.
@@ -32,16 +32,16 @@ class Autoblogger_Admin_Notices {
* @return void
*/
private function check_api_key_notice(): void {
$api_key = get_option( 'autoblogger_openrouter_api_key', '' );
$api_key = get_option( 'prautoblogger_openrouter_api_key', '' );
if ( '' !== $api_key ) {
return;
}
printf(
'<div class="notice notice-warning is-dismissible"><p>%s <a href="%s">%s</a></p></div>',
esc_html__( 'AutoBlogger: OpenRouter API key is not configured. Content generation is disabled.', 'autoblogger' ),
esc_url( admin_url( 'admin.php?page=autoblogger-settings' ) ),
esc_html__( 'Configure now', 'autoblogger' )
esc_html__( 'PRAutoBlogger: OpenRouter API key is not configured. Content generation is disabled.', 'prautoblogger' ),
esc_url( admin_url( 'admin.php?page=prautoblogger-settings' ) ),
esc_html__( 'Configure now', 'prautoblogger' )
);
}
@@ -51,20 +51,20 @@ class Autoblogger_Admin_Notices {
* @return void
*/
private function check_budget_notice(): void {
$cost_tracker = new Autoblogger_Cost_Tracker();
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
$utilization = $cost_tracker->get_budget_utilization();
if ( $utilization >= 100.0 ) {
printf(
'<div class="notice notice-error"><p>%s</p></div>',
esc_html__( 'AutoBlogger: Monthly API budget EXCEEDED. Content generation is paused until next month or budget increase.', 'autoblogger' )
esc_html__( 'PRAutoBlogger: Monthly API budget EXCEEDED. Content generation is paused until next month or budget increase.', 'prautoblogger' )
);
} elseif ( $utilization >= 80.0 ) {
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
esc_html( sprintf(
/* translators: %s: budget utilization percentage */
__( 'AutoBlogger: Monthly API budget is at %.0f%%. Consider increasing the budget or reducing daily article count.', 'autoblogger' ),
__( 'PRAutoBlogger: Monthly API budget is at %.0f%%. Consider increasing the budget or reducing daily article count.', 'prautoblogger' ),
$utilization
) )
);
@@ -77,20 +77,20 @@ class Autoblogger_Admin_Notices {
* @return void
*/
private function check_subreddit_notice(): void {
$subreddits = json_decode( get_option( 'autoblogger_target_subreddits', '[]' ), true );
$subreddits = json_decode( get_option( 'prautoblogger_target_subreddits', '[]' ), true );
if ( ! empty( $subreddits ) ) {
return;
}
// Only show on our settings page.
$screen = get_current_screen();
if ( ! $screen || 'toplevel_page_autoblogger-settings' !== $screen->id ) {
if ( ! $screen || 'toplevel_page_prautoblogger-settings' !== $screen->id ) {
return;
}
printf(
'<div class="notice notice-info is-dismissible"><p>%s</p></div>',
esc_html__( 'AutoBlogger: No subreddits configured. Add target subreddits in Source Configuration to start collecting data.', 'autoblogger' )
esc_html__( 'PRAutoBlogger: No subreddits configured. Add target subreddits in Source Configuration to start collecting data.', 'prautoblogger' )
);
}
}

View File

@@ -2,28 +2,28 @@
declare(strict_types=1);
/**
* Registers and renders the main AutoBlogger settings page in wp-admin.
* Registers and renders the main PRAutoBlogger settings page in wp-admin.
*
* Uses the WordPress Settings API for option registration, sanitization, and rendering.
* Settings are defined as a declarative array — adding a new setting is one array entry.
*
* Triggered by: Autoblogger::register_admin_hooks() registers menu and settings hooks.
* Dependencies: Autoblogger_Encryption (for API key storage), Autoblogger_Settings_Fields.
* Triggered by: PRAutoBlogger::register_admin_hooks() registers menu and settings hooks.
* Dependencies: PRAutoBlogger_Encryption (for API key storage), PRAutoBlogger_Settings_Fields.
*
* @see class-autoblogger.php — Registers hooks that call this class.
* @see class-prautoblogger.php — Registers hooks that call this class.
* @see class-settings-fields.php — Defines all settings fields and sections.
* @see CONVENTIONS.md — "How To: Add a New Admin Setting".
*/
class Autoblogger_Admin_Page {
class PRAutoBlogger_Admin_Page {
private const PAGE_SLUG = 'autoblogger-settings';
private const OPTION_GROUP = 'autoblogger_settings_group';
private const PAGE_SLUG = 'prautoblogger-settings';
private const OPTION_GROUP = 'prautoblogger_settings_group';
/** Register the top-level AutoBlogger menu item. */
/** Register the top-level PRAutoBlogger menu item. */
public function on_register_menu(): void {
add_menu_page(
__( 'AutoBlogger', 'autoblogger' ),
__( 'AutoBlogger', 'autoblogger' ),
__( 'PRAutoBlogger', 'prautoblogger' ),
__( 'PRAutoBlogger', 'prautoblogger' ),
'manage_options',
self::PAGE_SLUG,
[ $this, 'render_page' ],
@@ -34,15 +34,15 @@ class Autoblogger_Admin_Page {
/** Register all settings with the WordPress Settings API. */
public function on_register_settings(): void {
$sections = Autoblogger_Settings_Fields::get_sections();
$fields = Autoblogger_Settings_Fields::get_fields();
$sections = PRAutoBlogger_Settings_Fields::get_sections();
$fields = PRAutoBlogger_Settings_Fields::get_fields();
foreach ( $sections as $section_id => $section ) {
add_settings_section( $section_id, $section['title'], '__return_empty_string', self::PAGE_SLUG );
}
foreach ( $fields as $field ) {
$section = $field['section'] ?? 'autoblogger_api';
$section = $field['section'] ?? 'prautoblogger_api';
register_setting( self::OPTION_GROUP, $field['id'], [
'type' => $field['wp_type'] ?? 'string',
'sanitize_callback' => [ $this, 'sanitize_field' ],
@@ -52,30 +52,30 @@ class Autoblogger_Admin_Page {
}
}
/** Enqueue admin CSS and JS on all AutoBlogger admin pages. */
/** Enqueue admin CSS and JS on all PRAutoBlogger admin pages. */
public function on_enqueue_assets( string $hook_suffix ): void {
$pages = [
'toplevel_page_' . self::PAGE_SLUG,
'autoblogger_page_autoblogger-metrics',
'autoblogger_page_autoblogger-review-queue',
'autoblogger_page_autoblogger-logs',
'prautoblogger_page_prautoblogger-metrics',
'prautoblogger_page_prautoblogger-review-queue',
'prautoblogger_page_prautoblogger-logs',
];
if ( ! in_array( $hook_suffix, $pages, true ) ) {
return;
}
wp_enqueue_style( 'autoblogger-admin', AUTOBLOGGER_PLUGIN_URL . 'assets/css/admin.css', [], AUTOBLOGGER_VERSION );
wp_enqueue_script( 'autoblogger-admin', AUTOBLOGGER_PLUGIN_URL . 'assets/js/admin.js', [ 'jquery' ], AUTOBLOGGER_VERSION, true );
wp_enqueue_style( 'prautoblogger-admin', PRAUTOBLOGGER_PLUGIN_URL . 'assets/css/admin.css', [], PRAUTOBLOGGER_VERSION );
wp_enqueue_script( 'prautoblogger-admin', PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/admin.js', [ 'jquery' ], PRAUTOBLOGGER_VERSION, true );
wp_localize_script( 'autoblogger-admin', 'autobloggerAdmin', [
wp_localize_script( 'prautoblogger-admin', 'prautobloggerAdmin', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'generateNonce' => wp_create_nonce( 'autoblogger_generate_now' ),
'testNonce' => wp_create_nonce( 'autoblogger_test_connection' ),
'reviewNonce' => wp_create_nonce( 'autoblogger_review_queue' ),
'generatingText' => __( 'Generating...', 'autoblogger' ),
'generateText' => __( 'Generate Now', 'autoblogger' ),
'testingText' => __( 'Testing...', 'autoblogger' ),
'testText' => __( 'Test Connections', 'autoblogger' ),
'generateNonce' => wp_create_nonce( 'prautoblogger_generate_now' ),
'testNonce' => wp_create_nonce( 'prautoblogger_test_connection' ),
'reviewNonce' => wp_create_nonce( 'prautoblogger_review_queue' ),
'generatingText' => __( 'Generating...', 'prautoblogger' ),
'generateText' => __( 'Generate Now', 'prautoblogger' ),
'testingText' => __( 'Testing...', 'prautoblogger' ),
'testText' => __( 'Test Connections', 'prautoblogger' ),
] );
}
@@ -84,9 +84,9 @@ class Autoblogger_Admin_Page {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$sections = Autoblogger_Settings_Fields::get_sections();
$fields = Autoblogger_Settings_Fields::get_fields();
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/settings-page.php';
$sections = PRAutoBlogger_Settings_Fields::get_sections();
$fields = PRAutoBlogger_Settings_Fields::get_fields();
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/settings-page.php';
}
/**
@@ -102,7 +102,7 @@ class Autoblogger_Admin_Page {
$value = get_option( $args['id'], $default );
if ( 'password' === $type && '' !== $value ) {
$decrypted = Autoblogger_Encryption::decrypt( $value );
$decrypted = PRAutoBlogger_Encryption::decrypt( $value );
$value = '' !== $decrypted ? '••••••••' : '';
}
@@ -120,7 +120,7 @@ class Autoblogger_Admin_Page {
case 'password':
printf( '<input type="password" id="%s" name="%s" value="" class="ab-input" placeholder="%s" autocomplete="off" />',
$id, $id, '' !== $value ? esc_attr__( 'Saved (enter new value to change)', 'autoblogger' ) : '' );
$id, $id, '' !== $value ? esc_attr__( 'Saved (enter new value to change)', 'prautoblogger' ) : '' );
break;
case 'textarea':
@@ -152,11 +152,11 @@ class Autoblogger_Admin_Page {
break;
case 'author_select':
wp_dropdown_users( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto (first admin) —', 'autoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select' ] );
wp_dropdown_users( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto (first admin) —', 'prautoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select' ] );
break;
case 'category_select':
wp_dropdown_categories( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto-assign by type —', 'autoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select', 'hide_empty' => false ] );
wp_dropdown_categories( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto-assign by type —', 'prautoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select', 'hide_empty' => false ] );
break;
}
@@ -181,13 +181,13 @@ class Autoblogger_Admin_Page {
$option_name = substr( $filter, strlen( 'sanitize_option_' ) );
}
$encrypted = [ 'autoblogger_openrouter_api_key', 'autoblogger_reddit_client_secret', 'autoblogger_ga4_credentials_json' ];
$encrypted = [ 'prautoblogger_openrouter_api_key', 'prautoblogger_reddit_client_secret', 'prautoblogger_ga4_credentials_json' ];
if ( in_array( $option_name, $encrypted, true ) ) {
if ( '' === $value ) { return get_option( $option_name, '' ); }
return Autoblogger_Encryption::encrypt( sanitize_text_field( $value ) );
return PRAutoBlogger_Encryption::encrypt( sanitize_text_field( $value ) );
}
$json_fields = [ 'autoblogger_target_subreddits', 'autoblogger_topic_exclusions', 'autoblogger_enabled_sources' ];
$json_fields = [ 'prautoblogger_target_subreddits', 'prautoblogger_topic_exclusions', 'prautoblogger_enabled_sources' ];
if ( in_array( $option_name, $json_fields, true ) ) {
if ( is_array( $value ) ) { return wp_json_encode( array_values( array_map( 'sanitize_text_field', $value ) ) ); }
if ( is_string( $value ) && '[' !== substr( trim( $value ), 0, 1 ) ) {
@@ -197,7 +197,7 @@ class Autoblogger_Admin_Page {
return sanitize_text_field( $value );
}
$numeric = [ 'autoblogger_daily_article_target', 'autoblogger_monthly_budget_usd', 'autoblogger_min_word_count', 'autoblogger_max_word_count', 'autoblogger_default_author', 'autoblogger_default_category' ];
$numeric = [ 'prautoblogger_daily_article_target', 'prautoblogger_monthly_budget_usd', 'prautoblogger_min_word_count', 'prautoblogger_max_word_count', 'prautoblogger_default_author', 'prautoblogger_default_category' ];
if ( in_array( $option_name, $numeric, true ) ) {
return is_numeric( $value ) ? $value : 0;
}

View File

@@ -2,17 +2,17 @@
declare(strict_types=1);
/**
* WordPress Dashboard widget showing AutoBlogger generation status at a glance.
* WordPress Dashboard widget showing PRAutoBlogger generation status at a glance.
*
* Displays: last generation time, articles generated today, monthly cost,
* and budget utilization.
*
* Triggered by: Could be registered via wp_dashboard_setup hook.
* Dependencies: Autoblogger_Cost_Tracker.
* Dependencies: PRAutoBlogger_Cost_Tracker.
*
* @see class-autoblogger.php — Can register this on wp_dashboard_setup.
* @see class-prautoblogger.php — Can register this on wp_dashboard_setup.
*/
class Autoblogger_Dashboard_Widget {
class PRAutoBlogger_Dashboard_Widget {
/**
* Register the dashboard widget.
@@ -25,8 +25,8 @@ class Autoblogger_Dashboard_Widget {
}
wp_add_dashboard_widget(
'autoblogger_status',
__( 'AutoBlogger Status', 'autoblogger' ),
'prautoblogger_status',
__( 'PRAutoBlogger Status', 'prautoblogger' ),
[ $this, 'render_widget' ]
);
}
@@ -37,15 +37,15 @@ class Autoblogger_Dashboard_Widget {
* @return void
*/
public function render_widget(): void {
$cost_tracker = new Autoblogger_Cost_Tracker();
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
$monthly_spend = $cost_tracker->get_monthly_spend();
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
$utilization = $cost_tracker->get_budget_utilization();
$next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
$next_run = wp_next_scheduled( 'prautoblogger_daily_generation' );
printf( '<p><strong>%s</strong> $%.2f / $%.2f (%.0f%%)</p>',
esc_html__( 'Monthly Spend:', 'autoblogger' ),
esc_html__( 'Monthly Spend:', 'prautoblogger' ),
$monthly_spend,
$budget,
$utilization
@@ -53,15 +53,15 @@ class Autoblogger_Dashboard_Widget {
if ( false !== $next_run ) {
printf( '<p><strong>%s</strong> %s</p>',
esc_html__( 'Next Generation:', 'autoblogger' ),
esc_html__( 'Next Generation:', 'prautoblogger' ),
esc_html( wp_date( 'M j, Y g:i A', $next_run ) )
);
}
printf(
'<p><a href="%s" class="button button-secondary">%s</a></p>',
esc_url( admin_url( 'admin.php?page=autoblogger-metrics' ) ),
esc_html__( 'View Full Metrics', 'autoblogger' )
esc_url( admin_url( 'admin.php?page=prautoblogger-metrics' ) ),
esc_html__( 'View Full Metrics', 'prautoblogger' )
);
}
}

View File

@@ -6,29 +6,29 @@ declare(strict_types=1);
*
* Provides level filtering (error/warning/info/debug), text search,
* pagination, and a one-click "Clear Old Logs" action. Reads from the
* ab_event_log table via Autoblogger_Logger::query().
* prab_event_log table via PRAutoBlogger_Logger::query().
*
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
* Dependencies: Autoblogger_Logger.
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
* Dependencies: PRAutoBlogger_Logger.
*
* @see core/class-logger.php — Writes log entries and provides query().
* @see templates/admin/log-viewer.php — Renders the HTML.
* @see class-autoblogger.php — Registers the AJAX clear action.
* @see class-prautoblogger.php — Registers the AJAX clear action.
*/
class Autoblogger_Log_Viewer {
class PRAutoBlogger_Log_Viewer {
/**
* Register the log viewer submenu page under AutoBlogger.
* Register the log viewer submenu page under PRAutoBlogger.
*
* @return void
*/
public function on_register_menu(): void {
add_submenu_page(
'autoblogger-settings',
__( 'Activity Log', 'autoblogger' ),
__( 'Activity Log', 'autoblogger' ),
'prautoblogger-settings',
__( 'Activity Log', 'prautoblogger' ),
__( 'Activity Log', 'prautoblogger' ),
'manage_options',
'autoblogger-logs',
'prautoblogger-logs',
[ $this, 'render_page' ]
);
}
@@ -47,36 +47,36 @@ class Autoblogger_Log_Viewer {
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
$paged = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
$result = Autoblogger_Logger::query( $level, $search, $paged, 50 );
$result = PRAutoBlogger_Logger::query( $level, $search, $paged, 50 );
$rows = $result['rows'];
$total = $result['total'];
$total_pages = (int) ceil( $total / 50 );
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/log-viewer.php';
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/log-viewer.php';
}
/**
* AJAX handler: clear logs older than 30 days.
*
* Side effects: deletes rows from ab_event_log.
* Side effects: deletes rows from prab_event_log.
*
* @return void
*/
public function on_ajax_clear_logs(): void {
check_ajax_referer( 'autoblogger_clear_logs', 'nonce' );
check_ajax_referer( 'prautoblogger_clear_logs', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
return;
}
$days = isset( $_POST['days'] ) ? absint( $_POST['days'] ) : 30;
$deleted = Autoblogger_Logger::prune( $days );
$deleted = PRAutoBlogger_Logger::prune( $days );
wp_send_json_success( [
'message' => sprintf(
/* translators: %d: number of entries deleted */
__( '%d log entries cleared.', 'autoblogger' ),
__( '%d log entries cleared.', 'prautoblogger' ),
$deleted
),
] );

View File

@@ -7,26 +7,26 @@ declare(strict_types=1);
* Shows: monthly spend, daily cost chart, spend by pipeline stage, budget
* utilization, top/bottom performing posts, and recent generation logs.
*
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
* Dependencies: Autoblogger_Cost_Tracker.
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
* Dependencies: PRAutoBlogger_Cost_Tracker.
*
* @see class-autoblogger.php — Registers the hook.
* @see class-prautoblogger.php — Registers the hook.
* @see core/class-cost-tracker.php — Provides cost data.
*/
class Autoblogger_Metrics_Page {
class PRAutoBlogger_Metrics_Page {
/**
* Register the metrics submenu page under AutoBlogger.
* Register the metrics submenu page under PRAutoBlogger.
*
* @return void
*/
public function on_register_menu(): void {
add_submenu_page(
'autoblogger-settings',
__( 'Metrics & Costs', 'autoblogger' ),
__( 'Metrics & Costs', 'autoblogger' ),
'prautoblogger-settings',
__( 'Metrics & Costs', 'prautoblogger' ),
__( 'Metrics & Costs', 'prautoblogger' ),
'manage_options',
'autoblogger-metrics',
'prautoblogger-metrics',
[ $this, 'render_page' ]
);
}
@@ -41,6 +41,6 @@ class Autoblogger_Metrics_Page {
return;
}
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metrics-page.php';
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metrics-page.php';
}
}

View File

@@ -2,18 +2,18 @@
declare(strict_types=1);
/**
* Adds a metabox to posts showing AutoBlogger generation metadata.
* Adds a metabox to posts showing PRAutoBlogger generation metadata.
*
* Only appears on posts that were generated by AutoBlogger (checked via
* _autoblogger_generated post meta).
* Only appears on posts that were generated by PRAutoBlogger (checked via
* _prautoblogger_generated post meta).
*
* Triggered by: Autoblogger::register_admin_hooks() on `add_meta_boxes`.
* Triggered by: PRAutoBlogger::register_admin_hooks() on `add_meta_boxes`.
* Dependencies: WordPress post meta API.
*
* @see class-autoblogger.php — Registers the hook.
* @see class-prautoblogger.php — Registers the hook.
* @see core/class-publisher.php — Writes the metadata this class displays.
*/
class Autoblogger_Post_Metabox {
class PRAutoBlogger_Post_Metabox {
/**
* Register the metabox for the post editor.
@@ -27,13 +27,13 @@ class Autoblogger_Post_Metabox {
}
global $post;
if ( ! $post || '1' !== get_post_meta( $post->ID, '_autoblogger_generated', true ) ) {
if ( ! $post || '1' !== get_post_meta( $post->ID, '_prautoblogger_generated', true ) ) {
return;
}
add_meta_box(
'autoblogger_generation_info',
__( 'AutoBlogger Generation Info', 'autoblogger' ),
'prautoblogger_generation_info',
__( 'PRAutoBlogger Generation Info', 'prautoblogger' ),
[ $this, 'render_metabox' ],
'post',
'side',
@@ -49,6 +49,6 @@ class Autoblogger_Post_Metabox {
* @return void
*/
public function render_metabox( \WP_Post $post ): void {
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metabox-generation-info.php';
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metabox-generation-info.php';
}
}

View File

@@ -7,27 +7,27 @@ declare(strict_types=1);
* Provides approve (publish), edit (opens WP editor), and reject (trash)
* actions on each draft. Supports bulk approve/reject via checkboxes.
*
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
* Dependencies: WordPress WP_Query, post meta API.
*
* @see class-autoblogger.php — Registers the hook and AJAX handlers.
* @see class-prautoblogger.php — Registers the hook and AJAX handlers.
* @see core/class-publisher.php — Creates the drafts this page displays.
* @see templates/admin/review-queue.php — Renders the HTML.
*/
class Autoblogger_Review_Queue {
class PRAutoBlogger_Review_Queue {
/**
* Register the review queue submenu page under AutoBlogger.
* Register the review queue submenu page under PRAutoBlogger.
*
* @return void
*/
public function on_register_menu(): void {
$hook = add_submenu_page(
'autoblogger-settings',
__( 'Review Queue', 'autoblogger' ),
__( 'Review Queue', 'autoblogger' ),
'prautoblogger-settings',
__( 'Review Queue', 'prautoblogger' ),
__( 'Review Queue', 'prautoblogger' ),
'manage_options',
'autoblogger-review-queue',
'prautoblogger-review-queue',
[ $this, 'render_page' ]
);
@@ -49,7 +49,7 @@ class Autoblogger_Review_Queue {
$paged = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
$query = $this->get_pending_posts( $paged );
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/review-queue.php';
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/review-queue.php';
}
/**
@@ -67,7 +67,7 @@ class Autoblogger_Review_Queue {
'paged' => $paged,
'meta_query' => [
[
'key' => '_autoblogger_generated',
'key' => '_prautoblogger_generated',
'value' => '1',
],
],
@@ -87,19 +87,19 @@ class Autoblogger_Review_Queue {
* @return void
*/
public function on_handle_bulk_actions(): void {
if ( ! isset( $_POST['autoblogger_bulk_action'] ) ) {
if ( ! isset( $_POST['prautoblogger_bulk_action'] ) ) {
return;
}
check_admin_referer( 'autoblogger_review_queue_bulk', 'autoblogger_review_nonce' );
check_admin_referer( 'prautoblogger_review_queue_bulk', 'prautoblogger_review_nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$action = sanitize_text_field( wp_unslash( $_POST['autoblogger_bulk_action'] ) );
$post_ids = isset( $_POST['autoblogger_post_ids'] ) && is_array( $_POST['autoblogger_post_ids'] )
? array_map( 'absint', $_POST['autoblogger_post_ids'] )
$action = sanitize_text_field( wp_unslash( $_POST['prautoblogger_bulk_action'] ) );
$post_ids = isset( $_POST['prautoblogger_post_ids'] ) && is_array( $_POST['prautoblogger_post_ids'] )
? array_map( 'absint', $_POST['prautoblogger_post_ids'] )
: [];
if ( empty( $post_ids ) ) {
@@ -115,7 +115,7 @@ class Autoblogger_Review_Queue {
if ( 'approve' === $action ) {
$result = wp_update_post( [ 'ID' => $post_id, 'post_status' => 'publish' ], true );
if ( ! is_wp_error( $result ) ) {
update_post_meta( $post_id, '_autoblogger_approved_at', gmdate( 'c' ) );
update_post_meta( $post_id, '_prautoblogger_approved_at', gmdate( 'c' ) );
$count++;
}
} elseif ( 'reject' === $action ) {
@@ -128,9 +128,9 @@ class Autoblogger_Review_Queue {
$redirect = add_query_arg(
[
'page' => 'autoblogger-review-queue',
'autoblogger_bulk_done' => $count,
'autoblogger_bulk_type' => $action,
'page' => 'prautoblogger-review-queue',
'prautoblogger_bulk_done' => $count,
'prautoblogger_bulk_type' => $action,
],
admin_url( 'admin.php' )
);
@@ -147,16 +147,16 @@ class Autoblogger_Review_Queue {
* @return void
*/
public function on_ajax_approve_post(): void {
check_ajax_referer( 'autoblogger_review_queue', 'nonce' );
check_ajax_referer( 'prautoblogger_review_queue', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
return;
}
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( $post_id <= 0 ) {
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'autoblogger' ) ] );
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'prautoblogger' ) ] );
return;
}
@@ -167,8 +167,8 @@ class Autoblogger_Review_Queue {
return;
}
update_post_meta( $post_id, '_autoblogger_approved_at', gmdate( 'c' ) );
wp_send_json_success( [ 'message' => __( 'Post published.', 'autoblogger' ), 'post_id' => $post_id ] );
update_post_meta( $post_id, '_prautoblogger_approved_at', gmdate( 'c' ) );
wp_send_json_success( [ 'message' => __( 'Post published.', 'prautoblogger' ), 'post_id' => $post_id ] );
}
/**
@@ -179,25 +179,25 @@ class Autoblogger_Review_Queue {
* @return void
*/
public function on_ajax_reject_post(): void {
check_ajax_referer( 'autoblogger_review_queue', 'nonce' );
check_ajax_referer( 'prautoblogger_review_queue', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
return;
}
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( $post_id <= 0 ) {
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'autoblogger' ) ] );
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'prautoblogger' ) ] );
return;
}
$result = wp_trash_post( $post_id );
if ( false === $result || null === $result ) {
wp_send_json_error( [ 'message' => __( 'Failed to reject post.', 'autoblogger' ) ] );
wp_send_json_error( [ 'message' => __( 'Failed to reject post.', 'prautoblogger' ) ] );
return;
}
wp_send_json_success( [ 'message' => __( 'Post rejected.', 'autoblogger' ), 'post_id' => $post_id ] );
wp_send_json_success( [ 'message' => __( 'Post rejected.', 'prautoblogger' ), 'post_id' => $post_id ] );
}
}

View File

@@ -2,18 +2,18 @@
declare(strict_types=1);
/**
* Declarative settings fields and sections for the AutoBlogger admin page.
* Declarative settings fields and sections for the PRAutoBlogger admin page.
*
* Centralizes all field and section definitions in one place, making it trivial
* to add new settings (just add one array entry). Decoupled from page rendering logic.
*
* Triggered by: Autoblogger_Admin_Page::on_register_settings() calls static methods here.
* Triggered by: PRAutoBlogger_Admin_Page::on_register_settings() calls static methods here.
* Dependencies: None — contains only data definitions and static methods.
*
* @see admin/class-admin-page.php — Calls get_sections() and get_fields() to register settings.
* @see CONVENTIONS.md — "How To: Add a New Admin Setting".
*/
class Autoblogger_Settings_Fields {
class PRAutoBlogger_Settings_Fields {
/**
* Get all settings sections. Each section maps to a tab in the admin UI.
@@ -22,40 +22,40 @@ class Autoblogger_Settings_Fields {
*/
public static function get_sections(): array {
return [
'autoblogger_api' => [
'title' => __( 'API Keys', 'autoblogger' ),
'prautoblogger_api' => [
'title' => __( 'API Keys', 'prautoblogger' ),
'icon' => 'dashicons-admin-network',
'description' => __( 'Connect your external services. Keys are encrypted at rest.', 'autoblogger' ),
'description' => __( 'Connect your external services. Keys are encrypted at rest.', 'prautoblogger' ),
],
'autoblogger_models' => [
'title' => __( 'AI Models', 'autoblogger' ),
'prautoblogger_models' => [
'title' => __( 'AI Models', 'prautoblogger' ),
'icon' => 'dashicons-superhero-alt',
'description' => __( 'Choose which models power each stage of the pipeline.', 'autoblogger' ),
'description' => __( 'Choose which models power each stage of the pipeline.', 'prautoblogger' ),
],
'autoblogger_content' => [
'title' => __( 'Content', 'autoblogger' ),
'prautoblogger_content' => [
'title' => __( 'Content', 'prautoblogger' ),
'icon' => 'dashicons-edit-large',
'description' => __( 'Control tone, length, pipeline mode, and topic guardrails.', 'autoblogger' ),
'description' => __( 'Control tone, length, pipeline mode, and topic guardrails.', 'prautoblogger' ),
],
'autoblogger_sources' => [
'title' => __( 'Sources', 'autoblogger' ),
'prautoblogger_sources' => [
'title' => __( 'Sources', 'prautoblogger' ),
'icon' => 'dashicons-rss',
'description' => __( 'Configure where AutoBlogger finds trending topics.', 'autoblogger' ),
'description' => __( 'Configure where PRAutoBlogger finds trending topics.', 'prautoblogger' ),
],
'autoblogger_schedule' => [
'title' => __( 'Schedule & Budget', 'autoblogger' ),
'prautoblogger_schedule' => [
'title' => __( 'Schedule & Budget', 'prautoblogger' ),
'icon' => 'dashicons-calendar-alt',
'description' => __( 'Set daily generation schedule, volume, and spending limits.', 'autoblogger' ),
'description' => __( 'Set daily generation schedule, volume, and spending limits.', 'prautoblogger' ),
],
'autoblogger_publishing' => [
'title' => __( 'Publishing', 'autoblogger' ),
'prautoblogger_publishing' => [
'title' => __( 'Publishing', 'prautoblogger' ),
'icon' => 'dashicons-megaphone',
'description' => __( 'Control how generated content is published.', 'autoblogger' ),
'description' => __( 'Control how generated content is published.', 'prautoblogger' ),
],
'autoblogger_analytics' => [
'title' => __( 'Analytics', 'autoblogger' ),
'prautoblogger_analytics' => [
'title' => __( 'Analytics', 'prautoblogger' ),
'icon' => 'dashicons-chart-area',
'description' => __( 'Connect Google Analytics 4 for post performance scoring.', 'autoblogger' ),
'description' => __( 'Connect Google Analytics 4 for post performance scoring.', 'prautoblogger' ),
],
];
}
@@ -69,222 +69,222 @@ class Autoblogger_Settings_Fields {
return [
// ── API Keys ────────────────────────────────────────────────
[
'id' => 'autoblogger_openrouter_api_key',
'label' => __( 'OpenRouter API Key', 'autoblogger' ),
'id' => 'prautoblogger_openrouter_api_key',
'label' => __( 'OpenRouter API Key', 'prautoblogger' ),
'type' => 'password',
'section' => 'autoblogger_api',
'description' => __( 'Get your key at openrouter.ai/keys', 'autoblogger' ),
'section' => 'prautoblogger_api',
'description' => __( 'Get your key at openrouter.ai/keys', 'prautoblogger' ),
'icon' => '🔑',
],
[
'id' => 'autoblogger_reddit_client_id',
'label' => __( 'Reddit Client ID', 'autoblogger' ),
'id' => 'prautoblogger_reddit_client_id',
'label' => __( 'Reddit Client ID', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_api',
'description' => __( 'Create a script app at reddit.com/prefs/apps', 'autoblogger' ),
'section' => 'prautoblogger_api',
'description' => __( 'Create a script app at reddit.com/prefs/apps', 'prautoblogger' ),
'icon' => '🤖',
],
[
'id' => 'autoblogger_reddit_client_secret',
'label' => __( 'Reddit Client Secret', 'autoblogger' ),
'id' => 'prautoblogger_reddit_client_secret',
'label' => __( 'Reddit Client Secret', 'prautoblogger' ),
'type' => 'password',
'section' => 'autoblogger_api',
'section' => 'prautoblogger_api',
],
// ── AI Models ───────────────────────────────────────────────
[
'id' => 'autoblogger_analysis_model',
'label' => __( 'Analysis Model', 'autoblogger' ),
'id' => 'prautoblogger_analysis_model',
'label' => __( 'Analysis Model', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_models',
'default' => AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
'description' => __( 'Used for source analysis. Pick a cheap, fast model.', 'autoblogger' ),
'badge' => __( 'Low cost', 'autoblogger' ),
'section' => 'prautoblogger_models',
'default' => PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
'description' => __( 'Used for source analysis. Pick a cheap, fast model.', 'prautoblogger' ),
'badge' => __( 'Low cost', 'prautoblogger' ),
],
[
'id' => 'autoblogger_writing_model',
'label' => __( 'Writing Model', 'autoblogger' ),
'id' => 'prautoblogger_writing_model',
'label' => __( 'Writing Model', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_models',
'default' => AUTOBLOGGER_DEFAULT_WRITING_MODEL,
'description' => __( 'Used for article generation. Quality matters here.', 'autoblogger' ),
'badge' => __( 'Quality', 'autoblogger' ),
'section' => 'prautoblogger_models',
'default' => PRAUTOBLOGGER_DEFAULT_WRITING_MODEL,
'description' => __( 'Used for article generation. Quality matters here.', 'prautoblogger' ),
'badge' => __( 'Quality', 'prautoblogger' ),
],
[
'id' => 'autoblogger_editor_model',
'label' => __( 'Editor Model', 'autoblogger' ),
'id' => 'prautoblogger_editor_model',
'label' => __( 'Editor Model', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_models',
'default' => AUTOBLOGGER_DEFAULT_EDITOR_MODEL,
'description' => __( 'Used for the chief editor review pass.', 'autoblogger' ),
'badge' => __( 'Quality', 'autoblogger' ),
'section' => 'prautoblogger_models',
'default' => PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL,
'description' => __( 'Used for the chief editor review pass.', 'prautoblogger' ),
'badge' => __( 'Quality', 'prautoblogger' ),
],
// ── Content ─────────────────────────────────────────────────
[
'id' => 'autoblogger_niche_description',
'label' => __( 'Niche Description', 'autoblogger' ),
'id' => 'prautoblogger_niche_description',
'label' => __( 'Niche Description', 'prautoblogger' ),
'type' => 'textarea',
'section' => 'autoblogger_content',
'description' => __( 'Describe your site\'s niche. Guides content analysis and generation.', 'autoblogger' ),
'section' => 'prautoblogger_content',
'description' => __( 'Describe your site\'s niche. Guides content analysis and generation.', 'prautoblogger' ),
],
[
'id' => 'autoblogger_tone',
'label' => __( 'Content Tone', 'autoblogger' ),
'id' => 'prautoblogger_tone',
'label' => __( 'Content Tone', 'prautoblogger' ),
'type' => 'select',
'section' => 'autoblogger_content',
'section' => 'prautoblogger_content',
'default' => 'informational',
'options' => [
'informational' => __( 'Informational', 'autoblogger' ),
'conversational' => __( 'Conversational', 'autoblogger' ),
'professional' => __( 'Professional', 'autoblogger' ),
'casual' => __( 'Casual', 'autoblogger' ),
'authoritative' => __( 'Authoritative', 'autoblogger' ),
'informational' => __( 'Informational', 'prautoblogger' ),
'conversational' => __( 'Conversational', 'prautoblogger' ),
'professional' => __( 'Professional', 'prautoblogger' ),
'casual' => __( 'Casual', 'prautoblogger' ),
'authoritative' => __( 'Authoritative', 'prautoblogger' ),
],
],
[
'id' => 'autoblogger_writing_pipeline',
'label' => __( 'Writing Pipeline', 'autoblogger' ),
'id' => 'prautoblogger_writing_pipeline',
'label' => __( 'Writing Pipeline', 'prautoblogger' ),
'type' => 'select',
'section' => 'autoblogger_content',
'section' => 'prautoblogger_content',
'default' => 'multi_step',
'options' => [
'multi_step' => __( 'Multi-step (outline → draft → polish)', 'autoblogger' ),
'single_pass' => __( 'Single-pass (one LLM call)', 'autoblogger' ),
'multi_step' => __( 'Multi-step (outline → draft → polish)', 'prautoblogger' ),
'single_pass' => __( 'Single-pass (one LLM call)', 'prautoblogger' ),
],
],
[
'id' => 'autoblogger_min_word_count',
'label' => __( 'Min Word Count', 'autoblogger' ),
'id' => 'prautoblogger_min_word_count',
'label' => __( 'Min Word Count', 'prautoblogger' ),
'type' => 'number',
'section' => 'autoblogger_content',
'section' => 'prautoblogger_content',
'default' => 800,
'min' => 200,
],
[
'id' => 'autoblogger_max_word_count',
'label' => __( 'Max Word Count', 'autoblogger' ),
'id' => 'prautoblogger_max_word_count',
'label' => __( 'Max Word Count', 'prautoblogger' ),
'type' => 'number',
'section' => 'autoblogger_content',
'section' => 'prautoblogger_content',
'default' => 2000,
'min' => 500,
],
[
'id' => 'autoblogger_topic_exclusions',
'label' => __( 'Topic Exclusions', 'autoblogger' ),
'id' => 'prautoblogger_topic_exclusions',
'label' => __( 'Topic Exclusions', 'prautoblogger' ),
'type' => 'textarea',
'section' => 'autoblogger_content',
'description' => __( 'Comma-separated topics to never write about.', 'autoblogger' ),
'section' => 'prautoblogger_content',
'description' => __( 'Comma-separated topics to never write about.', 'prautoblogger' ),
],
// ── Sources ─────────────────────────────────────────────────
[
'id' => 'autoblogger_target_subreddits',
'label' => __( 'Target Subreddits', 'autoblogger' ),
'id' => 'prautoblogger_target_subreddits',
'label' => __( 'Target Subreddits', 'prautoblogger' ),
'type' => 'textarea',
'section' => 'autoblogger_sources',
'description' => __( 'Comma-separated, without r/. E.g.: peptides, Nootropics, biohackers', 'autoblogger' ),
'section' => 'prautoblogger_sources',
'description' => __( 'Comma-separated, without r/. E.g.: peptides, Nootropics, biohackers', 'prautoblogger' ),
],
[
'id' => 'autoblogger_enabled_sources',
'label' => __( 'Enabled Sources', 'autoblogger' ),
'id' => 'prautoblogger_enabled_sources',
'label' => __( 'Enabled Sources', 'prautoblogger' ),
'type' => 'checkboxes',
'section' => 'autoblogger_sources',
'section' => 'prautoblogger_sources',
'options' => [
'reddit' => __( 'Reddit', 'autoblogger' ),
'tiktok' => __( 'TikTok (coming soon)', 'autoblogger' ),
'instagram' => __( 'Instagram (coming soon)', 'autoblogger' ),
'youtube' => __( 'YouTube (coming soon)', 'autoblogger' ),
'reddit' => __( 'Reddit', 'prautoblogger' ),
'tiktok' => __( 'TikTok (coming soon)', 'prautoblogger' ),
'instagram' => __( 'Instagram (coming soon)', 'prautoblogger' ),
'youtube' => __( 'YouTube (coming soon)', 'prautoblogger' ),
],
'default' => '["reddit"]',
'description' => __( 'Select which platforms to monitor for topics.', 'autoblogger' ),
'description' => __( 'Select which platforms to monitor for topics.', 'prautoblogger' ),
],
// ── Schedule & Budget ───────────────────────────────────────
[
'id' => 'autoblogger_daily_article_target',
'label' => __( 'Articles per Day', 'autoblogger' ),
'id' => 'prautoblogger_daily_article_target',
'label' => __( 'Articles per Day', 'prautoblogger' ),
'type' => 'number',
'section' => 'autoblogger_schedule',
'section' => 'prautoblogger_schedule',
'default' => 1,
'min' => 1,
'max' => 10,
],
[
'id' => 'autoblogger_schedule_time',
'label' => __( 'Generation Time', 'autoblogger' ),
'id' => 'prautoblogger_schedule_time',
'label' => __( 'Generation Time', 'prautoblogger' ),
'type' => 'time',
'section' => 'autoblogger_schedule',
'section' => 'prautoblogger_schedule',
'default' => '03:00',
'description' => __( 'Daily generation runs at this time (server timezone).', 'autoblogger' ),
'description' => __( 'Daily generation runs at this time (server timezone).', 'prautoblogger' ),
],
[
'id' => 'autoblogger_monthly_budget_usd',
'label' => __( 'Monthly Budget (USD)', 'autoblogger' ),
'id' => 'prautoblogger_monthly_budget_usd',
'label' => __( 'Monthly Budget (USD)', 'prautoblogger' ),
'type' => 'number',
'section' => 'autoblogger_schedule',
'section' => 'prautoblogger_schedule',
'default' => 50,
'min' => 0,
'step' => '0.01',
'description' => __( 'Hard stop when reached. 0 = unlimited.', 'autoblogger' ),
'description' => __( 'Hard stop when reached. 0 = unlimited.', 'prautoblogger' ),
],
// ── Publishing ──────────────────────────────────────────────
[
'id' => 'autoblogger_auto_publish',
'label' => __( 'Auto-Publish', 'autoblogger' ),
'id' => 'prautoblogger_auto_publish',
'label' => __( 'Auto-Publish', 'prautoblogger' ),
'type' => 'toggle',
'section' => 'autoblogger_publishing',
'section' => 'prautoblogger_publishing',
'default' => '0',
'description' => __( 'Automatically publish editor-approved posts. When off, all posts go to the Review Queue as drafts.', 'autoblogger' ),
'description' => __( 'Automatically publish editor-approved posts. When off, all posts go to the Review Queue as drafts.', 'prautoblogger' ),
],
[
'id' => 'autoblogger_default_author',
'label' => __( 'Default Author', 'autoblogger' ),
'id' => 'prautoblogger_default_author',
'label' => __( 'Default Author', 'prautoblogger' ),
'type' => 'author_select',
'section' => 'autoblogger_publishing',
'section' => 'prautoblogger_publishing',
'default' => 0,
'description' => __( 'WordPress user assigned as author of generated posts.', 'autoblogger' ),
'description' => __( 'WordPress user assigned as author of generated posts.', 'prautoblogger' ),
],
[
'id' => 'autoblogger_default_category',
'label' => __( 'Default Category', 'autoblogger' ),
'id' => 'prautoblogger_default_category',
'label' => __( 'Default Category', 'prautoblogger' ),
'type' => 'category_select',
'section' => 'autoblogger_publishing',
'section' => 'prautoblogger_publishing',
'default' => 0,
'description' => __( 'Fallback category for generated posts.', 'autoblogger' ),
'description' => __( 'Fallback category for generated posts.', 'prautoblogger' ),
],
// ── Logging ─────────────────────────────────────────────────
[
'id' => 'autoblogger_log_level',
'label' => __( 'Log Level', 'autoblogger' ),
'id' => 'prautoblogger_log_level',
'label' => __( 'Log Level', 'prautoblogger' ),
'type' => 'select',
'section' => 'autoblogger_publishing',
'section' => 'prautoblogger_publishing',
'default' => 'info',
'options' => [
'error' => __( 'Error — only failures', 'autoblogger' ),
'warning' => __( 'Warning — errors + warnings', 'autoblogger' ),
'info' => __( 'Info — key events (recommended)', 'autoblogger' ),
'debug' => __( 'Debug — everything (verbose)', 'autoblogger' ),
'error' => __( 'Error — only failures', 'prautoblogger' ),
'warning' => __( 'Warning — errors + warnings', 'prautoblogger' ),
'info' => __( 'Info — key events (recommended)', 'prautoblogger' ),
'debug' => __( 'Debug — everything (verbose)', 'prautoblogger' ),
],
'description' => __( 'Controls detail level in the Activity Log.', 'autoblogger' ),
'description' => __( 'Controls detail level in the Activity Log.', 'prautoblogger' ),
],
// ── Analytics ───────────────────────────────────────────────
[
'id' => 'autoblogger_ga4_property_id',
'label' => __( 'GA4 Property ID', 'autoblogger' ),
'id' => 'prautoblogger_ga4_property_id',
'label' => __( 'GA4 Property ID', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_analytics',
'description' => __( 'Format: properties/XXXXXXXXX. Leave blank to skip GA4.', 'autoblogger' ),
'section' => 'prautoblogger_analytics',
'description' => __( 'Format: properties/XXXXXXXXX. Leave blank to skip GA4.', 'prautoblogger' ),
],
[
'id' => 'autoblogger_ga4_credentials_json',
'label' => __( 'Service Account JSON', 'autoblogger' ),
'id' => 'prautoblogger_ga4_credentials_json',
'label' => __( 'Service Account JSON', 'prautoblogger' ),
'type' => 'password',
'section' => 'autoblogger_analytics',
'description' => __( 'Paste the full JSON key file for a service account with Analytics read access.', 'autoblogger' ),
'section' => 'prautoblogger_analytics',
'description' => __( 'Paste the full JSON key file for a service account with Analytics read access.', 'prautoblogger' ),
],
];
}

View File

@@ -5,13 +5,13 @@ declare(strict_types=1);
* Handles plugin activation: creates database tables, sets default options,
* and schedules the initial cron job.
*
* Triggered by: WordPress `register_activation_hook` in autoblogger.php.
* Triggered by: WordPress `register_activation_hook` in prautoblogger.php.
* Dependencies: WordPress $wpdb, dbDelta().
*
* @see class-deactivator.php — Reverses transient/cron setup on deactivation.
* @see ARCHITECTURE.md — Database schema definitions.
*/
class Autoblogger_Activator {
class PRAutoBlogger_Activator {
/**
* Run activation tasks.
@@ -30,7 +30,7 @@ class Autoblogger_Activator {
self::schedule_cron();
// Store the DB version so we can run migrations on future updates.
update_option( 'autoblogger_db_version', AUTOBLOGGER_DB_VERSION, false );
update_option( 'prautoblogger_db_version', PRAUTOBLOGGER_DB_VERSION, false );
}
/**
@@ -44,7 +44,7 @@ class Autoblogger_Activator {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$prefix = $wpdb->prefix . 'autoblogger_';
$prefix = $wpdb->prefix . 'prautoblogger_';
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
@@ -156,21 +156,21 @@ class Autoblogger_Activator {
*/
private static function set_default_options(): void {
$defaults = [
'autoblogger_analysis_model' => AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
'autoblogger_writing_model' => AUTOBLOGGER_DEFAULT_WRITING_MODEL,
'autoblogger_editor_model' => AUTOBLOGGER_DEFAULT_EDITOR_MODEL,
'autoblogger_daily_article_target' => 1,
'autoblogger_writing_pipeline' => 'multi_step',
'autoblogger_niche_description' => '',
'autoblogger_target_subreddits' => '[]',
'autoblogger_monthly_budget_usd' => 50.00,
'autoblogger_tone' => 'informational',
'autoblogger_min_word_count' => 800,
'autoblogger_max_word_count' => 2000,
'autoblogger_topic_exclusions' => '[]',
'autoblogger_enabled_sources' => '["reddit"]',
'autoblogger_schedule_time' => '03:00',
'autoblogger_log_level' => 'info',
'prautoblogger_analysis_model' => PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
'prautoblogger_writing_model' => PRAUTOBLOGGER_DEFAULT_WRITING_MODEL,
'prautoblogger_editor_model' => PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL,
'prautoblogger_daily_article_target' => 1,
'prautoblogger_writing_pipeline' => 'multi_step',
'prautoblogger_niche_description' => '',
'prautoblogger_target_subreddits' => '[]',
'prautoblogger_monthly_budget_usd' => 50.00,
'prautoblogger_tone' => 'informational',
'prautoblogger_min_word_count' => 800,
'prautoblogger_max_word_count' => 2000,
'prautoblogger_topic_exclusions' => '[]',
'prautoblogger_enabled_sources' => '["reddit"]',
'prautoblogger_schedule_time' => '03:00',
'prautoblogger_log_level' => 'info',
];
foreach ( $defaults as $key => $value ) {
@@ -181,8 +181,8 @@ class Autoblogger_Activator {
/**
* Schedule the daily generation cron event if not already scheduled.
*
* The custom 'autoblogger_six_hours' schedule is normally registered via
* the `cron_schedules` filter in class-autoblogger.php. However, activation
* The custom 'prautoblogger_six_hours' schedule is normally registered via
* the `cron_schedules` filter in class-prautoblogger.php. However, activation
* runs before `plugins_loaded`, so the filter may not be registered yet.
* We register it inline here to ensure it exists at scheduling time.
*
@@ -191,36 +191,36 @@ class Autoblogger_Activator {
* @return void
*/
private static function schedule_cron(): void {
if ( ! wp_next_scheduled( 'autoblogger_daily_generation' ) ) {
if ( ! wp_next_scheduled( 'prautoblogger_daily_generation' ) ) {
// Schedule for the configured time tomorrow.
$time_str = get_option( 'autoblogger_schedule_time', '03:00' );
$time_str = get_option( 'prautoblogger_schedule_time', '03:00' );
$parts = explode( ':', $time_str );
$hour = isset( $parts[0] ) ? absint( $parts[0] ) : 3;
$minute = isset( $parts[1] ) ? absint( $parts[1] ) : 0;
$timestamp = strtotime( "tomorrow {$hour}:{$minute}" );
if ( false !== $timestamp ) {
wp_schedule_event( $timestamp, 'daily', 'autoblogger_daily_generation' );
wp_schedule_event( $timestamp, 'daily', 'prautoblogger_daily_generation' );
}
}
// Ensure the six-hour schedule exists before referencing it.
// Activation can fire before plugins_loaded, so the cron_schedules filter
// in class-autoblogger.php may not be hooked yet.
// in class-prautoblogger.php may not be hooked yet.
$schedules = wp_get_schedules();
if ( ! isset( $schedules['autoblogger_six_hours'] ) ) {
if ( ! isset( $schedules['prautoblogger_six_hours'] ) ) {
add_filter( 'cron_schedules', static function ( array $scheds ): array {
$scheds['autoblogger_six_hours'] = [
$scheds['prautoblogger_six_hours'] = [
'interval' => 6 * HOUR_IN_SECONDS,
'display' => __( 'Every Six Hours', 'autoblogger' ),
'display' => __( 'Every Six Hours', 'prautoblogger' ),
];
return $scheds;
} );
}
// Schedule a separate metrics collection job (runs every 6 hours).
if ( ! wp_next_scheduled( 'autoblogger_collect_metrics' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'autoblogger_six_hours', 'autoblogger_collect_metrics' );
if ( ! wp_next_scheduled( 'prautoblogger_collect_metrics' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'prautoblogger_six_hours', 'prautoblogger_collect_metrics' );
}
}
}

View File

@@ -2,17 +2,17 @@
declare(strict_types=1);
/**
* PSR-4-inspired autoloader for AutoBlogger classes.
* PSR-4-inspired autoloader for PRAutoBlogger classes.
*
* Maps class names like `Autoblogger_Content_Generator` to file paths like
* Maps class names like `PRAutoBlogger_Content_Generator` to file paths like
* `includes/core/class-content-generator.php` by scanning known subdirectories.
*
* Triggered by: PHP's spl_autoload when a class is first referenced.
* Dependencies: None — this is loaded before anything else.
*
* @see autoblogger.php — Registers this autoloader at boot time.
* @see prautoblogger.php — Registers this autoloader at boot time.
*/
class Autoblogger_Autoloader {
class PRAutoBlogger_Autoloader {
/**
* Directories to scan for class files, relative to the plugin's includes/ dir.
@@ -25,6 +25,7 @@ class Autoblogger_Autoloader {
'core/',
'providers/',
'models/',
'frontend/',
];
/**
@@ -39,7 +40,7 @@ class Autoblogger_Autoloader {
/**
* Attempt to load a class file.
*
* Converts `Autoblogger_Content_Generator` → `class-content-generator.php`
* Converts `PRAutoBlogger_Content_Generator` → `class-content-generator.php`
* and searches each registered subdirectory under includes/.
*
* @param string $class_name Fully qualified class name.
@@ -48,12 +49,12 @@ class Autoblogger_Autoloader {
*/
public static function autoload( string $class_name ): void {
// Only handle our own classes.
if ( 0 !== strpos( $class_name, 'Autoblogger' ) ) {
if ( 0 !== strpos( $class_name, 'PRAutoBlogger' ) ) {
return;
}
$file_name = self::class_to_filename( $class_name );
$base_dir = AUTOBLOGGER_PLUGIN_DIR . 'includes/';
$base_dir = PRAUTOBLOGGER_PLUGIN_DIR . 'includes/';
foreach ( self::$directories as $directory ) {
$file_path = $base_dir . $directory . $file_name;
@@ -67,16 +68,16 @@ class Autoblogger_Autoloader {
/**
* Convert a class name to its expected filename.
*
* Autoblogger_Content_Generator → class-content-generator.php
* Autoblogger_LLM_Provider_Interface → interface-llm-provider.php
* PRAutoBlogger_Content_Generator → class-content-generator.php
* PRAutoBlogger_LLM_Provider_Interface → interface-llm-provider.php
*
* @param string $class_name The class name to convert.
*
* @return string The expected filename.
*/
private static function class_to_filename( string $class_name ): string {
// Remove the Autoblogger_ prefix.
$name = (string) preg_replace( '/^Autoblogger_/', '', $class_name );
// Remove the PRAutoBlogger_ prefix.
$name = (string) preg_replace( '/^PRAutoBlogger_/', '', $class_name );
// Determine file prefix: interface- or class-.
$prefix = 'class-';

View File

@@ -7,13 +7,13 @@ declare(strict_types=1);
* Note: does NOT delete data or options — that happens in uninstall.php.
* Deactivation should be reversible (user can reactivate and pick up where they left off).
*
* Triggered by: WordPress `register_deactivation_hook` in autoblogger.php.
* Triggered by: WordPress `register_deactivation_hook` in prautoblogger.php.
* Dependencies: None.
*
* @see class-activator.php — Sets up what this class tears down.
* @see uninstall.php — Permanent data removal on plugin deletion.
*/
class Autoblogger_Deactivator {
class PRAutoBlogger_Deactivator {
/**
* Run deactivation tasks.
@@ -34,8 +34,8 @@ class Autoblogger_Deactivator {
*/
private static function clear_cron(): void {
$hooks = [
'autoblogger_daily_generation',
'autoblogger_collect_metrics',
'prautoblogger_daily_generation',
'prautoblogger_collect_metrics',
];
foreach ( $hooks as $hook ) {
@@ -55,9 +55,9 @@ class Autoblogger_Deactivator {
*/
private static function clear_transients(): void {
$transients = [
'autoblogger_reddit_token',
'autoblogger_generation_lock',
'autoblogger_openrouter_models',
'prautoblogger_reddit_token',
'prautoblogger_generation_lock',
'prautoblogger_openrouter_models',
];
foreach ( $transients as $transient ) {

View File

@@ -13,7 +13,7 @@ declare(strict_types=1);
*
* @see admin/class-admin-page.php — Uses this to store/retrieve API keys.
*/
class Autoblogger_Encryption {
class PRAutoBlogger_Encryption {
private const METHOD = 'aes-256-cbc';
@@ -35,7 +35,7 @@ class Autoblogger_Encryption {
$ciphertext = openssl_encrypt( $plaintext, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
if ( false === $ciphertext ) {
Autoblogger_Logger::instance()->error( 'Encryption failed: ' . openssl_error_string(), 'encryption' );
PRAutoBlogger_Logger::instance()->error( 'Encryption failed: ' . openssl_error_string(), 'encryption' );
return '';
}
@@ -62,7 +62,7 @@ class Autoblogger_Encryption {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$raw = base64_decode( $encrypted, true );
if ( false === $raw || strlen( $raw ) <= $iv_len ) {
Autoblogger_Logger::instance()->error( 'Decryption failed: invalid ciphertext format.', 'encryption' );
PRAutoBlogger_Logger::instance()->error( 'Decryption failed: invalid ciphertext format.', 'encryption' );
return '';
}
@@ -71,7 +71,7 @@ class Autoblogger_Encryption {
$plaintext = openssl_decrypt( $ciphertext, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
if ( false === $plaintext ) {
Autoblogger_Logger::instance()->error( 'Decryption failed: ' . openssl_error_string(), 'encryption' );
PRAutoBlogger_Logger::instance()->error( 'Decryption failed: ' . openssl_error_string(), 'encryption' );
return '';
}

View File

@@ -2,21 +2,21 @@
declare(strict_types=1);
/**
* Main orchestrator for the AutoBlogger plugin.
* Main orchestrator for the PRAutoBlogger plugin.
*
* Registers all WordPress hooks (actions and filters) and wires up dependencies.
* This is the ONLY place hooks are registered — individual classes contain only
* business logic, not hook registration.
*
* Triggered by: autoblogger() singleton in autoblogger.php, called on `plugins_loaded`.
* Dependencies: All other plugin classes (loaded via autoloader), Autoblogger_Pipeline_Runner.
* Triggered by: prautoblogger() singleton in prautoblogger.php, called on `plugins_loaded`.
* Dependencies: All other plugin classes (loaded via autoloader), PRAutoBlogger_Pipeline_Runner.
*
* @see autoblogger.php — Plugin bootstrap that instantiates this class.
* @see prautoblogger.php — Plugin bootstrap that instantiates this class.
* @see core/class-pipeline-runner.php — Executes the generation pipeline.
* @see ARCHITECTURE.md — Data flow diagram showing how classes interact.
* @see CONVENTIONS.md — Hook naming conventions.
*/
class Autoblogger {
class PRAutoBlogger {
/**
* Whether the plugin has been initialized.
@@ -52,15 +52,18 @@ class Autoblogger {
// Cron hooks — always registered so WP-Cron can fire them.
$this->register_cron_hooks();
// Frontend hooks — shortcode and REST API for public-facing widget.
$this->register_frontend_hooks();
// AJAX hooks — always registered so admin AJAX works.
$this->register_ajax_hooks();
/**
* Fires after AutoBlogger has finished registering all hooks.
* Fires after PRAutoBlogger has finished registering all hooks.
*
* Useful for add-ons that need to hook into AutoBlogger's lifecycle.
* Useful for add-ons that need to hook into PRAutoBlogger's lifecycle.
*/
do_action( 'autoblogger_loaded' );
do_action( 'prautoblogger_loaded' );
}
/**
@@ -69,53 +72,71 @@ class Autoblogger {
* @return void
*/
private function register_admin_hooks(): void {
$admin_page = new Autoblogger_Admin_Page();
$admin_page = new PRAutoBlogger_Admin_Page();
add_action( 'admin_menu', [ $admin_page, 'on_register_menu' ] );
add_action( 'admin_init', [ $admin_page, 'on_register_settings' ] );
add_action( 'admin_enqueue_scripts', [ $admin_page, 'on_enqueue_assets' ] );
$notices = new Autoblogger_Admin_Notices();
$notices = new PRAutoBlogger_Admin_Notices();
add_action( 'admin_notices', [ $notices, 'on_display_notices' ] );
$metabox = new Autoblogger_Post_Metabox();
$metabox = new PRAutoBlogger_Post_Metabox();
add_action( 'add_meta_boxes', [ $metabox, 'on_register_metabox' ] );
$metrics_page = new Autoblogger_Metrics_Page();
$metrics_page = new PRAutoBlogger_Metrics_Page();
add_action( 'admin_menu', [ $metrics_page, 'on_register_menu' ] );
$dashboard_widget = new Autoblogger_Dashboard_Widget();
$dashboard_widget = new PRAutoBlogger_Dashboard_Widget();
add_action( 'wp_dashboard_setup', [ $dashboard_widget, 'on_register_widget' ] );
$review_queue = new Autoblogger_Review_Queue();
$review_queue = new PRAutoBlogger_Review_Queue();
add_action( 'admin_menu', [ $review_queue, 'on_register_menu' ] );
$log_viewer = new Autoblogger_Log_Viewer();
$log_viewer = new PRAutoBlogger_Log_Viewer();
add_action( 'admin_menu', [ $log_viewer, 'on_register_menu' ] );
// Block false update notifications from wordpress.org — our plugin name
// collides with a different "PRAutoBlogger" in the plugin directory.
add_filter( 'site_transient_update_plugins', [ $this, 'filter_block_false_updates' ] );
}
/**
* Register frontend hooks (shortcode and REST API for posts widget).
*
* The shortcode renders a mount-point div; the React component hydrates it
* on the client side by fetching posts from the REST endpoint.
*
* @return void
*/
private function register_frontend_hooks(): void {
$posts_widget = new PRAutoBlogger_Posts_Widget();
add_action( 'init', [ $posts_widget, 'on_register_shortcode' ] );
add_action( 'rest_api_init', [ $posts_widget, 'on_register_rest_route' ] );
}
/** Register cron-triggered hooks for scheduled generation and metrics. */
private function register_cron_hooks(): void {
add_action( 'autoblogger_daily_generation', [ $this, 'on_daily_generation' ] );
add_action( 'autoblogger_collect_metrics', [ $this, 'on_collect_metrics' ] );
add_action( 'prautoblogger_daily_generation', [ $this, 'on_daily_generation' ] );
add_action( 'prautoblogger_collect_metrics', [ $this, 'on_collect_metrics' ] );
}
/** Register AJAX handlers for admin actions. */
private function register_ajax_hooks(): void {
add_action( 'wp_ajax_autoblogger_generate_now', [ $this, 'on_ajax_generate_now' ] );
add_action( 'wp_ajax_autoblogger_test_connection', [ $this, 'on_ajax_test_connection' ] );
add_action( 'wp_ajax_prautoblogger_generate_now', [ $this, 'on_ajax_generate_now' ] );
add_action( 'wp_ajax_prautoblogger_test_connection', [ $this, 'on_ajax_test_connection' ] );
$review_queue = new Autoblogger_Review_Queue();
add_action( 'wp_ajax_autoblogger_approve_post', [ $review_queue, 'on_ajax_approve_post' ] );
add_action( 'wp_ajax_autoblogger_reject_post', [ $review_queue, 'on_ajax_reject_post' ] );
$review_queue = new PRAutoBlogger_Review_Queue();
add_action( 'wp_ajax_prautoblogger_approve_post', [ $review_queue, 'on_ajax_approve_post' ] );
add_action( 'wp_ajax_prautoblogger_reject_post', [ $review_queue, 'on_ajax_reject_post' ] );
$log_viewer = new Autoblogger_Log_Viewer();
add_action( 'wp_ajax_autoblogger_clear_logs', [ $log_viewer, 'on_ajax_clear_logs' ] );
$log_viewer = new PRAutoBlogger_Log_Viewer();
add_action( 'wp_ajax_prautoblogger_clear_logs', [ $log_viewer, 'on_ajax_clear_logs' ] );
}
/**
* Check if database needs migration after plugin update.
*
* Compares stored DB version with current AUTOBLOGGER_DB_VERSION.
* Compares stored DB version with current PRAUTOBLOGGER_DB_VERSION.
* If different, re-runs activation to apply schema changes via dbDelta.
*
* Side effects: may update database schema and db_version option.
@@ -123,9 +144,9 @@ class Autoblogger {
* @return void
*/
public function on_check_db_version(): void {
$stored_version = get_option( 'autoblogger_db_version', '0' );
if ( version_compare( $stored_version, AUTOBLOGGER_DB_VERSION, '<' ) ) {
Autoblogger_Activator::activate();
$stored_version = get_option( 'prautoblogger_db_version', '0' );
if ( version_compare( $stored_version, PRAUTOBLOGGER_DB_VERSION, '<' ) ) {
PRAutoBlogger_Activator::activate();
}
}
@@ -137,13 +158,32 @@ class Autoblogger {
* @return array<string, array{interval: int, display: string}> Modified schedules.
*/
public function filter_add_cron_schedules( array $schedules ): array {
$schedules['autoblogger_six_hours'] = [
$schedules['prautoblogger_six_hours'] = [
'interval' => 6 * HOUR_IN_SECONDS,
'display' => __( 'Every Six Hours', 'autoblogger' ),
'display' => __( 'Every Six Hours', 'prautoblogger' ),
];
return $schedules;
}
/**
* Block false update notifications from wordpress.org.
*
* Our plugin slug may collide with a different plugin in the directory.
* This filter removes any update notice that wordpress.org might push
* for our plugin basename, since we manage updates via our own CI/CD.
*
* @param object $transient The update_plugins transient data.
*
* @return object Modified transient with our plugin removed from updates.
*/
public function filter_block_false_updates( $transient ) {
$plugin_basename = PRAUTOBLOGGER_PLUGIN_BASENAME;
if ( isset( $transient->response[ $plugin_basename ] ) ) {
unset( $transient->response[ $plugin_basename ] );
}
return $transient;
}
/**
* Handle the daily generation cron event.
*
@@ -155,14 +195,14 @@ class Autoblogger {
*/
public function on_daily_generation(): void {
if ( ! $this->acquire_generation_lock() ) {
Autoblogger_Logger::instance()->info( 'Daily generation skipped: already running (lock held).', 'scheduler' );
PRAutoBlogger_Logger::instance()->info( 'Daily generation skipped: already running (lock held).', 'scheduler' );
return;
}
try {
( new Autoblogger_Pipeline_Runner() )->run();
( new PRAutoBlogger_Pipeline_Runner() )->run();
} catch ( \Exception $e ) {
Autoblogger_Logger::instance()->error( 'Daily generation FAILED: ' . $e->getMessage(), 'scheduler' );
PRAutoBlogger_Logger::instance()->error( 'Daily generation FAILED: ' . $e->getMessage(), 'scheduler' );
} finally {
$this->release_generation_lock();
}
@@ -177,10 +217,10 @@ class Autoblogger {
*/
public function on_collect_metrics(): void {
try {
$metrics = new Autoblogger_Metrics_Collector();
$metrics = new PRAutoBlogger_Metrics_Collector();
$metrics->collect_all();
} catch ( \Exception $e ) {
Autoblogger_Logger::instance()->error( 'Metrics collection FAILED: ' . $e->getMessage(), 'metrics' );
PRAutoBlogger_Logger::instance()->error( 'Metrics collection FAILED: ' . $e->getMessage(), 'metrics' );
}
}
@@ -195,23 +235,23 @@ class Autoblogger {
* @return void
*/
public function on_ajax_generate_now(): void {
check_ajax_referer( 'autoblogger_generate_now', 'nonce' );
check_ajax_referer( 'prautoblogger_generate_now', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
return;
}
if ( ! $this->acquire_generation_lock() ) {
wp_send_json_error( [ 'message' => __( 'A generation run is already in progress.', 'autoblogger' ) ] );
wp_send_json_error( [ 'message' => __( 'A generation run is already in progress.', 'prautoblogger' ) ] );
return;
}
try {
$results = ( new Autoblogger_Pipeline_Runner() )->run();
$results = ( new PRAutoBlogger_Pipeline_Runner() )->run();
wp_send_json_success( $results );
} catch ( \Exception $e ) {
Autoblogger_Logger::instance()->error( 'Manual generation FAILED: ' . $e->getMessage(), 'pipeline' );
PRAutoBlogger_Logger::instance()->error( 'Manual generation FAILED: ' . $e->getMessage(), 'pipeline' );
wp_send_json_error( [ 'message' => $e->getMessage() ] );
} finally {
$this->release_generation_lock();
@@ -220,9 +260,9 @@ class Autoblogger {
/** AJAX handler: test API connections (OpenRouter, Reddit). */
public function on_ajax_test_connection(): void {
check_ajax_referer( 'autoblogger_test_connection', 'nonce' );
check_ajax_referer( 'prautoblogger_test_connection', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
return;
}
@@ -230,16 +270,16 @@ class Autoblogger {
$results = [];
if ( 'openrouter' === $service || 'all' === $service ) {
$llm = new Autoblogger_OpenRouter_Provider();
$llm = new PRAutoBlogger_OpenRouter_Provider();
$results['openrouter'] = $llm->validate_credentials()
? [ 'status' => 'ok', 'message' => __( 'OpenRouter connected.', 'autoblogger' ) ]
: [ 'status' => 'error', 'message' => __( 'OpenRouter connection failed. Check your API key.', 'autoblogger' ) ];
? [ 'status' => 'ok', 'message' => __( 'OpenRouter connected.', 'prautoblogger' ) ]
: [ 'status' => 'error', 'message' => __( 'OpenRouter connection failed. Check your API key.', 'prautoblogger' ) ];
}
if ( 'reddit' === $service || 'all' === $service ) {
$reddit = new Autoblogger_Reddit_Provider();
$reddit = new PRAutoBlogger_Reddit_Provider();
$results['reddit'] = $reddit->validate_credentials()
? [ 'status' => 'ok', 'message' => __( 'Reddit API connected.', 'autoblogger' ) ]
: [ 'status' => 'error', 'message' => __( 'Reddit connection failed. Check client ID/secret.', 'autoblogger' ) ];
? [ 'status' => 'ok', 'message' => __( 'Reddit API connected.', 'prautoblogger' ) ]
: [ 'status' => 'error', 'message' => __( 'Reddit connection failed. Check client ID/secret.', 'prautoblogger' ) ];
}
wp_send_json_success( $results );
}
@@ -255,7 +295,7 @@ class Autoblogger {
private function acquire_generation_lock(): bool {
global $wpdb;
$lock_name = 'autoblogger_generation_lock';
$lock_name = 'prautoblogger_generation_lock';
$now = (string) time();
// Clean up expired locks older than 1 hour — prevents permanent deadlock.
@@ -290,7 +330,7 @@ class Autoblogger {
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name = %s",
'autoblogger_generation_lock'
'prautoblogger_generation_lock'
)
);
}

View File

@@ -8,21 +8,21 @@ declare(strict_types=1);
* Can approve, request revisions (and produce them), or reject content.
* Rejected content is saved as a draft for human review.
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 5).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 5).
* Dependencies: LLM_Provider_Interface, Cost_Tracker.
*
* @see core/class-content-generator.php — Produces the content we review.
* @see core/class-publisher.php — Publishes approved content.
* @see ARCHITECTURE.md — Data flow step 5.
*/
class Autoblogger_Chief_Editor {
class PRAutoBlogger_Chief_Editor {
private Autoblogger_LLM_Provider_Interface $llm;
private Autoblogger_Cost_Tracker $cost_tracker;
private PRAutoBlogger_LLM_Provider_Interface $llm;
private PRAutoBlogger_Cost_Tracker $cost_tracker;
public function __construct(
Autoblogger_LLM_Provider_Interface $llm,
Autoblogger_Cost_Tracker $cost_tracker
PRAutoBlogger_LLM_Provider_Interface $llm,
PRAutoBlogger_Cost_Tracker $cost_tracker
) {
$this->llm = $llm;
$this->cost_tracker = $cost_tracker;
@@ -38,13 +38,13 @@ class Autoblogger_Chief_Editor {
* Side effects: LLM API call(s), cost logging.
*
* @param string $content The generated HTML content.
* @param Autoblogger_Article_Idea $idea The original article idea.
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
*
* @return Autoblogger_Editorial_Review
* @return PRAutoBlogger_Editorial_Review
*/
public function review( string $content, Autoblogger_Article_Idea $idea ): Autoblogger_Editorial_Review {
$model = get_option( 'autoblogger_editor_model', AUTOBLOGGER_DEFAULT_EDITOR_MODEL );
$niche = get_option( 'autoblogger_niche_description', '' );
public function review( string $content, PRAutoBlogger_Article_Idea $idea ): PRAutoBlogger_Editorial_Review {
$model = get_option( 'prautoblogger_editor_model', PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL );
$niche = get_option( 'prautoblogger_niche_description', '' );
$system_prompt = $this->build_system_prompt( $niche );
$user_prompt = $this->build_review_prompt( $content, $idea );
@@ -114,11 +114,11 @@ class Autoblogger_Chief_Editor {
/**
* @param string $content
* @param Autoblogger_Article_Idea $idea
* @param PRAutoBlogger_Article_Idea $idea
*
* @return string
*/
private function build_review_prompt( string $content, Autoblogger_Article_Idea $idea ): string {
private function build_review_prompt( string $content, PRAutoBlogger_Article_Idea $idea ): string {
return sprintf(
"Review this article draft:\n\n" .
"ORIGINAL BRIEF:\n" .
@@ -142,14 +142,14 @@ class Autoblogger_Chief_Editor {
*
* @param string $content JSON response from LLM.
*
* @return Autoblogger_Editorial_Review
* @return PRAutoBlogger_Editorial_Review
*/
private function parse_review_response( string $content ): Autoblogger_Editorial_Review {
private function parse_review_response( string $content ): PRAutoBlogger_Editorial_Review {
$data = json_decode( $content, true );
if ( ! is_array( $data ) || ! isset( $data['verdict'] ) ) {
Autoblogger_Logger::instance()->error( 'Chief editor response was not valid JSON. Defaulting to rejected.', 'editor' );
return new Autoblogger_Editorial_Review( [
PRAutoBlogger_Logger::instance()->error( 'Chief editor response was not valid JSON. Defaulting to rejected.', 'editor' );
return new PRAutoBlogger_Editorial_Review( [
'verdict' => 'rejected',
'notes' => 'Editor response could not be parsed.',
'quality_score' => 0.0,
@@ -163,7 +163,7 @@ class Autoblogger_Chief_Editor {
$verdict = 'rejected';
}
return new Autoblogger_Editorial_Review( [
return new PRAutoBlogger_Editorial_Review( [
'verdict' => $verdict,
'notes' => sanitize_textarea_field( $data['notes'] ?? '' ),
'revised_content' => isset( $data['revised_content'] ) ? wp_kses_post( $data['revised_content'] ) : null,

View File

@@ -8,7 +8,7 @@ declare(strict_types=1);
* Uses a cheap model (configurable) to keep analysis costs low.
* Batches source data into manageable chunks for the LLM context window.
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 2).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 2).
* Dependencies: LLM_Provider_Interface, Cost_Tracker, WordPress $wpdb.
*
* @see providers/interface-llm-provider.php — LLM used for analysis.
@@ -16,14 +16,14 @@ declare(strict_types=1);
* @see core/class-idea-scorer.php — Consumes analysis results.
* @see ARCHITECTURE.md — Data flow step 2.
*/
class Autoblogger_Content_Analyzer {
class PRAutoBlogger_Content_Analyzer {
private Autoblogger_LLM_Provider_Interface $llm;
private Autoblogger_Cost_Tracker $cost_tracker;
private PRAutoBlogger_LLM_Provider_Interface $llm;
private PRAutoBlogger_Cost_Tracker $cost_tracker;
public function __construct(
Autoblogger_LLM_Provider_Interface $llm,
Autoblogger_Cost_Tracker $cost_tracker
PRAutoBlogger_LLM_Provider_Interface $llm,
PRAutoBlogger_Cost_Tracker $cost_tracker
) {
$this->llm = $llm;
$this->cost_tracker = $cost_tracker;
@@ -37,18 +37,18 @@ class Autoblogger_Content_Analyzer {
*
* Side effects: LLM API call, database reads/writes.
*
* @return Autoblogger_Analysis_Result[] Detected patterns.
* @return PRAutoBlogger_Analysis_Result[] Detected patterns.
*/
public function analyze_recent_data(): array {
$source_items = $this->fetch_recent_source_data();
if ( empty( $source_items ) ) {
Autoblogger_Logger::instance()->info( 'No new source data to analyze.', 'analyzer' );
PRAutoBlogger_Logger::instance()->info( 'No new source data to analyze.', 'analyzer' );
return [];
}
$niche = get_option( 'autoblogger_niche_description', '' );
$model = get_option( 'autoblogger_analysis_model', AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL );
$niche = get_option( 'prautoblogger_niche_description', '' );
$model = get_option( 'prautoblogger_analysis_model', PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL );
// Build the source data summary for the LLM (trim to avoid huge contexts).
$summary = $this->build_source_summary( $source_items );
@@ -98,7 +98,7 @@ class Autoblogger_Content_Analyzer {
*/
private function fetch_recent_source_data(): array {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_source_data';
$table = $wpdb->prefix . 'prautoblogger_source_data';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
return $wpdb->get_results(
@@ -153,7 +153,7 @@ class Autoblogger_Content_Analyzer {
*/
private function get_performance_context(): string {
global $wpdb;
$scores_table = $wpdb->prefix . 'autoblogger_content_scores';
$scores_table = $wpdb->prefix . 'prautoblogger_content_scores';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$top_posts = $wpdb->get_results(
@@ -239,12 +239,12 @@ class Autoblogger_Content_Analyzer {
* @param string $content LLM response content (JSON).
* @param array<int, array<string, mixed>> $source_items Original source data for ID mapping.
*
* @return Autoblogger_Analysis_Result[]
* @return PRAutoBlogger_Analysis_Result[]
*/
private function parse_analysis_response( string $content, array $source_items ): array {
$data = json_decode( $content, true );
if ( ! isset( $data['patterns'] ) || ! is_array( $data['patterns'] ) ) {
Autoblogger_Logger::instance()->error( 'Analysis response was not valid JSON or missing patterns key.', 'analyzer' );
PRAutoBlogger_Logger::instance()->error( 'Analysis response was not valid JSON or missing patterns key.', 'analyzer' );
return [];
}
@@ -252,7 +252,7 @@ class Autoblogger_Content_Analyzer {
$results = [];
foreach ( $data['patterns'] as $pattern ) {
$results[] = new Autoblogger_Analysis_Result( [
$results[] = new PRAutoBlogger_Analysis_Result( [
'analysis_type' => sanitize_text_field( $pattern['type'] ?? 'question' ),
'topic' => sanitize_text_field( $pattern['topic'] ?? '' ),
'summary' => sanitize_textarea_field( $pattern['summary'] ?? '' ),
@@ -276,13 +276,13 @@ class Autoblogger_Content_Analyzer {
*
* Side effects: database inserts.
*
* @param Autoblogger_Analysis_Result[] $results
* @param PRAutoBlogger_Analysis_Result[] $results
*
* @return void
*/
private function store_results( array $results ): void {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_analysis_results';
$table = $wpdb->prefix . 'prautoblogger_analysis_results';
foreach ( $results as $result ) {
$row = $result->to_db_row();

View File

@@ -8,21 +8,21 @@ declare(strict_types=1);
* - single_pass: One LLM call produces the complete article (cheaper, faster).
* - multi_step: Outline → Draft → Polish pipeline (higher quality, more cost).
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 4).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 4).
* Dependencies: LLM_Provider_Interface, Cost_Tracker.
*
* @see core/class-chief-editor.php — Reviews output of this class.
* @see providers/interface-llm-provider.php — LLM used for generation.
* @see ARCHITECTURE.md — Data flow step 4.
*/
class Autoblogger_Content_Generator {
class PRAutoBlogger_Content_Generator {
private Autoblogger_LLM_Provider_Interface $llm;
private Autoblogger_Cost_Tracker $cost_tracker;
private PRAutoBlogger_LLM_Provider_Interface $llm;
private PRAutoBlogger_Cost_Tracker $cost_tracker;
public function __construct(
Autoblogger_LLM_Provider_Interface $llm,
Autoblogger_Cost_Tracker $cost_tracker
PRAutoBlogger_LLM_Provider_Interface $llm,
PRAutoBlogger_Cost_Tracker $cost_tracker
) {
$this->llm = $llm;
$this->cost_tracker = $cost_tracker;
@@ -35,23 +35,23 @@ class Autoblogger_Content_Generator {
*
* Side effects: LLM API calls (1-3 depending on mode), cost logging.
*
* @param Autoblogger_Article_Idea $idea The scored idea to generate content for.
* @param PRAutoBlogger_Article_Idea $idea The scored idea to generate content for.
*
* @return string Generated HTML content.
*
* @throws \RuntimeException On generation failure.
*/
public function generate( Autoblogger_Article_Idea $idea ): string {
$mode = get_option( 'autoblogger_writing_pipeline', 'multi_step' );
public function generate( PRAutoBlogger_Article_Idea $idea ): string {
$mode = get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
$request = new Autoblogger_Content_Request(
$request = new PRAutoBlogger_Content_Request(
$idea,
$mode,
get_option( 'autoblogger_tone', 'informational' ),
absint( get_option( 'autoblogger_min_word_count', 800 ) ),
absint( get_option( 'autoblogger_max_word_count', 2000 ) ),
get_option( 'autoblogger_niche_description', '' ),
json_decode( get_option( 'autoblogger_topic_exclusions', '[]' ), true ) ?: []
get_option( 'prautoblogger_tone', 'informational' ),
absint( get_option( 'prautoblogger_min_word_count', 800 ) ),
absint( get_option( 'prautoblogger_max_word_count', 2000 ) ),
get_option( 'prautoblogger_niche_description', '' ),
json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?: []
);
if ( 'single_pass' === $mode ) {
@@ -64,12 +64,12 @@ class Autoblogger_Content_Generator {
/**
* Single-pass generation: one LLM call produces the complete article.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
*
* @return string Generated HTML content.
*/
private function generate_single_pass( Autoblogger_Content_Request $request ): string {
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
private function generate_single_pass( PRAutoBlogger_Content_Request $request ): string {
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
$idea = $request->get_idea();
$system = $this->build_writer_system_prompt( $request );
@@ -102,12 +102,12 @@ class Autoblogger_Content_Generator {
/**
* Multi-step generation: Outline → Draft → Polish.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
*
* @return string Generated HTML content.
*/
private function generate_multi_step( Autoblogger_Content_Request $request ): string {
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
private function generate_multi_step( PRAutoBlogger_Content_Request $request ): string {
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
// Step 1: Generate outline.
$outline = $this->stage_outline( $request, $model );
@@ -124,12 +124,12 @@ class Autoblogger_Content_Generator {
/**
* Multi-step stage 1: Generate an article outline.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
* @param string $model
*
* @return string The outline text.
*/
private function stage_outline( Autoblogger_Content_Request $request, string $model ): string {
private function stage_outline( PRAutoBlogger_Content_Request $request, string $model ): string {
$idea = $request->get_idea();
$prompt = sprintf(
@@ -168,13 +168,13 @@ class Autoblogger_Content_Generator {
/**
* Multi-step stage 2: Write the full draft from the outline.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
* @param string $outline
* @param string $model
*
* @return string Draft HTML content.
*/
private function stage_draft( Autoblogger_Content_Request $request, string $outline, string $model ): string {
private function stage_draft( PRAutoBlogger_Content_Request $request, string $outline, string $model ): string {
$prompt = sprintf(
"Using this outline, write the full blog post in HTML format.\n\n" .
"OUTLINE:\n%s\n\n" .
@@ -213,13 +213,13 @@ class Autoblogger_Content_Generator {
/**
* Multi-step stage 3: Polish and refine the draft.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
* @param string $draft
* @param string $model
*
* @return string Polished HTML content.
*/
private function stage_polish( Autoblogger_Content_Request $request, string $draft, string $model ): string {
private function stage_polish( PRAutoBlogger_Content_Request $request, string $draft, string $model ): string {
$prompt = "Review and polish this blog post draft. Improve:\n" .
"1. Flow and readability\n" .
"2. SEO optimization (headings, keyword placement)\n" .
@@ -249,11 +249,11 @@ class Autoblogger_Content_Generator {
/**
* Build the system prompt shared across all writing stages.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
*
* @return string
*/
private function build_writer_system_prompt( Autoblogger_Content_Request $request ): string {
private function build_writer_system_prompt( PRAutoBlogger_Content_Request $request ): string {
$niche = $request->get_niche_description();
$prompt = "You are an expert blog writer";
if ( '' !== $niche ) {
@@ -268,11 +268,11 @@ class Autoblogger_Content_Generator {
/**
* Build the user prompt for single-pass generation.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
*
* @return string
*/
private function build_single_pass_prompt( Autoblogger_Content_Request $request ): string {
private function build_single_pass_prompt( PRAutoBlogger_Content_Request $request ): string {
$idea = $request->get_idea();
return sprintf(

View File

@@ -11,15 +11,15 @@ declare(strict_types=1);
*
* Triggered by: Every class that makes an LLM API call (Content_Analyzer,
* Content_Generator, Chief_Editor, Metrics_Collector).
* Dependencies: WordPress $wpdb, Autoblogger_OpenRouter_Provider (for cost estimation).
* Dependencies: WordPress $wpdb, PRAutoBlogger_OpenRouter_Provider (for cost estimation).
*
* @see core/class-content-analyzer.php — Calls log_api_call() after analysis.
* @see core/class-content-generator.php — Calls log_api_call() after each stage.
* @see core/class-chief-editor.php — Calls log_api_call() after review.
* @see admin/class-metrics-page.php — Displays cost data from this class.
* @see ARCHITECTURE.md — ab_generation_log table schema.
* @see ARCHITECTURE.md — prab_generation_log table schema.
*/
class Autoblogger_Cost_Tracker {
class PRAutoBlogger_Cost_Tracker {
/**
* Running cost for the current pipeline execution.
@@ -59,7 +59,7 @@ class Autoblogger_Cost_Tracker {
/**
* Log an API call with its token usage and estimated cost.
*
* Side effects: database insert into ab_generation_log.
* Side effects: database insert into prab_generation_log.
*
* @param int|null $post_id WordPress post ID (null during generation, set later).
* @param string $stage Pipeline stage: 'analysis', 'outline', 'draft', 'polish', 'review'.
@@ -82,12 +82,12 @@ class Autoblogger_Cost_Tracker {
string $response_status = 'success',
string $error_message = ''
): void {
$llm = new Autoblogger_OpenRouter_Provider();
$llm = new PRAutoBlogger_OpenRouter_Provider();
$cost = $llm->estimate_cost( $model, $prompt_tokens, $completion_tokens );
$this->current_run_cost += $cost;
$log_entry = new Autoblogger_Generation_Log( [
$log_entry = new PRAutoBlogger_Generation_Log( [
'post_id' => $post_id,
'run_id' => $this->run_id,
'stage' => $stage,
@@ -102,13 +102,13 @@ class Autoblogger_Cost_Tracker {
] );
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert( $table, $log_entry->to_db_row() );
if ( false === $wpdb->insert_id ) {
Autoblogger_Logger::instance()->error( 'Failed to log API call: ' . $wpdb->last_error, 'cost-tracker' );
PRAutoBlogger_Logger::instance()->error( 'Failed to log API call: ' . $wpdb->last_error, 'cost-tracker' );
}
}
@@ -121,7 +121,7 @@ class Autoblogger_Cost_Tracker {
* @return bool True if monthly spend >= configured budget.
*/
public function is_budget_exceeded(): bool {
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
if ( $budget <= 0 ) {
// Budget of 0 means no limit.
return false;
@@ -141,7 +141,7 @@ class Autoblogger_Cost_Tracker {
*/
public function get_monthly_spend(): float {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
$first_of_month = gmdate( 'Y-m-01 00:00:00' );
@@ -174,7 +174,7 @@ class Autoblogger_Cost_Tracker {
*/
public function get_daily_spend( int $days = 30 ): array {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
$start_date = gmdate( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) );
@@ -209,7 +209,7 @@ class Autoblogger_Cost_Tracker {
*/
public function get_spend_by_stage( string $start_date, string $end_date ): array {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$results = $wpdb->get_results(
@@ -245,7 +245,7 @@ class Autoblogger_Cost_Tracker {
* @return float Percentage (0-100+). Can exceed 100 if overspent.
*/
public function get_budget_utilization(): float {
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
if ( $budget <= 0 ) {
return 0.0;
}

View File

@@ -7,13 +7,13 @@ declare(strict_types=1);
* Handles OAuth2 service account authentication and the GA4 Data API.
* Decoupled from metrics collection logic for testability and reusability.
*
* Triggered by: Autoblogger_Metrics_Collector::fetch_ga4_data() calls methods here.
* Dependencies: Autoblogger_Encryption (for credentials decryption), wp_remote_post/get().
* Triggered by: PRAutoBlogger_Metrics_Collector::fetch_ga4_data() calls methods here.
* Dependencies: PRAutoBlogger_Encryption (for credentials decryption), wp_remote_post/get().
*
* @see core/class-metrics-collector.php — Instantiates this class.
* @see ARCHITECTURE.md — External API integrations table.
*/
class Autoblogger_GA4_Client {
class PRAutoBlogger_GA4_Client {
/**
* Fetch Google Analytics 4 data for given posts.
@@ -27,17 +27,17 @@ class Autoblogger_GA4_Client {
* @return array<int, array{pageviews: int, avg_time_on_page: float, bounce_rate: float}>
*/
public function fetch_data( array $posts ): array {
$property_id = get_option( 'autoblogger_ga4_property_id', '' );
$property_id = get_option( 'prautoblogger_ga4_property_id', '' );
if ( '' === $property_id ) {
return [];
}
$credentials_encrypted = get_option( 'autoblogger_ga4_credentials_json', '' );
$credentials_encrypted = get_option( 'prautoblogger_ga4_credentials_json', '' );
if ( '' === $credentials_encrypted ) {
return [];
}
$credentials = Autoblogger_Encryption::decrypt( $credentials_encrypted );
$credentials = PRAutoBlogger_Encryption::decrypt( $credentials_encrypted );
if ( '' === $credentials ) {
return [];
}
@@ -61,7 +61,7 @@ class Autoblogger_GA4_Client {
// Using the REST API directly since we don't want a full Google SDK dependency.
$access_token = $this->get_access_token( $credentials );
if ( '' === $access_token ) {
Autoblogger_Logger::instance()->error( 'GA4: Failed to get access token. Possible causes: invalid service account JSON, expired or revoked private key, or network error reaching oauth2.googleapis.com. Verify credentials in AutoBlogger → Settings.', 'ga4' );
PRAutoBlogger_Logger::instance()->error( 'GA4: Failed to get access token. Possible causes: invalid service account JSON, expired or revoked private key, or network error reaching oauth2.googleapis.com. Verify credentials in PRAutoBlogger → Settings.', 'ga4' );
return [];
}
@@ -94,14 +94,14 @@ class Autoblogger_GA4_Client {
);
if ( is_wp_error( $response ) ) {
Autoblogger_Logger::instance()->error( 'GA4 API request failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
PRAutoBlogger_Logger::instance()->error( 'GA4 API request failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
return [];
}
$ga4_status = wp_remote_retrieve_response_code( $response );
if ( 200 !== $ga4_status ) {
$ga4_body = wp_remote_retrieve_body( $response );
Autoblogger_Logger::instance()->error(
PRAutoBlogger_Logger::instance()->error(
sprintf( 'GA4 API request failed (HTTP %d): %s', $ga4_status, substr( $ga4_body, 0, 500 ) ),
'ga4'
);
@@ -159,7 +159,7 @@ class Autoblogger_GA4_Client {
$key = openssl_pkey_get_private( $creds['private_key'] );
if ( false === $key ) {
Autoblogger_Logger::instance()->error( 'GA4: Failed to parse private key from service account credentials. The key may be malformed or the OpenSSL extension may be misconfigured.', 'ga4' );
PRAutoBlogger_Logger::instance()->error( 'GA4: Failed to parse private key from service account credentials. The key may be malformed or the OpenSSL extension may be misconfigured.', 'ga4' );
return '';
}
@@ -181,7 +181,7 @@ class Autoblogger_GA4_Client {
);
if ( is_wp_error( $response ) ) {
Autoblogger_Logger::instance()->error( 'GA4: OAuth token exchange failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
PRAutoBlogger_Logger::instance()->error( 'GA4: OAuth token exchange failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
return '';
}
@@ -190,7 +190,7 @@ class Autoblogger_GA4_Client {
if ( 200 !== $token_status || ! isset( $data['access_token'] ) ) {
$error_desc = $data['error_description'] ?? $data['error'] ?? 'unknown';
Autoblogger_Logger::instance()->error(
PRAutoBlogger_Logger::instance()->error(
sprintf( 'GA4: OAuth token exchange failed (HTTP %d): %s', $token_status, $error_desc ),
'ga4'
);

View File

@@ -8,25 +8,25 @@ declare(strict_types=1);
* scores ideas by relevance, frequency, and freshness, then returns
* the top N ideas matching the daily target.
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 3).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 3).
* Dependencies: WordPress $wpdb, WP_Query.
*
* @see core/class-content-analyzer.php — Produces the analysis results we consume.
* @see core/class-content-generator.php — Consumes the ranked ideas we produce.
* @see ARCHITECTURE.md — Data flow step 3.
*/
class Autoblogger_Idea_Scorer {
class PRAutoBlogger_Idea_Scorer {
/**
* Score, deduplicate, and rank article ideas.
*
* @param Autoblogger_Analysis_Result[] $analysis_results Raw analysis output.
* @param PRAutoBlogger_Analysis_Result[] $analysis_results Raw analysis output.
* @param int $target_count Number of ideas to return.
*
* @return Autoblogger_Article_Idea[] Top-ranked ideas, sorted by score descending.
* @return PRAutoBlogger_Article_Idea[] Top-ranked ideas, sorted by score descending.
*/
public function score_and_rank( array $analysis_results, int $target_count ): array {
$exclusions = json_decode( get_option( 'autoblogger_topic_exclusions', '[]' ), true ) ?: [];
$exclusions = json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?: [];
$ideas = [];
foreach ( $analysis_results as $result ) {
@@ -42,7 +42,7 @@ class Autoblogger_Idea_Scorer {
$metadata = $result->get_metadata() ?? [];
$ideas[] = new Autoblogger_Article_Idea( [
$ideas[] = new PRAutoBlogger_Article_Idea( [
'topic' => $result->get_topic(),
'article_type' => $this->map_type( $result->get_analysis_type() ),
'suggested_title' => $metadata['suggested_title'] ?? $result->get_topic(),
@@ -56,7 +56,7 @@ class Autoblogger_Idea_Scorer {
}
// Sort by score descending.
usort( $ideas, static function ( Autoblogger_Article_Idea $a, Autoblogger_Article_Idea $b ): int {
usort( $ideas, static function ( PRAutoBlogger_Article_Idea $a, PRAutoBlogger_Article_Idea $b ): int {
return $b->get_score() <=> $a->get_score();
} );
@@ -68,11 +68,11 @@ class Autoblogger_Idea_Scorer {
*
* Factors: LLM relevance score (50%), frequency (30%), recency bonus (20%).
*
* @param Autoblogger_Analysis_Result $result
* @param PRAutoBlogger_Analysis_Result $result
*
* @return float Score between 0 and 1.
*/
private function compute_score( Autoblogger_Analysis_Result $result ): float {
private function compute_score( PRAutoBlogger_Analysis_Result $result ): float {
$relevance = min( $result->get_relevance_score(), 1.0 );
$frequency = min( $result->get_frequency() / 10.0, 1.0 );
@@ -114,7 +114,7 @@ class Autoblogger_Idea_Scorer {
$query = new \WP_Query( [
'post_type' => 'post',
'post_status' => [ 'publish', 'draft', 'pending' ],
'meta_key' => '_autoblogger_generated',
'meta_key' => '_prautoblogger_generated',
'meta_value' => '1',
's' => $topic,
'posts_per_page' => 1,

View File

@@ -6,7 +6,7 @@ declare(strict_types=1);
*
* Writes log entries to a custom database table and optionally forwards
* to PHP's error_log() for server-level monitoring. The log level is
* user-configurable via Settings → AutoBlogger → General.
* user-configurable via Settings → PRAutoBlogger → General.
*
* Levels (ascending verbosity): error → warning → info → debug.
* Setting the level to "info" captures error, warning, and info — not debug.
@@ -15,10 +15,10 @@ declare(strict_types=1);
* Dependencies: WordPress $wpdb.
*
* @see admin/class-log-viewer.php — Reads and displays log entries.
* @see class-activator.php — Creates the ab_event_log table.
* @see class-activator.php — Creates the prab_event_log table.
* @see ARCHITECTURE.md — Logging architecture section.
*/
class Autoblogger_Logger {
class PRAutoBlogger_Logger {
/** Log levels, lower = more severe. */
public const LEVEL_ERROR = 0;
@@ -47,7 +47,7 @@ class Autoblogger_Logger {
private int $threshold;
private function __construct() {
$setting = get_option( 'autoblogger_log_level', 'info' );
$setting = get_option( 'prautoblogger_log_level', 'info' );
$this->threshold = self::LEVEL_MAP[ $setting ] ?? self::LEVEL_INFO;
}
@@ -122,11 +122,11 @@ class Autoblogger_Logger {
// Always forward errors to PHP error_log for server-level monitoring.
if ( $level <= self::LEVEL_WARNING ) {
error_log( sprintf( '[AutoBlogger][%s] %s — %s', strtoupper( $label ), $context, $message ) );
error_log( sprintf( '[PRAutoBlogger][%s] %s — %s', strtoupper( $label ), $context, $message ) );
}
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_event_log';
$table = $wpdb->prefix . 'prautoblogger_event_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert( $table, [
@@ -155,7 +155,7 @@ class Autoblogger_Logger {
int $per_page = 50
): array {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_event_log';
$table = $wpdb->prefix . 'prautoblogger_event_log';
$where = [];
$params = [];
@@ -207,7 +207,7 @@ class Autoblogger_Logger {
*/
public static function prune( int $days = 30 ): int {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_event_log';
$table = $wpdb->prefix . 'prautoblogger_event_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
return (int) $wpdb->query(

View File

@@ -5,21 +5,21 @@ declare(strict_types=1);
* Collects post performance metrics from WordPress native data and Google Analytics 4.
*
* Runs on a separate cron schedule (every 6 hours) to gather pageviews, bounce rate,
* time on page, and comments for AutoBlogger-generated posts. Computes a composite
* time on page, and comments for PRAutoBlogger-generated posts. Computes a composite
* "content score" that feeds back into the analysis engine for self-improvement.
*
* Triggered by: WP-Cron hook `autoblogger_collect_metrics` (every 6 hours).
* Dependencies: WordPress $wpdb, Autoblogger_GA4_Client (for GA4 API calls).
* Triggered by: WP-Cron hook `prautoblogger_collect_metrics` (every 6 hours).
* Dependencies: WordPress $wpdb, PRAutoBlogger_GA4_Client (for GA4 API calls).
*
* @see core/class-ga4-client.php — Handles GA4 API authentication and requests.
* @see core/class-content-analyzer.php — Reads scores for self-improvement loop.
* @see admin/class-metrics-page.php — Displays collected metrics.
* @see ARCHITECTURE.md — Data flow step 7.
*/
class Autoblogger_Metrics_Collector {
class PRAutoBlogger_Metrics_Collector {
/**
* Collect metrics for all AutoBlogger-generated posts.
* Collect metrics for all PRAutoBlogger-generated posts.
*
* Gathers WordPress native metrics (comments) and GA4 data (if configured),
* then computes composite scores.
@@ -34,7 +34,7 @@ class Autoblogger_Metrics_Collector {
return 0;
}
$ga4_client = new Autoblogger_GA4_Client();
$ga4_client = new PRAutoBlogger_GA4_Client();
$ga4_data = $ga4_client->fetch_data( $posts );
$scored = 0;
@@ -54,12 +54,12 @@ class Autoblogger_Metrics_Collector {
$scored++;
}
Autoblogger_Logger::instance()->info( sprintf( 'Metrics collected for %d posts.', $scored ), 'metrics' );
PRAutoBlogger_Logger::instance()->info( sprintf( 'Metrics collected for %d posts.', $scored ), 'metrics' );
return $scored;
}
/**
* Get all AutoBlogger-generated published posts.
* Get all PRAutoBlogger-generated published posts.
*
* @return \WP_Post[]
*/
@@ -67,7 +67,7 @@ class Autoblogger_Metrics_Collector {
$query = new \WP_Query( [
'post_type' => 'post',
'post_status' => 'publish',
'meta_key' => '_autoblogger_generated',
'meta_key' => '_prautoblogger_generated',
'meta_value' => '1',
'posts_per_page' => 100,
'orderby' => 'date',
@@ -89,7 +89,7 @@ class Autoblogger_Metrics_Collector {
// Post may have been deleted between the WP_Query and this call.
if ( null === $post ) {
Autoblogger_Logger::instance()->debug( sprintf( 'Metrics: Post ID %d no longer exists, skipping.', $post_id ), 'metrics' );
PRAutoBlogger_Logger::instance()->debug( sprintf( 'Metrics: Post ID %d no longer exists, skipping.', $post_id ), 'metrics' );
return null;
}
@@ -160,9 +160,9 @@ class Autoblogger_Metrics_Collector {
*/
private function store_score( int $post_id, array $wp_metrics, array $ga_metrics, array $score_data ): void {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_content_scores';
$table = $wpdb->prefix . 'prautoblogger_content_scores';
$entry = new Autoblogger_Content_Score( [
$entry = new PRAutoBlogger_Content_Score( [
'post_id' => $post_id,
'pageviews' => $ga_metrics['pageviews'] ?? 0,
'avg_time_on_page' => $ga_metrics['avg_time_on_page'] ?? 0.0,

View File

@@ -7,14 +7,14 @@ declare(strict_types=1);
* Orchestrates: Budget check → Collect source data → Analyze patterns →
* Score ideas → Generate content → Editorial review → Publish.
*
* Triggered by: Autoblogger::on_daily_generation() and Autoblogger::on_ajax_generate_now().
* Triggered by: PRAutoBlogger::on_daily_generation() and PRAutoBlogger::on_ajax_generate_now().
* Dependencies: All core content classes (Collector, Analyzer, Scorer, Generator, Editor, Publisher).
*
* @see class-autoblogger.php — Instantiates this class and calls run().
* @see class-prautoblogger.php — Instantiates this class and calls run().
* @see core/class-cost-tracker.php — Tracks cumulative API costs throughout the run.
* @see ARCHITECTURE.md — Data flow diagram showing pipeline steps.
*/
class Autoblogger_Pipeline_Runner {
class PRAutoBlogger_Pipeline_Runner {
/**
* Execute the full content generation pipeline.
@@ -29,7 +29,7 @@ class Autoblogger_Pipeline_Runner {
* @throws \RuntimeException If budget is exceeded or critical step fails.
*/
public function run(): array {
$cost_tracker = new Autoblogger_Cost_Tracker();
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
// Tag all log entries in this run with a unique ID so link_generation_logs()
// can accurately attribute costs to the correct post in batch runs.
@@ -38,35 +38,35 @@ class Autoblogger_Pipeline_Runner {
// Hard stop if monthly budget is exceeded.
if ( $cost_tracker->is_budget_exceeded() ) {
throw new \RuntimeException(
__( 'Monthly API budget exceeded. Generation halted.', 'autoblogger' )
__( 'Monthly API budget exceeded. Generation halted.', 'prautoblogger' )
);
}
$target_count = absint( get_option( 'autoblogger_daily_article_target', 1 ) );
$target_count = absint( get_option( 'prautoblogger_daily_article_target', 1 ) );
// Step 1: Collect source data.
$collector = new Autoblogger_Source_Collector();
$collector = new PRAutoBlogger_Source_Collector();
$collector->collect_from_all_sources();
// Step 2: Analyze collected data for patterns.
$llm = new Autoblogger_OpenRouter_Provider();
$analyzer = new Autoblogger_Content_Analyzer( $llm, $cost_tracker );
$llm = new PRAutoBlogger_OpenRouter_Provider();
$analyzer = new PRAutoBlogger_Content_Analyzer( $llm, $cost_tracker );
$analysis = $analyzer->analyze_recent_data();
// Step 3: Score and deduplicate ideas.
$scorer = new Autoblogger_Idea_Scorer();
$scorer = new PRAutoBlogger_Idea_Scorer();
$ideas = $scorer->score_and_rank( $analysis, $target_count );
if ( empty( $ideas ) ) {
Autoblogger_Logger::instance()->info( 'No viable article ideas found. Skipping generation.', 'pipeline' );
PRAutoBlogger_Logger::instance()->info( 'No viable article ideas found. Skipping generation.', 'pipeline' );
return [ 'generated' => 0, 'published' => 0, 'rejected' => 0, 'cost' => 0.0 ];
}
// Step 4 & 5: Generate content and run through editor for each idea.
$generator = new Autoblogger_Content_Generator( $llm, $cost_tracker );
$editor = new Autoblogger_Chief_Editor( $llm, $cost_tracker );
$publisher = new Autoblogger_Publisher();
$auto_publish = in_array( get_option( 'autoblogger_auto_publish', '0' ), [ '1', 'yes' ], true );
$generator = new PRAutoBlogger_Content_Generator( $llm, $cost_tracker );
$editor = new PRAutoBlogger_Chief_Editor( $llm, $cost_tracker );
$publisher = new PRAutoBlogger_Publisher();
$auto_publish = in_array( get_option( 'prautoblogger_auto_publish', '0' ), [ '1', 'yes' ], true );
$generated = 0;
$published = 0;
@@ -75,7 +75,7 @@ class Autoblogger_Pipeline_Runner {
foreach ( $ideas as $idea ) {
// Re-check budget before each article — stop early if we hit the limit.
if ( $cost_tracker->is_budget_exceeded() ) {
Autoblogger_Logger::instance()->warning( 'Budget limit reached during generation. Stopping early.', 'pipeline' );
PRAutoBlogger_Logger::instance()->warning( 'Budget limit reached during generation. Stopping early.', 'pipeline' );
break;
}
@@ -103,17 +103,17 @@ class Autoblogger_Pipeline_Runner {
} else {
$publisher->save_as_draft( $content, $idea, $review, $cost_tracker->get_run_id() );
$rejected++;
Autoblogger_Logger::instance()->info( 'Article rejected by editor: ' . $idea->get_topic(), 'pipeline' );
PRAutoBlogger_Logger::instance()->info( 'Article rejected by editor: ' . $idea->get_topic(), 'pipeline' );
}
} catch ( \Exception $e ) {
Autoblogger_Logger::instance()->error( 'Failed to generate article for topic "' . $idea->get_topic() . '": ' . $e->getMessage(), 'pipeline' );
PRAutoBlogger_Logger::instance()->error( 'Failed to generate article for topic "' . $idea->get_topic() . '": ' . $e->getMessage(), 'pipeline' );
// Continue with next idea — don't let one failure stop the batch.
}
}
$total_cost = $cost_tracker->get_current_run_cost();
Autoblogger_Logger::instance()->info(
PRAutoBlogger_Logger::instance()->info(
sprintf( 'Pipeline complete: %d generated, %d published, %d rejected. Cost: $%.4f', $generated, $published, $rejected, $total_cost ),
'pipeline'
);

View File

@@ -7,14 +7,14 @@ declare(strict_types=1);
* Handles both publishing (editor-approved) and saving as draft (editor-rejected).
* Stores all generation metadata as post_meta for transparency and auditability.
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 6).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 6).
* Dependencies: WordPress wp_insert_post(), post meta API.
*
* @see core/class-chief-editor.php — Produces the editorial review we consume.
* @see core/class-content-generator.php — Produces the content we publish.
* @see ARCHITECTURE.md — Data flow step 6.
*/
class Autoblogger_Publisher {
class PRAutoBlogger_Publisher {
/**
* Publish an editor-approved article.
@@ -25,8 +25,8 @@ class Autoblogger_Publisher {
* Side effects: Creates a WordPress post, writes post_meta.
*
* @param string $content The final HTML content.
* @param Autoblogger_Article_Idea $idea The original article idea.
* @param Autoblogger_Editorial_Review $review The editor's review.
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
* @param PRAutoBlogger_Editorial_Review $review The editor's review.
* @param string|null $run_id Pipeline run ID for log linking.
*
* @return int The created post ID.
@@ -35,8 +35,8 @@ class Autoblogger_Publisher {
*/
public function publish(
string $content,
Autoblogger_Article_Idea $idea,
Autoblogger_Editorial_Review $review,
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
?string $run_id = null
): int {
return $this->create_post( $content, $idea, $review, 'publish', $run_id );
@@ -46,8 +46,8 @@ class Autoblogger_Publisher {
* Save an editor-rejected article as a draft for human review.
*
* @param string $content The generated HTML content (pre-revision).
* @param Autoblogger_Article_Idea $idea The original article idea.
* @param Autoblogger_Editorial_Review $review The editor's review with rejection notes.
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
* @param PRAutoBlogger_Editorial_Review $review The editor's review with rejection notes.
* @param string|null $run_id Pipeline run ID for log linking.
*
* @return int The created post ID.
@@ -56,8 +56,8 @@ class Autoblogger_Publisher {
*/
public function save_as_draft(
string $content,
Autoblogger_Article_Idea $idea,
Autoblogger_Editorial_Review $review,
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
?string $run_id = null
): int {
return $this->create_post( $content, $idea, $review, 'draft', $run_id );
@@ -67,8 +67,8 @@ class Autoblogger_Publisher {
* Create a WordPress post with generation metadata.
*
* @param string $content HTML content.
* @param Autoblogger_Article_Idea $idea Article idea.
* @param Autoblogger_Editorial_Review $review Editorial review.
* @param PRAutoBlogger_Article_Idea $idea Article idea.
* @param PRAutoBlogger_Editorial_Review $review Editorial review.
* @param string $post_status 'publish' or 'draft'.
* @param string|null $run_id Pipeline run ID for log linking.
*
@@ -78,8 +78,8 @@ class Autoblogger_Publisher {
*/
private function create_post(
string $content,
Autoblogger_Article_Idea $idea,
Autoblogger_Editorial_Review $review,
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
string $post_status,
?string $run_id = null
): int {
@@ -93,15 +93,15 @@ class Autoblogger_Publisher {
];
/**
* Fires before an AutoBlogger post is created.
* Fires before an PRAutoBlogger post is created.
*
* Listeners registered in: class-autoblogger.php (main loader).
* Listeners registered in: class-prautoblogger.php (main loader).
*
* @param array $post_data Post data array.
* @param Autoblogger_Article_Idea $idea The article idea.
* @param Autoblogger_Editorial_Review $review The editorial review.
* @param PRAutoBlogger_Article_Idea $idea The article idea.
* @param PRAutoBlogger_Editorial_Review $review The editorial review.
*/
$post_data = apply_filters( 'autoblogger_filter_post_data', $post_data, $idea, $review );
$post_data = apply_filters( 'prautoblogger_filter_post_data', $post_data, $idea, $review );
$post_id = wp_insert_post( $post_data, true );
@@ -109,7 +109,7 @@ class Autoblogger_Publisher {
throw new \RuntimeException(
sprintf(
/* translators: %s: WordPress error message */
__( 'Failed to create post: %s', 'autoblogger' ),
__( 'Failed to create post: %s', 'prautoblogger' ),
$post_id->get_error_message()
)
);
@@ -121,20 +121,20 @@ class Autoblogger_Publisher {
// Update generation log entries to reference this post.
$this->link_generation_logs( $post_id, $run_id );
Autoblogger_Logger::instance()->info(
PRAutoBlogger_Logger::instance()->info(
sprintf( 'Post created: ID=%d, status=%s, title="%s"', $post_id, $post_status, $idea->get_suggested_title() ),
'publisher'
);
/**
* Fires after an AutoBlogger post is successfully created.
* Fires after an PRAutoBlogger post is successfully created.
*
* @param int $post_id The new post ID.
* @param string $post_status 'publish' or 'draft'.
* @param Autoblogger_Article_Idea $idea The article idea.
* @param Autoblogger_Editorial_Review $review The editorial review.
* @param PRAutoBlogger_Article_Idea $idea The article idea.
* @param PRAutoBlogger_Editorial_Review $review The editorial review.
*/
do_action( 'autoblogger_post_created', $post_id, $post_status, $idea, $review );
do_action( 'prautoblogger_post_created', $post_id, $post_status, $idea, $review );
return $post_id;
}
@@ -142,32 +142,32 @@ class Autoblogger_Publisher {
/**
* Build post_meta array for generation metadata.
*
* @param Autoblogger_Article_Idea $idea
* @param Autoblogger_Editorial_Review $review
* @param PRAutoBlogger_Article_Idea $idea
* @param PRAutoBlogger_Editorial_Review $review
*
* @return array<string, mixed>
*/
private function build_meta(
Autoblogger_Article_Idea $idea,
Autoblogger_Editorial_Review $review
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review
): array {
$pipeline_mode = get_option( 'autoblogger_writing_pipeline', 'multi_step' );
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
$pipeline_mode = get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
return [
'_autoblogger_generated' => '1',
'_autoblogger_analysis_id' => $idea->get_analysis_id(),
'_autoblogger_source_ids' => wp_json_encode( $idea->get_source_ids() ),
'_autoblogger_model_used' => $model,
'_autoblogger_pipeline_mode' => $pipeline_mode,
'_autoblogger_editor_verdict' => $review->get_verdict(),
'_autoblogger_editor_notes' => $review->get_notes(),
'_autoblogger_quality_score' => $review->get_quality_score(),
'_autoblogger_seo_score' => $review->get_seo_score(),
'_autoblogger_generated_at' => gmdate( 'c' ),
'_autoblogger_topic' => $idea->get_topic(),
'_autoblogger_article_type' => $idea->get_article_type(),
'_autoblogger_target_keywords' => wp_json_encode( $idea->get_target_keywords() ),
'_prautoblogger_generated' => '1',
'_prautoblogger_analysis_id' => $idea->get_analysis_id(),
'_prautoblogger_source_ids' => wp_json_encode( $idea->get_source_ids() ),
'_prautoblogger_model_used' => $model,
'_prautoblogger_pipeline_mode' => $pipeline_mode,
'_prautoblogger_editor_verdict' => $review->get_verdict(),
'_prautoblogger_editor_notes' => $review->get_notes(),
'_prautoblogger_quality_score' => $review->get_quality_score(),
'_prautoblogger_seo_score' => $review->get_seo_score(),
'_prautoblogger_generated_at' => gmdate( 'c' ),
'_prautoblogger_topic' => $idea->get_topic(),
'_prautoblogger_article_type' => $idea->get_article_type(),
'_prautoblogger_target_keywords' => wp_json_encode( $idea->get_target_keywords() ),
];
}
@@ -175,11 +175,11 @@ class Autoblogger_Publisher {
* Assign category and tags to a generated post.
*
* @param int $post_id
* @param Autoblogger_Article_Idea $idea
* @param PRAutoBlogger_Article_Idea $idea
*
* @return void
*/
private function assign_taxonomy_terms( int $post_id, Autoblogger_Article_Idea $idea ): void {
private function assign_taxonomy_terms( int $post_id, PRAutoBlogger_Article_Idea $idea ): void {
$type_category_map = [
'guide' => 'Guides',
'solution' => 'Solutions',
@@ -222,7 +222,7 @@ class Autoblogger_Publisher {
*/
private function link_generation_logs( int $post_id, ?string $run_id = null ): void {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
if ( null !== $run_id && '' !== $run_id ) {
// Precise linking via run_id — no risk of cross-article misattribution.
@@ -253,7 +253,7 @@ class Autoblogger_Publisher {
* @return int WordPress user ID.
*/
private function get_default_author_id(): int {
$author_id = absint( get_option( 'autoblogger_default_author', 0 ) );
$author_id = absint( get_option( 'prautoblogger_default_author', 0 ) );
if ( $author_id > 0 ) {
return $author_id;
}

View File

@@ -8,14 +8,14 @@ declare(strict_types=1);
* collect_data() method, and stores results in the ab_source_data table.
* Handles deduplication via UNIQUE index on (source_type, source_id).
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 1).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 1).
* Dependencies: Source provider implementations, WordPress $wpdb.
*
* @see providers/interface-source-provider.php — Interface providers implement.
* @see providers/class-reddit-provider.php — Primary source provider.
* @see ARCHITECTURE.md — Data flow step 1.
*/
class Autoblogger_Source_Collector {
class PRAutoBlogger_Source_Collector {
/**
* Collect data from all enabled source providers and store in database.
@@ -25,7 +25,7 @@ class Autoblogger_Source_Collector {
* @return int Total number of new records inserted.
*/
public function collect_from_all_sources(): int {
$enabled = json_decode( get_option( 'autoblogger_enabled_sources', '["reddit"]' ), true );
$enabled = json_decode( get_option( 'prautoblogger_enabled_sources', '["reddit"]' ), true );
if ( ! is_array( $enabled ) ) {
$enabled = [ 'reddit' ];
}
@@ -35,7 +35,7 @@ class Autoblogger_Source_Collector {
foreach ( $enabled as $source_type ) {
$provider = $this->get_provider( $source_type );
if ( null === $provider ) {
Autoblogger_Logger::instance()->warning( "No provider found for source type: {$source_type}", 'collector' );
PRAutoBlogger_Logger::instance()->warning( "No provider found for source type: {$source_type}", 'collector' );
continue;
}
@@ -44,12 +44,12 @@ class Autoblogger_Source_Collector {
$inserted = $this->store_data( $data );
$total_inserted += $inserted;
Autoblogger_Logger::instance()->info(
PRAutoBlogger_Logger::instance()->info(
sprintf( 'Collected %d items from %s (%d new).', count( $data ), $source_type, $inserted ),
'collector'
);
} catch ( \Exception $e ) {
Autoblogger_Logger::instance()->error( "Collection from {$source_type} FAILED: " . $e->getMessage(), 'collector' );
PRAutoBlogger_Logger::instance()->error( "Collection from {$source_type} FAILED: " . $e->getMessage(), 'collector' );
// Continue with other sources — one failure shouldn't stop everything.
}
}
@@ -64,13 +64,13 @@ class Autoblogger_Source_Collector {
*
* Side effects: database inserts.
*
* @param Autoblogger_Source_Data[] $items
* @param PRAutoBlogger_Source_Data[] $items
*
* @return int Number of new records inserted.
*/
private function store_data( array $items ): int {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_source_data';
$table = $wpdb->prefix . 'prautoblogger_source_data';
$inserted = 0;
foreach ( $items as $item ) {
@@ -110,11 +110,11 @@ class Autoblogger_Source_Collector {
*
* @param string $source_type Source type identifier.
*
* @return Autoblogger_Source_Provider_Interface|null
* @return PRAutoBlogger_Source_Provider_Interface|null
*/
private function get_provider( string $source_type ): ?Autoblogger_Source_Provider_Interface {
private function get_provider( string $source_type ): ?PRAutoBlogger_Source_Provider_Interface {
$providers = [
'reddit' => Autoblogger_Reddit_Provider::class,
'reddit' => PRAutoBlogger_Reddit_Provider::class,
// Future providers: 'tiktok', 'instagram', 'youtube'
];
@@ -123,7 +123,7 @@ class Autoblogger_Source_Collector {
*
* @param array<string, string> $providers Map of source_type => class name.
*/
$providers = apply_filters( 'autoblogger_filter_source_providers', $providers );
$providers = apply_filters( 'prautoblogger_filter_source_providers', $providers );
if ( ! isset( $providers[ $source_type ] ) ) {
return null;
@@ -134,7 +134,7 @@ class Autoblogger_Source_Collector {
// Guard against unloadable provider classes (e.g. stub providers not yet
// autoloadable, or classes removed during development).
if ( ! class_exists( $class ) ) {
Autoblogger_Logger::instance()->error(
PRAutoBlogger_Logger::instance()->error(
sprintf( 'Provider class "%s" for source type "%s" does not exist.', $class, $source_type ),
'collector'
);
@@ -154,7 +154,7 @@ class Autoblogger_Source_Collector {
private function get_config_for_source( string $source_type ): array {
if ( 'reddit' === $source_type ) {
return [
'subreddits' => json_decode( get_option( 'autoblogger_target_subreddits', '[]' ), true ) ?: [],
'subreddits' => json_decode( get_option( 'prautoblogger_target_subreddits', '[]' ), true ) ?: [],
'limit' => 25,
'time_filter' => 'day',
'include_comments' => true,

View File

@@ -0,0 +1,256 @@
<?php
declare(strict_types=1);
/**
* Registers the [prautoblogger_posts] shortcode and its supporting REST API endpoint.
*
* This is PRAutoBlogger's first public-facing frontend feature. It renders a React-powered
* card grid of generated posts, fetching data asynchronously from a lightweight REST
* endpoint to keep the initial page load fast.
*
* Triggered by: Main orchestrator (class-prautoblogger.php) on `init` and `rest_api_init`.
* Dependencies: wp.element (WordPress-bundled React), posts-widget.js, posts-widget.css.
*
* @see class-prautoblogger.php — Hook registration for this class.
* @see assets/js/posts-widget.js — React component that renders the cards.
* @see assets/css/posts-widget.css — Frontend styles matching the Peptide Repo theme.
* @see ARCHITECTURE.md — Data flow for frontend widget.
*/
class PRAutoBlogger_Posts_Widget {
/**
* REST API namespace for the widget endpoint.
*/
private const REST_NAMESPACE = 'prautoblogger/v1';
/**
* REST API route for fetching posts.
*/
private const REST_ROUTE = '/posts';
/**
* Maximum posts allowed per request (hard cap for safety).
*/
private const MAX_POSTS_PER_REQUEST = 12;
/**
* Register the [prautoblogger_posts] shortcode.
*
* Called on `init` by the main orchestrator.
*
* Side effects: registers a WordPress shortcode.
*
* @return void
*/
public function on_register_shortcode(): void {
add_shortcode( 'prautoblogger_posts', [ $this, 'render_shortcode' ] );
}
/**
* Register the REST API endpoint for fetching posts.
*
* Called on `rest_api_init` by the main orchestrator.
*
* Side effects: registers a WordPress REST route.
*
* @return void
*/
public function on_register_rest_route(): void {
register_rest_route(
self::REST_NAMESPACE,
self::REST_ROUTE,
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'handle_rest_request' ],
'permission_callback' => function () {
/**
* Filter whether the posts widget REST endpoint is publicly accessible.
*
* By default, this is true — the endpoint only serves published posts.
* Return false to require authentication (e.g., for private/staging sites).
*
* @param bool $is_public Whether the endpoint is publicly accessible.
*/
return (bool) apply_filters( 'prautoblogger_rest_posts_public', true );
},
'args' => [
'per_page' => [
'type' => 'integer',
'default' => 6,
'minimum' => 1,
'maximum' => self::MAX_POSTS_PER_REQUEST,
'sanitize_callback' => 'absint',
],
'category' => [
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
],
]
);
}
/**
* Render the shortcode output.
*
* Outputs a mount-point div and enqueues the React script + CSS.
* The React component hydrates the div on the client side.
*
* Shortcode attributes:
* count — Number of posts to display (default 6, max 12).
* category — Filter by category slug (default empty = all).
* title — Widget heading (default "Latest Research & Insights").
* subtitle — Widget subheading.
*
* @param array<string, string>|string $atts Shortcode attributes.
*
* @return string HTML mount-point div.
*/
public function render_shortcode( $atts ): string {
$atts = shortcode_atts(
[
'count' => '6',
'category' => '',
'title' => __( 'Latest Research & Insights', 'prautoblogger' ),
'subtitle' => __( 'Evidence-based articles on peptides, protocols, and emerging research', 'prautoblogger' ),
],
$atts,
'prautoblogger_posts'
);
$count = min( absint( $atts['count'] ), self::MAX_POSTS_PER_REQUEST );
if ( $count < 1 ) {
$count = 6;
}
$this->enqueue_frontend_assets( $count, $atts );
return '<div id="prab-posts-root"></div>';
}
/**
* Enqueue frontend CSS and JS for the widget.
*
* Uses wp.element (WordPress-bundled React) as a dependency — no build step needed.
* Passes configuration to JS via wp_localize_script.
*
* Side effects: enqueues styles and scripts.
*
* @param int $count Number of posts to fetch.
* @param array<string, string> $atts Shortcode attributes.
*
* @return void
*/
private function enqueue_frontend_assets( int $count, array $atts ): void {
$version = defined( 'PRAUTOBLOGGER_VERSION' ) ? PRAUTOBLOGGER_VERSION : '0.1.0';
wp_enqueue_style(
'prautoblogger-posts-widget',
PRAUTOBLOGGER_PLUGIN_URL . 'assets/css/posts-widget.css',
[],
$version
);
wp_enqueue_script(
'prautoblogger-posts-widget',
PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/posts-widget.js',
[ 'wp-element' ], // WordPress-bundled React.
$version,
true // Load in footer for performance.
);
wp_localize_script(
'prautoblogger-posts-widget',
'prabPostsWidget',
[
'restUrl' => esc_url_raw( rest_url( self::REST_NAMESPACE . self::REST_ROUTE ) ),
'count' => $count,
'category' => sanitize_text_field( $atts['category'] ),
'title' => sanitize_text_field( $atts['title'] ),
'subtitle' => sanitize_text_field( $atts['subtitle'] ),
'archiveUrl' => esc_url( get_post_type_archive_link( 'post' ) ?: '' ),
]
);
}
/**
* Handle the REST API request for posts.
*
* Returns a slim JSON array of published posts generated by PRAutoBlogger.
* Each post includes only the fields the frontend needs — no unnecessary data.
*
* @param \WP_REST_Request $request The incoming REST request.
*
* @return \WP_REST_Response JSON response with post data.
*/
public function handle_rest_request( \WP_REST_Request $request ): \WP_REST_Response {
$per_page = min( absint( $request->get_param( 'per_page' ) ), self::MAX_POSTS_PER_REQUEST );
if ( $per_page < 1 ) {
$per_page = 6;
}
$category = sanitize_text_field( $request->get_param( 'category' ) ?? '' );
$query_args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $per_page,
'orderby' => 'date',
'order' => 'DESC',
'meta_query' => [
[
'key' => '_prautoblogger_generated',
'value' => '1',
],
],
];
// Optional category filter.
if ( ! empty( $category ) ) {
$query_args['category_name'] = $category;
}
$query = new \WP_Query( $query_args );
$posts = [];
foreach ( $query->posts as $post ) {
$posts[] = $this->format_post_for_response( $post );
}
$response = new \WP_REST_Response( $posts, 200 );
// Cache for 5 minutes — posts don't change frequently.
$response->header( 'Cache-Control', 'public, max-age=300' );
return $response;
}
/**
* Format a single post for the REST API response.
*
* Returns only the fields the React component needs to render a card.
*
* @param \WP_Post $post WordPress post object.
*
* @return array<string, mixed> Formatted post data.
*/
private function format_post_for_response( \WP_Post $post ): array {
$categories = get_the_category( $post->ID );
$category_name = ! empty( $categories ) ? $categories[0]->name : '';
$category_slug = ! empty( $categories ) ? $categories[0]->slug : '';
$featured_image = get_the_post_thumbnail_url( $post->ID, 'medium_large' );
$word_count = str_word_count( wp_strip_all_tags( $post->post_content ) );
return [
'id' => $post->ID,
'title' => get_the_title( $post ),
'excerpt' => get_the_excerpt( $post ),
'url' => get_permalink( $post ),
'date' => get_the_date( 'c', $post ),
'category_name' => $category_name,
'category_slug' => $category_slug,
'featured_image' => $featured_image ?: '',
'word_count' => $word_count,
];
}
}

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
* @see core/class-content-analyzer.php — Creates these from source data + LLM analysis.
* @see core/class-idea-scorer.php — Ranks these into article ideas.
*/
class Autoblogger_Analysis_Result {
class PRAutoBlogger_Analysis_Result {
private int $id;
private string $analysis_type;

View File

@@ -12,7 +12,7 @@ declare(strict_types=1);
* @see core/class-idea-scorer.php — Creates and ranks these.
* @see core/class-content-generator.php — Uses these as generation input.
*/
class Autoblogger_Article_Idea {
class PRAutoBlogger_Article_Idea {
private string $topic;
private string $article_type;

View File

@@ -12,9 +12,9 @@ declare(strict_types=1);
*
* @see core/class-content-generator.php — Creates and consumes these.
*/
class Autoblogger_Content_Request {
class PRAutoBlogger_Content_Request {
private Autoblogger_Article_Idea $idea;
private PRAutoBlogger_Article_Idea $idea;
private string $pipeline_mode;
private string $tone;
private int $min_word_count;
@@ -23,7 +23,7 @@ class Autoblogger_Content_Request {
private array $topic_exclusions;
public function __construct(
Autoblogger_Article_Idea $idea,
PRAutoBlogger_Article_Idea $idea,
string $pipeline_mode,
string $tone,
int $min_word_count,
@@ -40,7 +40,7 @@ class Autoblogger_Content_Request {
$this->topic_exclusions = $topic_exclusions;
}
public function get_idea(): Autoblogger_Article_Idea {
public function get_idea(): PRAutoBlogger_Article_Idea {
return $this->idea;
}

View File

@@ -10,7 +10,7 @@ declare(strict_types=1);
* @see core/class-metrics-collector.php — Creates these from WP + GA4 data.
* @see core/class-content-analyzer.php — Reads past scores to refine analysis.
*/
class Autoblogger_Content_Score {
class PRAutoBlogger_Content_Score {
private int $id;
private int $post_id;

View File

@@ -10,7 +10,7 @@ declare(strict_types=1);
* @see core/class-chief-editor.php — Creates these after reviewing content.
* @see core/class-publisher.php — Uses verdict to decide publish vs. draft.
*/
class Autoblogger_Editorial_Review {
class PRAutoBlogger_Editorial_Review {
private string $verdict;
private string $notes;

View File

@@ -7,10 +7,10 @@ declare(strict_types=1);
* Triggered by: Cost_Tracker creates these; Metrics_Page displays them.
* Dependencies: None — pure data object.
*
* @see core/class-cost-tracker.php — Writes these to ab_generation_log table.
* @see core/class-cost-tracker.php — Writes these to prab_generation_log table.
* @see admin/class-metrics-page.php — Reads and displays these.
*/
class Autoblogger_Generation_Log {
class PRAutoBlogger_Generation_Log {
private int $id;
private ?int $post_id;

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
* @see providers/interface-source-provider.php — Providers return arrays of these.
* @see core/class-content-analyzer.php — Consumes these for pattern detection.
*/
class Autoblogger_Source_Data {
class PRAutoBlogger_Source_Data {
private int $id;
private string $source_type;

View File

@@ -12,16 +12,16 @@ declare(strict_types=1);
*
* @see interface-source-provider.php — Interface this class implements.
*/
class Autoblogger_Instagram_Provider implements Autoblogger_Source_Provider_Interface {
class PRAutoBlogger_Instagram_Provider implements PRAutoBlogger_Source_Provider_Interface {
/**
* @param array<string, mixed> $config
* @return Autoblogger_Source_Data[]
* @return PRAutoBlogger_Source_Data[]
* @throws \RuntimeException Always — not yet implemented.
*/
public function collect_data( array $config ): array {
throw new \RuntimeException(
__( 'Instagram source provider is not yet implemented. Remove "instagram" from enabled sources.', 'autoblogger' )
__( 'Instagram source provider is not yet implemented. Remove "instagram" from enabled sources.', 'prautoblogger' )
);
}

View File

@@ -7,13 +7,13 @@ declare(strict_types=1);
* Manages pricing tables, model lists, and cost estimation. Decoupled from
* the request logic for easy updates and testing.
*
* Triggered by: Autoblogger_OpenRouter_Provider calls pricing methods.
* Triggered by: PRAutoBlogger_OpenRouter_Provider calls pricing methods.
* Dependencies: None — pure data + calculation, no external calls by default.
*
* @see providers/class-openrouter-provider.php — Instantiates this class.
* @see ARCHITECTURE.md — Model selection flow.
*/
class Autoblogger_OpenRouter_Pricing {
class PRAutoBlogger_OpenRouter_Pricing {
/**
* Known model pricing (per million tokens, in USD).
@@ -41,7 +41,7 @@ class Autoblogger_OpenRouter_Pricing {
* @return array<int, array{id: string, name: string, context_length: int, pricing: array{prompt: float, completion: float}}>
*/
public function get_available_models(): array {
$cached = get_transient( 'autoblogger_openrouter_models' );
$cached = get_transient( 'prautoblogger_openrouter_models' );
if ( false !== $cached && is_array( $cached ) ) {
return $cached;
}
@@ -62,7 +62,7 @@ class Autoblogger_OpenRouter_Pricing {
);
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
Autoblogger_Logger::instance()->warning( 'Failed to fetch OpenRouter models list.', 'openrouter' );
PRAutoBlogger_Logger::instance()->warning( 'Failed to fetch OpenRouter models list.', 'openrouter' );
return [];
}
@@ -83,7 +83,7 @@ class Autoblogger_OpenRouter_Pricing {
}
}
set_transient( 'autoblogger_openrouter_models', $models, DAY_IN_SECONDS );
set_transient( 'prautoblogger_openrouter_models', $models, DAY_IN_SECONDS );
return $models;
}
@@ -114,7 +114,7 @@ class Autoblogger_OpenRouter_Pricing {
if ( null === $pricing ) {
// Unknown model — use a conservative estimate.
Autoblogger_Logger::instance()->warning( "Unknown model pricing for '{$model}'. Using conservative estimate.", 'openrouter' );
PRAutoBlogger_Logger::instance()->warning( "Unknown model pricing for '{$model}'. Using conservative estimate.", 'openrouter' );
$pricing = [ 'prompt' => 10.0, 'completion' => 30.0 ];
}
@@ -128,10 +128,10 @@ class Autoblogger_OpenRouter_Pricing {
* @return string Decrypted API key, or empty string if not set.
*/
private function get_api_key(): string {
$encrypted = get_option( 'autoblogger_openrouter_api_key', '' );
$encrypted = get_option( 'prautoblogger_openrouter_api_key', '' );
if ( '' === $encrypted ) {
return '';
}
return Autoblogger_Encryption::decrypt( $encrypted );
return PRAutoBlogger_Encryption::decrypt( $encrypted );
}
}

View File

@@ -9,14 +9,14 @@ declare(strict_types=1);
* separate provider integrations.
*
* Triggered by: Content_Analyzer, Content_Generator, Chief_Editor, Metrics_Collector.
* Dependencies: Autoblogger_Encryption (for API key decryption), wp_remote_post(), Autoblogger_OpenRouter_Pricing.
* Dependencies: PRAutoBlogger_Encryption (for API key decryption), wp_remote_post(), PRAutoBlogger_OpenRouter_Pricing.
*
* @see interface-llm-provider.php — Interface this class implements.
* @see class-openrouter-pricing.php — Pricing and model list lookups.
* @see class-cost-tracker.php — Called after every request to log token usage.
* @see ARCHITECTURE.md — Data flow diagram showing where this fits.
*/
class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interface {
class PRAutoBlogger_OpenRouter_Provider implements PRAutoBlogger_LLM_Provider_Interface {
private const API_BASE_URL = 'https://openrouter.ai/api/v1';
@@ -51,7 +51,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
$api_key = $this->get_api_key();
if ( '' === $api_key ) {
throw new \RuntimeException(
__( 'OpenRouter API key is not configured. Go to AutoBlogger → Settings.', 'autoblogger' )
__( 'OpenRouter API key is not configured. Go to PRAutoBlogger → Settings.', 'prautoblogger' )
);
}
@@ -72,16 +72,16 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
$last_error = '';
for ( $attempt = 1; $attempt <= AUTOBLOGGER_MAX_RETRIES; $attempt++ ) {
for ( $attempt = 1; $attempt <= PRAUTOBLOGGER_MAX_RETRIES; $attempt++ ) {
$response = wp_remote_post(
self::API_BASE_URL . '/chat/completions',
[
'timeout' => AUTOBLOGGER_API_TIMEOUT_SECONDS,
'timeout' => PRAUTOBLOGGER_API_TIMEOUT_SECONDS,
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
'HTTP-Referer' => home_url(),
'X-Title' => 'AutoBlogger WordPress Plugin',
'X-Title' => 'PRAutoBlogger WordPress Plugin',
],
'body' => wp_json_encode( $body ),
]
@@ -89,13 +89,13 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
if ( is_wp_error( $response ) ) {
$last_error = $response->get_error_message();
Autoblogger_Logger::instance()->warning(
sprintf( 'OpenRouter request failed (attempt %d/%d): %s', $attempt, AUTOBLOGGER_MAX_RETRIES, $last_error ),
PRAutoBlogger_Logger::instance()->warning(
sprintf( 'OpenRouter request failed (attempt %d/%d): %s', $attempt, PRAUTOBLOGGER_MAX_RETRIES, $last_error ),
'openrouter'
);
if ( $attempt < AUTOBLOGGER_MAX_RETRIES ) {
$delay = AUTOBLOGGER_RETRY_BASE_DELAY_SECONDS * pow( 2, $attempt - 1 );
if ( $attempt < PRAUTOBLOGGER_MAX_RETRIES ) {
$delay = PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS * pow( 2, $attempt - 1 );
sleep( (int) $delay );
}
continue;
@@ -108,17 +108,17 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
// Rate limited or server error — retry.
if ( $status_code >= 429 || $status_code >= 500 ) {
$last_error = sprintf( 'HTTP %d: %s', $status_code, $body_raw );
Autoblogger_Logger::instance()->warning(
sprintf( 'OpenRouter HTTP %d (attempt %d/%d): %s', $status_code, $attempt, AUTOBLOGGER_MAX_RETRIES, substr( $body_raw, 0, 500 ) ),
PRAutoBlogger_Logger::instance()->warning(
sprintf( 'OpenRouter HTTP %d (attempt %d/%d): %s', $status_code, $attempt, PRAUTOBLOGGER_MAX_RETRIES, substr( $body_raw, 0, 500 ) ),
'openrouter'
);
if ( $attempt < AUTOBLOGGER_MAX_RETRIES ) {
if ( $attempt < PRAUTOBLOGGER_MAX_RETRIES ) {
// Respect Retry-After header if present.
$retry_after = wp_remote_retrieve_header( $response, 'retry-after' );
$delay = $retry_after
? min( (int) $retry_after, 60 )
: AUTOBLOGGER_RETRY_BASE_DELAY_SECONDS * pow( 2, $attempt - 1 );
: PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS * pow( 2, $attempt - 1 );
sleep( (int) $delay );
}
continue;
@@ -132,7 +132,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
throw new \RuntimeException(
sprintf(
/* translators: %d: HTTP status code, %s: error message */
__( 'OpenRouter API error (HTTP %1$d): %2$s', 'autoblogger' ),
__( 'OpenRouter API error (HTTP %1$d): %2$s', 'prautoblogger' ),
$status_code,
$error_msg
)
@@ -142,7 +142,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
// Success — parse response.
if ( ! isset( $data['choices'][0]['message']['content'] ) ) {
throw new \RuntimeException(
__( 'OpenRouter returned unexpected response format.', 'autoblogger' )
__( 'OpenRouter returned unexpected response format.', 'prautoblogger' )
);
}
@@ -162,8 +162,8 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
throw new \RuntimeException(
sprintf(
/* translators: %d: max retries, %s: last error message */
__( 'OpenRouter API failed after %1$d attempts. Last error: %2$s', 'autoblogger' ),
AUTOBLOGGER_MAX_RETRIES,
__( 'OpenRouter API failed after %1$d attempts. Last error: %2$s', 'prautoblogger' ),
PRAUTOBLOGGER_MAX_RETRIES,
$last_error
)
);
@@ -179,7 +179,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
* @return array<int, array{id: string, name: string, context_length: int, pricing: array{prompt: float, completion: float}}>
*/
public function get_available_models(): array {
$pricing = new Autoblogger_OpenRouter_Pricing();
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
return $pricing->get_available_models();
}
@@ -195,7 +195,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
* @return float Estimated cost in USD.
*/
public function estimate_cost( string $model, int $prompt_tokens, int $completion_tokens ): float {
$pricing = new Autoblogger_OpenRouter_Pricing();
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
return $pricing->estimate_cost( $model, $prompt_tokens, $completion_tokens );
}
@@ -244,10 +244,10 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
* @return string Decrypted API key, or empty string if not set.
*/
private function get_api_key(): string {
$encrypted = get_option( 'autoblogger_openrouter_api_key', '' );
$encrypted = get_option( 'prautoblogger_openrouter_api_key', '' );
if ( '' === $encrypted ) {
return '';
}
return Autoblogger_Encryption::decrypt( $encrypted );
return PRAutoBlogger_Encryption::decrypt( $encrypted );
}
}

View File

@@ -7,17 +7,17 @@ declare(strict_types=1);
* Encapsulates OAuth2 token management, rate limit tracking, and HTTP methods
* for the Reddit API. Decoupled from collection logic for testability.
*
* Triggered by: Autoblogger_Reddit_Provider calls fetch methods here.
* Dependencies: Autoblogger_Encryption (for client secret), wp_remote_post/get().
* Triggered by: PRAutoBlogger_Reddit_Provider calls fetch methods here.
* Dependencies: PRAutoBlogger_Encryption (for client secret), wp_remote_post/get().
*
* @see providers/class-reddit-provider.php — Instantiates this class.
* @see ARCHITECTURE.md — External API integrations table.
*/
class Autoblogger_Reddit_API_Client {
class PRAutoBlogger_Reddit_API_Client {
private const AUTH_URL = 'https://www.reddit.com/api/v1/access_token';
private const API_BASE = 'https://oauth.reddit.com';
private const USER_AGENT = 'AutoBlogger/0.1.0 (WordPress Plugin)';
private const USER_AGENT = 'PRAutoBlogger/0.1.0 (WordPress Plugin)';
/**
* Get an OAuth2 access token, using cached transient if available.
@@ -32,15 +32,15 @@ class Autoblogger_Reddit_API_Client {
*/
public function get_access_token( bool $force_refresh = false ): string {
if ( ! $force_refresh ) {
$cached = get_transient( 'autoblogger_reddit_token' );
$cached = get_transient( 'prautoblogger_reddit_token' );
if ( false !== $cached && is_string( $cached ) ) {
return $cached;
}
}
$client_id = get_option( 'autoblogger_reddit_client_id', '' );
$client_secret = Autoblogger_Encryption::decrypt(
get_option( 'autoblogger_reddit_client_secret', '' )
$client_id = get_option( 'prautoblogger_reddit_client_id', '' );
$client_secret = PRAutoBlogger_Encryption::decrypt(
get_option( 'prautoblogger_reddit_client_secret', '' )
);
if ( '' === $client_id || '' === $client_secret ) {
@@ -63,25 +63,25 @@ class Autoblogger_Reddit_API_Client {
);
if ( is_wp_error( $response ) ) {
Autoblogger_Logger::instance()->error( 'Reddit OAuth failed: ' . $response->get_error_message(), 'reddit' );
PRAutoBlogger_Logger::instance()->error( 'Reddit OAuth failed: ' . $response->get_error_message(), 'reddit' );
return '';
}
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
Autoblogger_Logger::instance()->error( 'Reddit OAuth HTTP ' . wp_remote_retrieve_response_code( $response ), 'reddit' );
PRAutoBlogger_Logger::instance()->error( 'Reddit OAuth HTTP ' . wp_remote_retrieve_response_code( $response ), 'reddit' );
return '';
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! isset( $data['access_token'] ) ) {
Autoblogger_Logger::instance()->error( 'Reddit OAuth response missing access_token.', 'reddit' );
PRAutoBlogger_Logger::instance()->error( 'Reddit OAuth response missing access_token.', 'reddit' );
return '';
}
$token = $data['access_token'];
$expires = isset( $data['expires_in'] ) ? (int) $data['expires_in'] - 60 : 3540;
set_transient( 'autoblogger_reddit_token', $token, $expires );
set_transient( 'prautoblogger_reddit_token', $token, $expires );
return $token;
}
@@ -179,7 +179,7 @@ class Autoblogger_Reddit_API_Client {
);
if ( is_wp_error( $response ) ) {
Autoblogger_Logger::instance()->error( 'Reddit API GET failed: ' . $response->get_error_message(), 'reddit' );
PRAutoBlogger_Logger::instance()->error( 'Reddit API GET failed: ' . $response->get_error_message(), 'reddit' );
return null;
}
@@ -189,7 +189,7 @@ class Autoblogger_Reddit_API_Client {
$rate_reset = wp_remote_retrieve_header( $response, 'x-ratelimit-reset' );
if ( '' !== $rate_remaining ) {
set_transient( 'autoblogger_reddit_rate_limit', [
set_transient( 'prautoblogger_reddit_rate_limit', [
'remaining' => (int) $rate_remaining,
'limit' => (int) $rate_limit + (int) $rate_remaining,
'resets_at' => gmdate( 'Y-m-d H:i:s', time() + (int) $rate_reset ),
@@ -198,7 +198,7 @@ class Autoblogger_Reddit_API_Client {
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
Autoblogger_Logger::instance()->error( sprintf( 'Reddit API HTTP %d: %s', $status_code, substr( wp_remote_retrieve_body( $response ), 0, 300 ) ), 'reddit' );
PRAutoBlogger_Logger::instance()->error( sprintf( 'Reddit API HTTP %d: %s', $status_code, substr( wp_remote_retrieve_body( $response ), 0, 300 ) ), 'reddit' );
return null;
}

View File

@@ -9,14 +9,14 @@ declare(strict_types=1);
* storing them in the ab_source_data table.
*
* Triggered by: Source_Collector calls collect_data() on each enabled provider.
* Dependencies: Autoblogger_Reddit_API_Client (for HTTP calls and auth).
* Dependencies: PRAutoBlogger_Reddit_API_Client (for HTTP calls and auth).
*
* @see interface-source-provider.php — Interface this class implements.
* @see providers/class-reddit-api-client.php — HTTP client for Reddit API.
* @see core/class-source-collector.php — Orchestrates collection from all providers.
* @see ARCHITECTURE.md — External API integrations table.
*/
class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interface {
class PRAutoBlogger_Reddit_Provider implements PRAutoBlogger_Source_Provider_Interface {
/**
* Collect posts and comments from configured subreddits.
@@ -34,16 +34,16 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
* comments_per_post?: int,
* } $config Collection configuration.
*
* @return Autoblogger_Source_Data[]
* @return PRAutoBlogger_Source_Data[]
*
* @throws \RuntimeException On authentication failure or API error.
*/
public function collect_data( array $config ): array {
$api_client = new Autoblogger_Reddit_API_Client();
$api_client = new PRAutoBlogger_Reddit_API_Client();
$token = $api_client->get_access_token();
if ( '' === $token ) {
throw new \RuntimeException(
__( 'Reddit authentication failed. Check client ID and secret in settings.', 'autoblogger' )
__( 'Reddit authentication failed. Check client ID and secret in settings.', 'prautoblogger' )
);
}
@@ -62,7 +62,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
$posts = $api_client->fetch_posts( $token, $subreddit, $limit, $time_filter );
foreach ( $posts as $post ) {
$collected[] = new Autoblogger_Source_Data( [
$collected[] = new PRAutoBlogger_Source_Data( [
'source_type' => 'reddit',
'source_id' => 't3_' . $post['id'],
'subreddit' => $subreddit,
@@ -92,7 +92,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
);
foreach ( $comments as $comment ) {
$collected[] = new Autoblogger_Source_Data( [
$collected[] = new PRAutoBlogger_Source_Data( [
'source_type' => 'reddit',
'source_id' => 't1_' . $comment['id'],
'subreddit' => $subreddit,
@@ -115,7 +115,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
}
} catch ( \Exception $e ) {
// Log but continue with other subreddits.
Autoblogger_Logger::instance()->error(
PRAutoBlogger_Logger::instance()->error(
sprintf( 'Reddit: Failed to collect from r/%s: %s', $subreddit, $e->getMessage() ),
'reddit'
);
@@ -140,7 +140,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
* @return bool
*/
public function validate_credentials(): bool {
$api_client = new Autoblogger_Reddit_API_Client();
$api_client = new PRAutoBlogger_Reddit_API_Client();
$token = $api_client->get_access_token( true );
return '' !== $token;
}
@@ -153,7 +153,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
public function get_rate_limit_status(): array {
// Reddit rate limits are tracked per-response via headers.
// We store the last seen values in a transient.
$status = get_transient( 'autoblogger_reddit_rate_limit' );
$status = get_transient( 'prautoblogger_reddit_rate_limit' );
if ( is_array( $status ) ) {
return $status;
}
@@ -171,7 +171,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
* @return string[]
*/
private function get_configured_subreddits(): array {
$json = get_option( 'autoblogger_target_subreddits', '[]' );
$json = get_option( 'prautoblogger_target_subreddits', '[]' );
$list = json_decode( $json, true );
return is_array( $list ) ? $list : [];
}

View File

@@ -13,16 +13,16 @@ declare(strict_types=1);
* @see interface-source-provider.php — Interface this class implements.
* @see ARCHITECTURE.md — Planned source providers.
*/
class Autoblogger_Tiktok_Provider implements Autoblogger_Source_Provider_Interface {
class PRAutoBlogger_Tiktok_Provider implements PRAutoBlogger_Source_Provider_Interface {
/**
* @param array<string, mixed> $config
* @return Autoblogger_Source_Data[]
* @return PRAutoBlogger_Source_Data[]
* @throws \RuntimeException Always — not yet implemented.
*/
public function collect_data( array $config ): array {
throw new \RuntimeException(
__( 'TikTok source provider is not yet implemented. Remove "tiktok" from enabled sources.', 'autoblogger' )
__( 'TikTok source provider is not yet implemented. Remove "tiktok" from enabled sources.', 'prautoblogger' )
);
}

View File

@@ -12,16 +12,16 @@ declare(strict_types=1);
*
* @see interface-source-provider.php — Interface this class implements.
*/
class Autoblogger_Youtube_Provider implements Autoblogger_Source_Provider_Interface {
class PRAutoBlogger_Youtube_Provider implements PRAutoBlogger_Source_Provider_Interface {
/**
* @param array<string, mixed> $config
* @return Autoblogger_Source_Data[]
* @return PRAutoBlogger_Source_Data[]
* @throws \RuntimeException Always — not yet implemented.
*/
public function collect_data( array $config ): array {
throw new \RuntimeException(
__( 'YouTube source provider is not yet implemented. Remove "youtube" from enabled sources.', 'autoblogger' )
__( 'YouTube source provider is not yet implemented. Remove "youtube" from enabled sources.', 'prautoblogger' )
);
}

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
* @see CONVENTIONS.md — "How To: Add a New LLM Provider".
* @see ARCHITECTURE.md — External API integrations table.
*/
interface Autoblogger_LLM_Provider_Interface {
interface PRAutoBlogger_LLM_Provider_Interface {
/**
* Send a chat completion request to the LLM.

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
* @see core/class-source-collector.php — Orchestrates collection from all providers.
* @see CONVENTIONS.md — "How To: Add a New Source Provider".
*/
interface Autoblogger_Source_Provider_Interface {
interface PRAutoBlogger_Source_Provider_Interface {
/**
* Collect posts/comments from this social platform.
@@ -23,7 +23,7 @@ interface Autoblogger_Source_Provider_Interface {
* keywords?: string[],
* } $config Source-specific configuration.
*
* @return Autoblogger_Source_Data[] Array of collected data objects.
* @return PRAutoBlogger_Source_Data[] Array of collected data objects.
*
* @throws \RuntimeException On API error after retries exhausted.
*/