rl-warmup-plugin/includes/class-rl-mailwarmer-campaign-helper.php

1487 lines
59 KiB
PHP

<?php
/**
* Helper class for managing Campaigns.
*/
class RL_MailWarmer_Campaign_Helper
{
/**
* Calculate the campaign timeline with randomized daily email goals.
*
* @param int $campaign_id The campaign post ID.
* @return array Weekly email schedule with randomized daily goals.
* @throws Exception If the campaign parameters are invalid.
*/
public static function calculate_campaign_timeline($campaign_id)
{
// Fetch campaign details
$warmup_period = (int) get_post_meta($campaign_id, 'warmup_period', true); // Weeks
$target_volume = (int) get_post_meta($campaign_id, 'target_volume', true); // Daily emails to ramp up to
$start_date = get_post_meta($campaign_id, 'start_date', true); // Campaign start date
if (!$warmup_period || !$target_volume || !$start_date) {
throw new Exception(__('Invalid campaign parameters.', 'rl-mailwarmer'));
}
// Fetch ACF options
$holidays_raw = get_field('calendar_holidays', 'option'); // Holidays in 12/25/25 format
$holidays = array_map(
fn($date) => DateTime::createFromFormat('m/d/y', trim($date))->format('Y-m-d'),
explode("\n", $holidays_raw)
);
$min_starting_volume = (int) get_field('min_starting_email_volume', 'option') ?: 5;
$max_daily_volume = (int) get_field('max_campaign_daily_email_volume', 'option') ?: 1000;
// Calculate starting daily volume (2.5% of target volume)
$starting_daily_volume = max(ceil($target_volume * 0.025), $min_starting_volume);
// Initialize variables
$timeline = [];
$total_days = $warmup_period * 7; // Total days in the campaign
$start_date = new DateTime($start_date);
// Calculate daily ramp-up rate
$daily_increase = ($target_volume - $starting_daily_volume) / ($total_days * .75);
// log_to_file("calculate_campaign_timeline - Ramping up from $min_starting_volume to $target_volume over $total_days days, increasing by $daily_increase each day with no more than $max_daily_volume emails in a day");
// Generate timeline
for ($day = 0; $day < $total_days; $day++) {
$current_date = clone $start_date;
$current_date->modify("+{$day} days");
$date_formatted = $current_date->format('Y-m-d');
// Adjust for holidays and weekends
$is_weekend = in_array($current_date->format('N'), [6, 7]);
$is_holiday = in_array($date_formatted, $holidays);
$reduction_factor = ($is_weekend || $is_holiday) ? mt_rand(65, 82) / 100 : 1;
// Calculate daily volume
$daily_volume = min(
ceil(($starting_daily_volume + ($daily_increase * $day)) * $reduction_factor),
ceil($target_volume + ($target_volume * mt_rand(5,20))/100)
);
/*$daily_volume = ceil(($starting_daily_volume + ($daily_increase * $day)) * $reduction_factor);
if ($daily_volume >= $target_volume) {
$daily_volume = ceil( $target_volume + ($target_volume * mt_rand(5,20))/100 );
}*/
if ($daily_volume > $max_daily_volume) {
log_to_file("calculate_campaign_timeline - Max Daily Volume hit for campaign $campaign_id! Capping number of emails");
$daily_volume = $max_daily_volume;
}
$timeline[$date_formatted] = [ 'target_volume' => $daily_volume, 'current_volume' => 0, 'items_sent' => 0 ];
// log_to_file("calculate_campaign_timeline - $day: $daily_volume");
// array_push($timeline, )
// $timeline[$date_formatted] = $daily_volume;
}
// Save the timeline as a JSON string to the campaign post
// $timeline_json = json_encode($timeline);
// update_post_meta($campaign_id, 'campaign_timeline', $timeline_json);
// log_to_file("calculate_campaign_timeline - Empty Timeline: $timeline_json");
$filled_timeline = self::fill_campaign_timeline($campaign_id, $timeline);
log_to_file("calculate_campaign_timeline - Filled Timeline: ", $filled_timeline);
// Check the number of saved messages per date
$message_counts = RL_MailWarmer_DB_Helper::get_message_counts_by_date($campaign_id);
// log_to_file("fill_campaign_timeline - Message counts: ", $message_counts);
// Save the updated campaign timeline
update_post_meta($campaign_id, 'campaign_timeline', json_encode($filled_timeline, JSON_PRETTY_PRINT));
update_post_meta($campaign_id, 'message_counts', json_encode($message_counts, JSON_PRETTY_PRINT));
return $timeline;
}
public static function fill_campaign_timeline(int $campaign_id, array $campaign_timeline): array {
$filled_dates = [];
$weekly_volumes = [];
$current_week = '';
$weekly_total = 0;
$ratios = [
'extra-long' => [
'percent_of_volume_lower' => 0,
'percent_of_volume_upper' => 0,
'num_participants_lower' => 2,
'num_participants_upper' => 8,
'num_responses_lower' => 5,
'num_responses_upper' => 12
],
'long' => [
'percent_of_volume_lower' => 2,
'percent_of_volume_upper' => 4,
'num_participants_lower' => 3,
'num_participants_upper' => 6,
'num_responses_lower' => 4,
'num_responses_upper' => 6
],
'medium' => [
'percent_of_volume_lower' => 2,
'percent_of_volume_upper' => 6,
'num_participants_lower' => 3,
'num_participants_upper' => 5,
'num_responses_lower' => 3,
'num_responses_upper' => 5
],
'short' => [
'percent_of_volume_lower' => 15,
'percent_of_volume_upper' => 20,
'num_participants_lower' => 2,
'num_participants_upper' => 4,
'num_responses_lower' => 2,
'num_responses_upper' => 4
],
];
$warmup_period = (int) get_post_meta($campaign_id, 'warmup_period', true); // Weeks
$start_date = get_post_meta($campaign_id, 'start_date', true); // Campaign start date
$total_days = $warmup_period * 7; // Total days in the campaign
$start_date = date('Y-m-d H:i:s', strtotime($start_date));
$end_date = date('Y-m-d 23:59:59', strtotime($start_date . " + {$total_days} days"));
// $end_date = date('Y-m-d H:i', strtotime($start_date . " +{$total_days} days"));
log_to_file("fill_campaign_timeline - Start: $start_date End: $end_date");
// // Calculate weekly volumes
// foreach ($campaign_timeline as $date => $data) {
// $week = date('Y-W', strtotime($date));
// if ($week !== $current_week) {
// if ($current_week !== '') {
// $weekly_volumes[$current_week] = $weekly_total;
// }
// $current_week = $week;
// $weekly_total = 0;
// }
// $weekly_total += $data['target_volume'];
// if (next($campaign_timeline) === false) {
// $weekly_volumes[$week] = $weekly_total;
// }
// }
// Calculate weekly volumes
foreach ($campaign_timeline as $date => $data) {
$timestamp = strtotime($date);
$year = date('o', $timestamp); // ISO year
$week = date('W', $timestamp); // ISO week
$week_key = sprintf('%d-%02d', $year, $week);
if (!isset($weekly_volumes[$week_key])) {
$weekly_volumes[$week_key] = 0;
}
$weekly_volumes[$week_key] += $data['target_volume'];
}
// Process each week
foreach ($weekly_volumes as $week => $volume) {
log_to_file("fill_campaign_timeline - Week $week goal: $volume");
$weekly_scheduled_messages = 0;
foreach ($ratios as $length => $ratio) {
// Calculate number of conversations for this length
$percent = mt_rand($ratio['percent_of_volume_lower'], $ratio['percent_of_volume_upper']) / 100;
$num_conversations = ceil($volume * $percent);
log_to_file("fill_campaign_timeline - Generating $num_conversations $length conversations");
// Skip if no conversations to generate
if ($num_conversations === 0) continue;
// Generate conversations
for ($i = 0; $i < $num_conversations; $i++) {
// Check if weekly volume reached
if ($weekly_scheduled_messages >= $volume) {
break 2; // Break both loops
}
// Generate random number of responses for this conversation
$num_responses = mt_rand($ratio['num_responses_lower'], $ratio['num_responses_upper']);
// Get week start date
list($year, $weekNum) = explode('-', $week);
$week_start = date('Y-m-d', strtotime($year . 'W' . $weekNum));
if ($week_start < $start_date) {
$week_start = $start_date;
}
// Generate conversation blueprint
$conversation_steps = self::generate_conversation_blueprint(
$campaign_id,
$num_responses,
$week_start,
$end_date,
$filled_dates
);
// Update timeline volumes
foreach ($conversation_steps as $step) {
$step_date = date('Y-m-d', strtotime($step['scheduled_for']));
if (isset($campaign_timeline[$step_date])) {
$campaign_timeline[$step_date]['current_volume']++;
$weekly_scheduled_messages++;
// Check if date is now filled
if ($campaign_timeline[$step_date]['current_volume'] >=
$campaign_timeline[$step_date]['target_volume']) {
$filled_dates[] = $step_date;
}
}
}
}
}
}
// log_to_file("fill_campaign_timeline - Campaign Timeline without single-step conversations: ", $campaign_timeline);
// Fill remaining capacity with single-message conversations
log_to_file("fill_campaign_timeline - Filling remaining days with single conversations");
foreach ($campaign_timeline as $date => $data) {
$remaining = $data['target_volume'] - $data['current_volume'];
// log_to_file("fill_campaign_timeline - $date remaining: $remaining");
while ($remaining > 0) {
$conversation_steps = self::generate_conversation_blueprint(
$campaign_id,
0,
$date,
$date
);
foreach ($conversation_steps as $step) {
$step_date = date('Y-m-d', strtotime($step['scheduled_for']));
if (isset($campaign_timeline[$step_date])) {
$campaign_timeline[$step_date]['current_volume']++;
$remaining--;
if ($campaign_timeline[$step_date]['current_volume'] >=
$campaign_timeline[$step_date]['target_volume']) {
$filled_dates[] = $step_date;
}
}
}
}
}
return $campaign_timeline;
}
/**
* Generate a conversation blueprint for a campaign.
*
* @param int $campaign_id The ID of the campaign.
* @param int $number_responses The number of responses/messages to schedule. Defaults to 0.
* @return int The ID of the created conversation in the database.
* @throws Exception If required data is missing or saving fails.
*/
public static function generate_conversation_blueprint($campaign_id, $number_responses = 0, $start_date = false, $end_date = false, $filled_dates = []) {
global $wpdb;
// Fetch the start date for the campaign if one isn't passed
if (!$start_date) {
$start_date = get_post_meta($campaign_id, 'start_date', true);
if (!$start_date) {
throw new Exception(__('generate_conversation_blueprint - Campaign start date is missing.', 'rl-mailwarmer'));
}
}
// log_to_file("generate_conversation_blueprint - Generating conversation with starting date: $start_date & end date: $end_date");
// Step 1: Generate placeholders for the conversation steps
$array_args = ['step' => '', 'status' => 'scheduled'];
$conversation_steps = array_fill(0, $number_responses + 1, $array_args);
$conversation_steps = self::add_timestamps_to_conversation($conversation_steps, $start_date, $end_date, $filled_dates);
// log_to_file("generate_conversation_blueprint - Conversation Steps", $conversation_steps);
// AI Prompt Defaults
$args = [
'initiated_by' => null,
'received_by' => null,
'subject' => null,
'length' => null,
'num_participants' => null,
'num_responses' => 0,
'reply_pool' => [],
'cc_pool' => [],
];
// $args = wp_parse_args($args, $defaults);
$args['num_responses'] = $number_responses;
// Fetch campaign target profession
// log_to_file("generate_conversation_blueprint - Target Profession");
$target_profession = get_field('target_profession', $campaign_id);
// log_to_file("generate_conversation_blueprint - From Email");
$from_emails = get_post_meta($campaign_id, 'email_accounts', true);
if (!$from_emails) {
throw new Exception(__('generate_conversation_blueprint - from_email is missing.', 'rl-mailwarmer'));
}
$from_pool = [];
foreach ($from_emails as $email_id) {
$from_pool[] = get_the_title($email_id);
}
// $email = get_post($from_email);
log_to_file("generate_conversation_blueprint - From pool: ", $from_pool);
// Fetch scrubber pool if 'received_by' is not passed
// log_to_file("generate_conversation_blueprint - Scrubber Pool");
if (!$args['received_by']) {
$args['received_by'] = get_field('scrubber_pool', 'option') ? rl_get_textarea_meta_as_array('option', 'options_scrubber_pool') : '';
// $args['received_by'] = explode(',', $scrubber_pool);
}
// Fetch the CC Pool
// log_to_file("generate_conversation_blueprint - CC Pool");
$cc_pool = self::get_email_accounts_for_pool($campaign_id, 'cc');
// Get up to 4 random items
$num_to_select = min(4, count($cc_pool)); // Ensure we don't request more items than are available
$random_keys = array_rand($cc_pool, $num_to_select);
// If only one item is selected, ensure it's returned as an array
// $args['cc_pool'] = is_array($random_keys) ? array_intersect_key($cc_pool, array_flip($random_keys)) : [$cc_pool[$random_keys]];
// log_to_file("generate_conversation_blueprint - Reply Pool");
// Fetch the Reply pool if there will be any replies needed
if ( intval($args['num_responses']) > 0) {
$reply_pool = self::get_email_accounts_for_pool($campaign_id, 'reply');
$args['reply_pool'] = array_values(array_intersect($args['received_by'], $reply_pool));
}
// log_to_file("generate_conversation_blueprint - Conversation Topics");
// Fetch topics for the conversation if 'subject' is not passed
if (!$args['subject']) {
$default_topics = get_option('options_default_topic_pool') ? rl_get_textarea_meta_as_array("option", "default_topic_pool") : [];
$campaign_topics = get_post_meta($campaign_id, 'campaign_conversation_topics', true) ? rl_get_textarea_meta_as_array($campaign_id, 'campaign_conversation_topics') : [];
// $topics_2 = get_post_meta($campaign_id, 'campaign_conversation_topics', true);
// log_to_file("generate_conversation_blueprint - Default topics:", $default_topics);
// log_to_file("generate_conversation_blueprint - Campaign topics:", $campaign_topics);
// log_to_file("generate_conversation_blueprint - Campaign topics2 : $topics_2");
$all_topics = array_merge($default_topics, $campaign_topics);
// $all_topics_count = count($all_topics);
// log_to_file("generate_conversation - All topics $all_topics_count: ", $all_topics);
if (!empty($all_topics)) {
$args['subject'] = $all_topics[array_rand($all_topics)];
} else {
$args['subject'] = __('General Inquiry', 'rl-mailwarmer');
}
}
// log_to_file("generate_conversation_blueprint - Prompt");
// Generate the prompt
$prompt = sprintf(
"Generate a JSON email conversation with distinct participant personalities; up to 5%% errors; initiating email can be sent to multiple people; replies and follow-ups only from the sender or addresses in both 'can_reply' AND 'to_pool'. Include only: from, to, cc, subject, body. Don't include signatures Return only JSON, no notes\n%s",
json_encode([
'profession' => $target_profession,
'from_pool' => $from_pool,
'to_pool' => $args['received_by'],
'subject' => $args['subject'],
'num_of_replies' => $args['num_responses'],
'can_reply' => $args['reply_pool'],
'available_to_cc' => $args['cc_pool'],
])
);
// log_to_file("From prompt: ", $prompt);
// Step 2: Save the conversation to the database
$conversation_data = [
'campaign_ID' => $campaign_id,
'created_at' => current_time('mysql'),
'status' => 'new',
'first_message_timestamp' => $conversation_steps[0]['scheduled_for'],
'prompt' => $prompt,
'conversation_steps' => json_encode($conversation_steps),
];
$conversation_id = RL_MailWarmer_DB_Helper::insert_conversation($conversation_data);
// $conversation_id = 69;
// $conversation_table = $wpdb->prefix . 'rl_mailwarmer_conversation';
// $wpdb->insert($conversation_table, $conversation_data);
// $conversation_id = $wpdb->insert_id;
if (!$conversation_id) {
throw new Exception(__('generate_conversation_blueprint - Failed to save the conversation blueprint.', 'rl-mailwarmer'));
}
// Step 3: Save the individual message placeholders to the messages table
// $message_table = $wpdb->prefix . 'rl_mailwarmer_messages';
foreach ($conversation_steps as $step) {
$message_data = [
'campaign_ID' => $campaign_id,
'conversation_ID' => $conversation_id,
'scheduled_for_timestamp' => $step['scheduled_for'],
'status' => 'pending',
];
$message_id = RL_MailWarmer_DB_Helper::insert_message($message_data);
if (!$message_id) {
throw new Exception(__('generate_conversation_blueprint - Failed to save the message blueprint.', 'rl-mailwarmer'));
}
}
return $conversation_steps;
}
/**
* Generate a single conversation for a campaign
*
* @param int $campaign_id The ID of the campaign.
* @param array $args Optional arguments to customize the conversation.
* @return int|WP_Error The ID of the created conversation or WP_Error on failure.
*/
public static function generate_conversation($campaign_id, $args = []) {
if (empty($campaign_id)) {
return new WP_Error('invalid_campaign', __('Campaign ID is required.', 'rl-mailwarmer'));
}
// Fetch campaign details
$campaign = get_post($campaign_id);
if (!$campaign || $campaign->post_type !== 'campaign') {
return new WP_Error('invalid_campaign', __('Invalid campaign.', 'rl-mailwarmer'));
}
// Defaults
$defaults = [
'initiated_by' => null,
'received_by' => null,
'subject' => null,
'length' => null,
'num_participants' => null,
'num_responses' => null,
'reply_pool' => [],
'cc_pool' => [],
];
$args = wp_parse_args($args, $defaults);
// Fetch email accounts for this campaign's domain if 'initiated_by' is not passed
if (!$args['initiated_by']) {
$campaign_domain = get_field('domain', $campaign_id);
$email_accounts = get_posts([
'post_type' => 'email-account',
'meta_query' => [
[
'key' => 'domain',
'value' => $campaign_domain->ID,
'compare' => '=',
],
],
'posts_per_page' => -1,
'fields' => 'ids',
]);
if (empty($email_accounts)) {
return new WP_Error('no_initiator', __('No email accounts available for the campaign domain.', 'rl-mailwarmer'));
}
$args['initiated_by'] = $email_accounts[array_rand($email_accounts)];
}
// fetch the email address
// $from_id = $args['initiated_by'];
// $from_email = get_the_title($from_id);
// $args['initiated_by'] = $from_email;
// Set length defaults for participants and responses
if ($args['length']) {
$length_defaults = [
'extra-long' => ['num_participants' => mt_rand(2, 8), 'num_responses' => mt_rand(5, 8)],
'long' => ['num_participants' => mt_rand(3, 6), 'num_responses' => mt_rand(4, 6)],
'medium' => ['num_participants' => mt_rand(3, 5), 'num_responses' => mt_rand(3, 5)],
'short' => ['num_participants' => mt_rand(2, 4), 'num_responses' => mt_rand(1, 3)],
'single' => ['num_participants' => mt_rand(2, 6), 'num_responses' => 0],
];
if (isset($length_defaults[$args['length']])) {
$defaults = $length_defaults[$args['length']];
// log_to_file("Length defaults: ", $defaults['num_participants']);
if (isset($args['num_participants'])) {
unset($defaults['num_participants']);
}
if (isset($args['num_responses'])) {
unset($defaults['num_responses']);
}
$args = array_merge($args, $length_defaults[$args['length']]);
}
} else {
if (!$args['num_participants']) {
$args['num_participants'] = 2;
}
if (!$args['num_responses']) {
$args['num_responses'] = 0;
}
}
// Fetch topics for the conversation if 'subject' is not passed
if (!$args['subject']) {
$default_topics = get_option('options_default_topic_pool') ? rl_get_textarea_meta_as_array("option", "default_topic_pool") : [];
$campaign_topics = get_post_meta($campaign_id, 'campaign_conversation_topics', true) ? rl_get_textarea_meta_as_array($campaign_id, 'campaign_conversation_topics') : [];
$all_topics = array_merge($default_topics, $campaign_topics);
// $all_topics_count = count($all_topics);
// log_to_file("generate_conversation - All topics $all_topics_count: ", $all_topics);
if (!empty($all_topics)) {
$args['subject'] = $all_topics[array_rand($all_topics)];
} else {
$args['subject'] = __('General Inquiry', 'rl-mailwarmer');
}
}
// Fetch campaign duration
$start_date = get_field('start_date', $campaign_id);
$warmup_period = get_field('warmup_period', $campaign_id);
$end_date = date('Ymd', strtotime("+{$warmup_period} weeks", strtotime($start_date)));
// Fetch scrubber pool if 'received_by' is not passed
if (!$args['received_by']) {
$args['received_by'] = get_field('scrubber_pool', 'option') ? rl_get_textarea_meta_as_array('option', 'options_scrubber_pool') : '';
// $args['received_by'] = explode(',', $scrubber_pool);
}
// Fetch the CC Pool
$args['cc_pool'] = self::get_email_accounts_for_pool($campaign_id, 'cc');
// Fetch the Reply pool if there will be any replies needed
if ( intval($args['num_responses']) > 0) {
$args['reply_pool'] = self::get_email_accounts_for_pool($campaign_id, 'reply');
}
// Fetch campaign target profession
$target_profession = get_field('target_profession', $campaign_id);
// Generate the prompt
$prompt = sprintf(
"Generate a JSON email conversation with distinct participant personalities, up to 5%% errors, and replies only from the sender or addresses in both 'can_reply' and 'to_pool'. Include: from, to, cc, subject, body\n%s",
json_encode([
'profession' => $target_profession,
'from' => $args['initiated_by'],
'to_pool' => $args['received_by'],
'subject' => $args['subject'],
'num_of_replies' => $args['num_responses'],
'num_of_participants' => $args['num_participants'],
'can_reply' => $args['reply_pool'],
'available_to_cc' => $args['cc_pool'],
], JSON_PRETTY_PRINT)
);
log_to_file("generate_conversation - Prompt: $prompt");
// Save the conversation to the database
$conversation_steps = []; // Placeholder until AI generates steps
$conversation_id = RL_MailWarmer_DB_Helper::insert_conversation($campaign_id, $conversation_steps, $prompt);
if (!$conversation_id) {
return new WP_Error('db_error', __('Failed to create conversation.', 'rl-mailwarmer'));
}
// Generate AI content for the conversation
// $ai_response = self::clean_ai_response(self::generate_ai_conversation($prompt));
$ai_response = self::clean_ai_response(get_post_meta($campaign_id, 'last_ai_response', true));
if (is_wp_error($ai_response)) {
return $ai_response; // Handle AI generation failure gracefully
}
// Update the conversation with generated steps
self::update_conversation_steps($conversation_id, $ai_response);
// log_to_file("generate_conversation - AI response: $ai_response");
// update_post_meta($campaign_id, 'last_ai_response', wp_slash($ai_response));
// Parse the AI response and save messages
// $parsed_steps = self::parse_ai_response($ai_response);
$array_args = ['step' => ''];
$parsed_steps = array_fill(0, $args['num_responses'], $array_args );
// Save each timestamp into the parsed steps
$parsed_steps = self::add_timestamps_to_conversation($parsed_steps, $start_date);
log_to_file("generate_conversation - parsed_steps: ", $parsed_steps);
foreach ($parsed_steps as $index => $step) {
if (!empty($step['scheduled_for'])) {
$is_first_message = ($index === 0);
if (is_array($step['to'])) {
$step['to'] = implode(', ', $step['to']);
}
if (is_array($step['cc'])) {
$step['cc'] = implode(', ', $step['cc']);
}
// log_to_file("generate_conversation - message body: ", $step['body']);
// RL_MailWarmer_DB_Helper::insert_message(
// $campaign_id,
// $conversation_id,
// $step['scheduled_for'],
// $step['from'],
// $step['to'],
// $step['cc'],
// $step['subject'],
// $step['body'],
// $is_first_message
// );
}
}
return $conversation_id;
}
/**
* Generate AI content for the conversation using OpenAI's ChatGPT API.
*
* @param string $prompt The prompt to send to ChatGPT.
* @return string|WP_Error The AI response or WP_Error on failure.
*/
public static function generate_ai_conversation($prompt) {
$api_key = getenv('OPENAI_API_KEY');
if (empty($api_key)) {
return new WP_Error('missing_api_key', __('OpenAI API key is not configured.', 'rl-mailwarmer'));
}
try {
$client = \OpenAI::client($api_key);
$response = $client->chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'system', 'content' => 'You are a helpful assistant specialized in generating email conversations.'],
['role' => 'user', 'content' => $prompt],
],
'temperature' => 0.7,
'max_tokens' => 2000,
]);
return $response['choices'][0]['message']['content'];
} catch (\Exception $e) {
return new WP_Error('api_error', $e->getMessage());
}
}
/**
* Clean the AI response by removing Markdown-style code block delimiters.
*
* @param string $response The raw AI response.
* @return string The cleaned JSON string.
*/
public static function clean_ai_response($response) {
// Use regex to remove the ```json and ``` delimiters
return preg_replace('/^```json\s*|```$/m', '', trim($response));
}
/**
* Parse the AI response into structured steps for message creation.
*
* @param string $ai_response The JSON string returned by ChatGPT.
* @return array An array of parsed conversation steps.
*/
public static function parse_ai_response($ai_response) {
$steps = json_decode($ai_response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return new WP_Error('invalid_response', __('Failed to parse AI response.', 'rl-mailwarmer'));
}
return $steps;
}
/**
* Fetch all published email accounts matching the campaign domain with "include_in_cc_pool" set to true.
*
* @param int $campaign_id The ID of the campaign.
* @param string $pool The name of the pool. Valid options are: cc, reply
* @return array The email addresses to include in the CC pool.
*/
private static function get_email_accounts_for_pool($campaign_id, $pool) {
// Get the domain associated with the campaign
$domain_id = get_post_meta($campaign_id, 'domain', true);
if (empty($domain_id)) {
return []; // Return an empty array if no domain is found
}
// Query email accounts
if ($pool === 'reply') {
$args = [
'post_type' => 'email-account',
'post_status' => 'publish',
'meta_query' => [
'relation' => 'AND',
[
'key' => 'include_in_' . $pool . '_pool',
'value' => true,
'compare' => '=',
],
// [
// 'key' => 'domain',
// 'value' => $domain_id,
// 'compare' => '=',
// ],
],
'fields' => 'ids', // Retrieve only IDs for efficiency
'posts_per_page' => -1, // Fetch all matching posts
];
} else if ($pool === 'cc') {
$args = [
'post_type' => 'email-account',
'post_status' => 'publish',
'meta_query' => [
'relation' => 'AND',
[
'key' => 'include_in_' . $pool . '_pool',
'value' => true,
'compare' => '=',
],
// [
// 'key' => 'domain',
// 'value' => $domain_id,
// 'compare' => '=',
// ],
],
'fields' => 'ids', // Retrieve only IDs for efficiency
'posts_per_page' => -1, // Fetch all matching posts
];
}
$query = new WP_Query($args);
$email_pool = [];
if ($query->have_posts()) {
foreach ($query->posts as $post_id) {
$email_address = get_the_title($post_id);
if (!empty($email_address)) {
$email_pool[] = $email_address;
}
}
}
return $email_pool;
}
/**
* Add timestamps to parsed conversation steps.
*
* @param array $parsed_steps The parsed steps of the conversation.
* @param string $start_date The starting date for the timestamps.
* @return array The updated conversation steps with timestamps and step indices.
*/
public static function add_timestamps_to_conversation($parsed_steps, $start_date, $end_date = NULL, $filled_dates = []) {
// Ensure parsed_steps is an array
if (!is_array($parsed_steps) || empty($parsed_steps)) {
throw new Exception(__('Parsed steps must be a non-empty array.', 'rl-mailwarmer'));
}
// Initialize the updated steps array
$updated_steps = [];
// Iterate through each step and assign timestamps
foreach ($parsed_steps as $index => $step) {
// If this is not the first step, ensure the start_date is at least 5 minutes after the previous step
if ($index > 0) {
$previous_timestamp = strtotime($updated_steps[$index - 1]['scheduled_for']);
$start_date = date('Y-m-d H:i:s', max($timestamp, $previous_timestamp + 300)); // 300 seconds = 5 minutes
}
// Use the helper function to generate a random timestamp
$timestamp = self::generate_random_date_time($start_date, $end_date, $filled_dates );
// log_to_file("add_timestamps_to_conversation - Returned random timestamp: $timestamp");
// Update the step with the scheduled timestamp and step index
$step['scheduled_for'] = date('Y-m-d H:i:s', $timestamp);
$step['step'] = $index + 1;
// Update the starting date for the next iteration
$start_date = date('Y-m-d H:i:s', $timestamp);
// Append the updated step
$updated_steps[] = $step;
}
// log_to_file("add_timestamps_to_conversation - Updated steps: ", $updated_steps);
return $updated_steps;
}
/**
* Generate a semi-randomized timestamp with a random date and time.
*
* @param string $start_date The start date in 'Y-m-d' format. Defaults to today.
* @param string $end_date The end date in 'Y-m-d' format. Defaults to today + 7 days.
* @param string $start_time The start time in 'H:i' format (e.g., '08:00'). Defaults to '08:00'.
* @param string $end_time The end time in 'H:i' format (e.g., '18:00'). Defaults to '18:00'.
* @return int The random timestamp.
*/
private static function generate_random_date_time($start_date = null, $end_date = null, $filled_dates = [], $start_time = '07:20', $end_time = '21:00' ) {
// Default dates
$start_date = $start_date ?: date('Y-m-d H:i');
$end_date = $end_date ?: date('Y-m-d H:i', strtotime($start_date . ' +8 days'));
// log_to_file("generate_random_date_time - Generating random date time between $start_date & $end_date");
$start_date_timestamp = strtotime($start_date);
$end_date_timestamp = strtotime($end_date);
$retry = false;
if ($end_date_timestamp < $start_date_timestamp) {
throw new Exception('generate_random_date_time - End date must be greater than or equal to start date.');
} else if ( $end_date_timestamp === $start_date_timestamp ) {
// Single-part conversations have the same start & end date
$random_date = date('Y-m-d', $start_date_timestamp);
// log_to_file("generate_random_date_time - Using the same start & end date");
} else {
// Generate a random date within the timeframe
// log_to_file("generate_random_date_time - Finding a date that's not in \$filled_dates");
do {
// if ($retry) {
// // log_to_file("generate_random_date_time - Regenerating random date because it was found in filled_dates.");
// }
// Generate a random date between the start and end dates
$random_date_timestamp = mt_rand($start_date_timestamp, $end_date_timestamp);
$random_date = date('Y-m-d', $random_date_timestamp);
// $retry = true;
// log_to_file("generate_random_date_time - Random date $random_date");
// log_to_file("generate_random_date_time - Filled Dates: ", $filled_dates);
} while (in_array($random_date, $filled_dates));
// log_to_file("generate_random_date_time - Random date found!");
}
// Generate a random time within the specified range for the selected date
// log_to_file("generate_random_date_time - calling generate_random_time( $random_date, $start_time, $end_time)");
$random_time_timestamp = self::generate_random_time($random_date, $start_time, $end_time);
// log_to_file("generate_random_date_time - Returned random timestamp: $random_time_timestamp");
return $random_time_timestamp;
}
/**
* Generate a random timestamp for a given date between specific hours.
*
* @param string $date The date in 'Y-m-d' format.
* @param string $start_time The start time in 'H:i' format.
* @param string $end_time The end time in 'H:i' format.
* @return int The random timestamp.
*/
private static function generate_random_time($date, $start_time, $end_time) {
// Convert start and end times to timestamps
$start_timestamp = strtotime("$date $start_time");
$end_timestamp = strtotime("$date $end_time");
// log_to_file("generate_random_time - Start: $start_timestamp End: $end_timestamp");
if ($end_timestamp < $start_timestamp) {
throw new Exception('generate_random_time - End time must be greater or equal to start time.');
}
// Generate a random timestamp within the time range
$timestamp = mt_rand($start_timestamp, $end_timestamp);
// log_to_file("generate_random_time - Random Timestamp: $timestamp");
return $timestamp;
}
/**
* Update the conversation steps for a given conversation ID.
*
* @param int $conversation_id The ID of the conversation to update.
* @param string $ai_response The AI-generated response in JSON format.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
public static function update_conversation_steps($conversation_id, $ai_response) {
global $wpdb;
// log_to_file("update_conversation_steps - Running! $conversation_id ", $ai_response);
// Validate input
if (empty($conversation_id) || !is_int($conversation_id)) {
return new WP_Error('invalid_conversation_id', __('Invalid conversation ID.', 'rl-mailwarmer'));
}
if (empty($ai_response) || !is_string($ai_response)) {
return new WP_Error('invalid_ai_response', __('Invalid AI response.', 'rl-mailwarmer'));
}
// Define the table name
$table_name = $wpdb->prefix . 'rl_mailwarmer_conversation';
// Prepare the data
$data = [
'conversation_steps' => $ai_response, // Save the JSON response as a string
];
$where = [
'id' => $conversation_id,
];
// Update the database
$result = $wpdb->update($table_name, $data, $where, ['%s'], ['%d']);
if ($result === false) {
return new WP_Error('db_update_failed', __('Failed to update the conversation steps.', 'rl-mailwarmer'));
}
// log_to_file("update_conversation_steps - Updated conversation steps for #$conversation_id");
return true;
}
}
/**
* Add a meta box for generating the campaign timeline.
*/
add_action('add_meta_boxes', function () {
add_meta_box(
'generate_campaign_timeline',
__('Generate Timeline', 'rl-mailwarmer'),
'rl_mailwarmer_render_generate_timeline_box',
'campaign',
'side',
'default'
);
});
/**
* Render the "Generate Timeline" meta box.
*
* @param WP_Post $post The current post object.
*/
function rl_mailwarmer_render_generate_timeline_box($post)
{
// Add a nonce for security
wp_nonce_field('generate_timeline_nonce', 'generate_timeline_nonce_field');
// Render the form
?>
<form id="generate-timeline-form">
<p>
<button type="button" id="generate-timeline-button" class="button button-primary">
<?php esc_html_e('Generate Timeline', 'rl-mailwarmer'); ?>
</button>
</p>
<div id="generate-timeline-result"></div>
</form>
<?php
}
add_action('admin_enqueue_scripts', function ($hook) {
global $post;
// Ensure this is the Add/Edit Campaign screen
if (($hook === 'post.php' || $hook === 'post-new.php') && $post->post_type === 'campaign') {
wp_enqueue_script(
'rl-mailwarmer-generate-timeline-js',
RL_MAILWARMER_URL . 'js/generate-timeline.js', // Adjust path as needed
['jquery'],
null,
true
);
wp_localize_script('rl-mailwarmer-generate-timeline-js', 'rlMailWarmerGenerateTimeline', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('generate_timeline_nonce'),
]);
}
});
add_action('wp_ajax_rl_mailwarmer_generate_timeline', function () {
// Verify nonce
if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'generate_timeline_nonce')) {
wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer'));
}
// Validate and sanitize input
$post_id = intval($_POST['post_id']);
if (!$post_id || get_post_type($post_id) !== 'campaign') {
wp_send_json_error(__('Invalid campaign ID.', 'rl-mailwarmer'));
}
// Generate the timeline
try {
$timeline = RL_MailWarmer_Campaign_Helper::calculate_campaign_timeline($post_id);
// log_to_file("wp_ajax_rl_mailwarmer_generate_timeline - Generated Timeline: ", $timeline);
// update_post_meta($post_id, "campaign_timeline", $timeline);
wp_send_json_success($timeline);
} catch (Exception $e) {
wp_send_json_error($e->getMessage());
}
});
/*
* Generate Conversation Metabox
*
*/
add_action('add_meta_boxes', function () {
add_meta_box(
'generate_conversation_box', // Unique ID for the meta box
__('Generate Conversation', 'rl-mailwarmer'), // Title of the meta box
'rl_mailwarmer_render_conversation_box', // Callback to display the box content
'campaign', // Post type
'side', // Context: side, normal, or advanced
'default' // Priority
);
});
/**
* Render the Generate Conversation meta box.
*
* @param WP_Post $post The current post object.
*/
function rl_mailwarmer_render_conversation_box($post) {
// Add a nonce for security
wp_nonce_field('rl_generate_conversation_nonce', 'rl_generate_conversation_nonce_field');
// Meta box form
?>
<div>
<label for="initiated_by"><?php esc_html_e('Initiated By (optional)', 'rl-mailwarmer'); ?></label>
<input type="text" id="initiated_by" name="initiated_by" class="widefat" placeholder="Email address or account ID">
<label for="subject"><?php esc_html_e('Subject (optional)', 'rl-mailwarmer'); ?></label>
<input type="text" id="subject" name="subject" class="widefat" placeholder="Subject line">
<label for="length"><?php esc_html_e('Length (optional)', 'rl-mailwarmer'); ?></label>
<select id="length" name="length" class="widefat">
<option value=""><?php esc_html_e('Select length', 'rl-mailwarmer'); ?></option>
<option value="extra-long"><?php esc_html_e('Extra Long', 'rl-mailwarmer'); ?></option>
<option value="long"><?php esc_html_e('Long', 'rl-mailwarmer'); ?></option>
<option value="medium"><?php esc_html_e('Medium', 'rl-mailwarmer'); ?></option>
<option value="short"><?php esc_html_e('Short', 'rl-mailwarmer'); ?></option>
<option value="single"><?php esc_html_e('Single', 'rl-mailwarmer'); ?></option>
</select>
<button type="button" id="generate-conversation" class="button button-primary" style="margin-top: 10px;">
<?php esc_html_e('Generate Conversation', 'rl-mailwarmer'); ?>
</button>
</div>
<?php
}
add_action('admin_enqueue_scripts', function ($hook) {
global $post;
if (($hook === 'post.php' || $hook === 'post-new.php') && $post->post_type === 'campaign') {
wp_enqueue_script(
'rl-generate-conversation',
RL_MAILWARMER_URL . 'js/generate-conversation.js',
['jquery'],
null,
true
);
wp_localize_script('rl-generate-conversation', 'rlGenerateConversation', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('rl_generate_conversation_nonce'),
'post_id' => $post->ID,
]);
}
});
add_action('wp_ajax_rl_generate_conversation', function () {
check_ajax_referer('rl_generate_conversation_nonce', 'nonce');
$campaign_id = intval($_POST['post_id']);
$args = [
'initiated_by' => sanitize_text_field($_POST['initiated_by'] ?? ''),
'subject' => sanitize_text_field($_POST['subject'] ?? ''),
'length' => sanitize_text_field($_POST['length'] ?? ''),
];
$result = RL_MailWarmer_Campaign_Helper::generate_conversation($campaign_id, $args);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success(['conversation_id' => $result]);
});
/**
* Add the "Generate Blueprint" metabox to the Campaign edit page.
*/
add_action('add_meta_boxes', function () {
add_meta_box(
'rl_mailwarmer_generate_blueprint',
__('Generate Blueprint', 'rl-mailwarmer'),
'rl_mailwarmer_render_generate_blueprint_metabox',
'campaign', // Post type
'side',
'default'
);
});
/**
* Render the "Generate Blueprint" metabox.
*
* @param WP_Post $post The current post object.
*/
function rl_mailwarmer_render_generate_blueprint_metabox($post) {
wp_nonce_field('rl_mailwarmer_generate_blueprint_nonce', 'rl_mailwarmer_generate_blueprint_nonce_field');
?>
<label for="blueprint_length"><?php esc_html_e('Number of Responses:', 'rl-mailwarmer'); ?></label>
<input
type="number"
id="blueprint_length"
name="blueprint_length"
value="1"
min="1"
step="1"
class="widefat"
/>
<button id="generate-blueprint" class="button button-primary" style="margin-top: 10px;">
<?php esc_html_e('Generate Blueprint', 'rl-mailwarmer'); ?>
</button>
<div id="blueprint-result" style="margin-top: 10px;"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const button = document.getElementById('generate-blueprint');
const resultDiv = document.getElementById('blueprint-result');
const inputLength = document.getElementById('blueprint_length');
button.addEventListener('click', function (e) {
e.preventDefault();
button.disabled = true;
resultDiv.textContent = '<?php esc_html_e('Processing...', 'rl-mailwarmer'); ?>';
const length = inputLength.value;
jQuery.post(ajaxurl, {
action: 'rl_mailwarmer_generate_blueprint',
post_id: <?php echo esc_js($post->ID); ?>,
length: length,
security: '<?php echo esc_js(wp_create_nonce('rl_mailwarmer_generate_blueprint_nonce')); ?>'
}, function (response) {
button.disabled = false;
if (response.success) {
resultDiv.textContent = '<?php esc_html_e('Blueprint generated successfully!', 'rl-mailwarmer'); ?>';
} else {
resultDiv.textContent = '<?php esc_html_e('Error:', 'rl-mailwarmer'); ?> ' + response.data.message;
}
});
});
});
</script>
<?php
}
/**
* AJAX handler for generating conversation blueprints.
*/
add_action('wp_ajax_rl_mailwarmer_generate_blueprint', function () {
// Verify nonce
if (!check_ajax_referer('rl_mailwarmer_generate_blueprint_nonce', 'security', false)) {
wp_send_json_error(['message' => __('Invalid nonce.', 'rl-mailwarmer')]);
}
// Check permissions
if (!current_user_can('edit_post', $_POST['post_id'])) {
wp_send_json_error(['message' => __('Permission denied.', 'rl-mailwarmer')]);
}
// Validate inputs
$post_id = intval($_POST['post_id']);
$length = intval($_POST['length']);
if ($length < 1) {
wp_send_json_error(['message' => __('Invalid length specified.', 'rl-mailwarmer')]);
}
try {
// Generate the blueprint
RL_MailWarmer_Campaign_Helper::generate_conversation_blueprint($post_id, $length);
wp_send_json_success();
} catch (Exception $e) {
wp_send_json_error(['message' => $e->getMessage()]);
}
});
/**
* Add a flexbox grid displaying the campaign timeline with gradient colors based on intensity.
*/
add_action('edit_form_after_title', 'rl_mailwarmer_display_campaign_timeline');
function rl_mailwarmer_display_campaign_timeline($post) {
// log_to_file("rl_mailwarmer_display_campaign_timeline - Running for {$post->post_title}");
if ($post->post_type !== 'campaign') {
return;
}
// Fetch campaign timeline
$campaign_timeline = get_post_meta($post->ID, 'campaign_timeline', true);
if (empty($campaign_timeline)) {
echo '<p>' . esc_html__('No timeline available for this campaign.', 'rl-mailwarmer') . '</p>';
return;
}
$campaign_timeline = json_decode($campaign_timeline, true);
// Organize timeline by weeks for grid structure
$weeks = [];
$max_volume = 0;
foreach ($campaign_timeline as $date => $day) {
$max_volume = max($max_volume, $day['target_volume']);
$week_number = date('W', strtotime($date));
if (!isset($weeks[$week_number])) {
$weeks[$week_number] = [];
}
$weeks[$week_number][$date] = $day;
}
// log_to_file("edit_form_after_title - Weeks array: ", $weeks);
// Add padding days and flatten
$grid_columns = [];
foreach ($weeks as $week_number => $week) {
// Get the first date of this week
$first_date = array_key_first($week);
$first_day_num = date('N', strtotime($first_date)); // 1 (Monday) through 7 (Sunday)
// Add padding days before if needed
if ($first_day_num > 1) {
$padding_days = $first_day_num - 1;
$monday_date = date('Y-m-d', strtotime("-{$padding_days} days", strtotime($first_date)));
for ($i = 0; $i < $padding_days; $i++) {
$pad_date = date('Y-m-d', strtotime("+{$i} days", strtotime($monday_date)));
$grid_columns[$pad_date] = [
'target_volume' => 0,
'is_padding' => true
];
}
}
// Add actual days
foreach ($week as $date => $day) {
$day['is_padding'] = false;
$grid_columns[$date] = $day;
}
}
// log_to_file("rl_mailwarmer_display_campaign_timeline - Grid Columns: ", $grid_columns);
$dates = array_keys($grid_columns);
$volumes = array_map(function($day) {
return (isset($day['is_padding']) && $day['is_padding'] == true ) ? 0 : $day;
}, $grid_columns);
// log_to_file("rl_mailwarmer_display_campaign_timeline - Grid volumes: ", $volumes);
$chart_data = array_map(function($date, $volume) {
return [
'date' => date('M d', strtotime($date)),
'target' => isset($volume['target_volume']) ? $volume['target_volume'] : null ,
'sent' => isset($volume['items_sent']) ? $volume['items_sent'] : null
];
}, $dates, $volumes);
// log_to_file("Chart Data: ", $chart_data);
// Gradient colors array
// $gradients = ["#f12711", "#f24913", "#f36b15", "#f48d17", "#f5af19", "#f8c353", "#fad78c", "#fdebc6", "#ffffff"];
$gradients = ["#ffffff", "#fdebc6", "#fad78c", "#f8c353", "#f5af19", "#f48d17", "#f36b15", "#f24913", "#f12711"];
// Generate the grid
?>
<div class="timeline-grid">
<?php
foreach ($grid_columns as $date => $day):
if (!empty($day['is_padding'])):
?>
<div class="day padding">
<div class="date"><?php echo esc_html(date('M d, Y', strtotime($date))); ?></div>
<div class="volume">-</div>
</div>
<?php
else:
$intensity = ($day['target_volume'] / $max_volume) * 100;
$gradient_index = min(floor($intensity / (100 / (count($gradients) - 1))), count($gradients) - 1);
$background_color = $gradients[$gradient_index];
?>
<div class="day" style="background-color: <?php echo esc_attr($background_color); ?>;">
<div class="date"><?php echo esc_html(date('M d, Y', strtotime($date))); ?></div>
<div class="volume"><?php echo esc_html($day['target_volume']); ?> Emails</div>
</div>
<?php
endif;
endforeach;
?>
</div>
<canvas id="timelineChart" style="max-height: 300px;"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('timelineChart');
const data = <?php echo json_encode($chart_data); ?>;
new Chart(ctx, {
data: {
labels: data.map(row => row.date),
datasets: [{
type: 'bar',
label: 'Target Volume',
data: data.map(row => row.target),
backgroundColor: 'rgb(255, 0, 0)',
borderColor: 'rgb(255, 0, 0)',
borderWidth: 1,
order: 10,
fill: false
}, {
type: 'bar',
label: 'Emails Sent',
data: data.map(row => row.sent),
backgroundColor: 'rgba(0, 255, 0)',
borderColor: 'rgba(0, 255, 0, 0.8)',
borderWidth: 1,
order: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Number of Emails'
}
},
x: {
stacked: true
}
}
}
});
</script>
<?php
}
// Cal-Heatmap
// add_action('edit_form_after_title', function ($post) {
// if ($post->post_type === 'campaign') {
// echo '<div id="campaign-timeline-heatmap" style="width: 100%; height: 300px; border:1px solid #fff;"></div>';
// }
// });
// add_action('admin_enqueue_scripts', function ($hook) {
// global $post;
// if (($hook === 'post.php' || $hook === 'post-new.php') && $post->post_type === 'campaign') {
// // Enqueue D3.js (required for Cal-Heatmap)
// wp_enqueue_script(
// 'd3-js',
// 'https://d3js.org/d3.v7.min.js',
// [],
// '7.0.0',
// true
// );
// // Enqueue Cal-Heatmap
// wp_enqueue_script(
// 'cal-heatmap',
// 'https://unpkg.com/cal-heatmap/dist/cal-heatmap.min.js',
// ['d3-js'],
// '4.2.4',
// true
// );
// // Enqueue Cal-Heatmap CSS
// wp_enqueue_style(
// 'cal-heatmap',
// 'https://unpkg.com/cal-heatmap/dist/cal-heatmap.css',
// [],
// '4.2.4'
// );
// // Custom heatmap script
// wp_enqueue_script(
// 'rl-mailwarmer-campaign-heatmap',
// RL_MAILWARMER_URL . 'js/campaign-timeline-heatmap.js',
// ['cal-heatmap', 'jquery'],
// null,
// true
// );
// // Pass data to the script
// wp_localize_script('rl-mailwarmer-campaign-heatmap', 'rlMailWarmerHeatmap', [
// 'ajax_url' => admin_url('admin-ajax.php'),
// 'nonce' => wp_create_nonce('campaign_timeline_nonce'),
// 'post_id' => $post->ID,
// ]);
// }
// });
// add_action('wp_ajax_rl_mailwarmer_get_timeline', function () {
// // Verify nonce
// if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'campaign_timeline_nonce')) {
// wp_send_json_error(__('Invalid nonce', 'rl-mailwarmer'));
// }
// // Validate and sanitize input
// $post_id = intval($_POST['post_id']);
// if (!$post_id || get_post_type($post_id) !== 'campaign') {
// wp_send_json_error(__('Invalid campaign ID.', 'rl-mailwarmer'));
// }
// // Retrieve the timeline
// $timeline_json = get_post_meta($post_id, 'campaign_timeline', true);
// $timeline = json_decode($timeline_json, true);
// if (!$timeline) {
// wp_send_json_error(__('No timeline data found.', 'rl-mailwarmer'));
// }
// // Convert timeline to Cal-Heatmap format (timestamp => value)
// $heatmap_data = [];
// foreach ($timeline as $date ) {
// $timestamp = "'" . strtotime($date['date']) . "'"; // Convert to UNIX timestamp
// $heatmap_data[$timestamp] = $date['target_volume'];
// }
// wp_send_json_success($timeline);
// });