1487 lines
59 KiB
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);
|
|
// });
|