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 ?>
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 ?>' . esc_html__('No timeline available for this campaign.', 'rl-mailwarmer') . '
'; 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 ?>