person_type == 'agent') { $data['content'] = apply_filters('fluent_support/parse_smartcode_data', $data['content'], [ 'customer' => $ticket->customer, 'agent' => $person ]); } $convoType = Arr::get($data, 'conversation_type', 'response'); // Input is already unslashed (or was never slashed, for API/MCP callers). // Don't unslash again or literal backslashes are lost. $content = wp_kses_post($data['content']); $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content); $content = apply_filters('fluent_support/response_content_before_use_anywhere', $content); $response = [ 'person_id' => $person->id, 'ticket_id' => $ticket->id, 'conversation_type' => $convoType, 'content' => $content, 'source' => Arr::get($data, 'source', 'web'), 'content_hash' => md5($content) ]; if (!empty($data['message_id'])) { $response['message_id'] = sanitize_text_field($data['message_id']); } $response = apply_filters('fluent_support/new_' . $person->person_type . '_' . $convoType, $response, $ticket, $person); $createdResponse = \FluentSupport\App\Models\Conversation::create($response); if (!empty($cc_emails)) { $createdResponse->updateSettingsValue('cc_email', $cc_emails); // Keep a running list of every CC'd address on the ticket. $existingCcEmails = $ticket->getSettingsValue('all_cc_email', []); $newData = array_merge($existingCcEmails, $cc_emails); $ticket->updateSettingsValue('all_cc_email', array_unique($newData)); } $createdResponse->load('person'); if ($person->person_type == 'agent' && $ticket->status == 'new' && $convoType == 'response') { $ticket->status = 'active'; if ($ticket->created_at) { $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at); } else { $ticket->first_response_time = 300; } } $agentAdded = false; $previousAgentId = (int) $ticket->agent_id; $updateData = []; if ($person->person_type == 'agent') { if (!$ticket->agent_id && $convoType == 'response') { $ticket->agent_id = $person->id; $agentAdded = true; $updateData = [ 'agent_id' => $person->id ]; } if ($convoType == 'response') { if ($resetWaitingSince && !$informationalReply) { $ticket->last_agent_response = current_time('mysql'); $ticket->waiting_since = current_time('mysql'); } } } else { if ($ticket->last_agent_response && strtotime($ticket->last_agent_response) > strtotime($ticket->last_customer_response)) { $ticket->waiting_since = current_time('mysql'); } $ticket->last_customer_response = current_time('mysql'); } if ($convoType == 'response') { $ticket->response_count += 1; } $closed = false; if (Arr::get($data, 'close_ticket') == 'yes' && $ticket->status != 'closed') { $ticket->status = 'closed'; $ticket->resolved_at = current_time('mysql'); $ticket->closed_by = $person->id; $ticket->total_close_time = current_time('timestamp') - strtotime($ticket->created_at); $closed = true; $internalNote = __('Ticket has been closed', 'fluent-support'); $internalNote = apply_filters('fluent_support/ticket_close_internal_note', $internalNote, $ticket); if ($internalNote) { Conversation::create([ 'ticket_id' => $ticket->id, 'person_id' => $person->id, 'conversation_type' => 'internal_info', 'content' => $internalNote ]); } } $ticket->save(); Helper::tempImageMoveUploadDir($ticket->id, 'conversation', $createdResponse->id, $person->id); // Finalize files uploaded ahead of the response (status in-active). if ($attachmentHashes = Arr::get($data, 'attachments', [])) { $attachments = Attachment::where('ticket_id', $ticket->id) ->whereIn('file_hash', $attachmentHashes) ->where('status', 'in-active') ->get(); if (!$attachments->isEmpty()) { $storageDriver = Helper::getUploadDriverKey(); foreach ($attachments as $attachment) { if ($storageDriver != 'local') { do_action_ref_array('fluent_support/finalize_file_upload_' . $storageDriver, [&$attachment, $ticket->id]); } if ($attachment->driver == 'local') { $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticket->id); if ($newFileInfo && !empty($newFileInfo['file_path'])) { $attachment->file_path = $newFileInfo['file_path']; $attachment->full_url = $newFileInfo['url']; } } $attachment->ticket_id = $ticket->id; $attachment->person_id = $createdResponse->person_id; $attachment->conversation_id = $createdResponse->id; $attachment->status = 'active'; $attachment->save(); } $createdResponse->load('attachments'); } } if ($silently) { // The caller fires the hooks itself (fireResponseHooks()), e.g. after // its transaction commits; 'closed' / 'agent_added' tell it what happened. return [ 'response' => $createdResponse, 'ticket' => $ticket, 'update_data' => $updateData, 'closed' => $closed, 'agent_added' => $agentAdded, 'previous_agent_id' => $previousAgentId, ]; } $updateData = $this->fireResponseHooks($createdResponse, $ticket, $person, $closed, $agentAdded, $previousAgentId, $updateData); return [ 'response' => $createdResponse, 'ticket' => $ticket, 'update_data' => $updateData ]; } /** * Fire the hooks createResponse() fires after a response is stored. * * Split out so a caller that wrapped createResponse(..., $silently = true) in * a transaction can fire them once the rows are committed: the customer * email must not leave for a reply that is about to be rolled back. * * @param Conversation $createdResponse * @param \FluentSupport\App\Models\Ticket $ticket * @param \FluentSupport\App\Models\Person $person * @param bool $closed The response closed the ticket. * @param bool $agentAdded The response assigned the ticket to $person. * @param int $previousAgentId * @param array $updateData * @return array $updateData, with 'agent' when the assignment hook fired. */ public function fireResponseHooks($createdResponse, $ticket, $person, $closed, $agentAdded = false, $previousAgentId = 0, $updateData = []) { $convoType = $createdResponse->conversation_type; if ($agentAdded) { $assigner = Helper::getCurrentAgent(); $ticket->load('agent'); /* * Fires when a ticket is assigned to an agent. * * @since v1.0.0 * @param object $person * @param object $ticket * @param object $assigner * @param int $previousAgentId */ do_action('fluent_support/agent_assigned_to_ticket', $person, $ticket, $assigner, $previousAgentId); $updateData['agent'] = $ticket->agent; } /* * Fires after a response or note is added. * * @since v1.0.0 * @param string $convoType * @param string $personType * @param object $createdResponse * @param object $ticket * @param object $person */ do_action('fluent_support/' . $convoType . '_added_by_' . $person->person_type, $createdResponse, $ticket, $person); if ($closed) { /* * Fires when a ticket is closed. * * @since v1.0.0 * @param object $ticket * @param object $person */ do_action('fluent_support/ticket_closed', $ticket, $person); /* * Fires when a ticket is closed, per closer type. * * @since v1.0.0 * @param string $personType * @param object $ticket * @param object $person */ do_action('fluent_support/ticket_closed_by_' . $person->person_type, $ticket, $person); } return $updateData; } /** * Publish a draft reply: it becomes a sent response and the customer is notified. * Shared by the admin "Approve" action and MCP's publish-draft-reply. * * @param \FluentSupport\App\Models\Ticket $ticket * @param \FluentSupport\App\Models\Conversation $response The draft. * @param \FluentSupport\App\Models\Person $person The agent publishing it. * @param string $content Final content, already sanitized. * @return \FluentSupport\App\Models\Conversation */ public function publishDraft($ticket, $response, $person, $content) { $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content); $response->content = $content; $response->conversation_type = 'response'; $response->created_at = current_time('mysql'); $response->save(); if ($person->person_type == 'agent' && $ticket->status == 'new') { $ticket->status = 'active'; if ($ticket->created_at) { $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at); } else { $ticket->first_response_time = 300; } } if ($resetWaitingSince) { $ticket->last_agent_response = current_time('mysql'); $ticket->waiting_since = current_time('mysql'); } $ticket->response_count += 1; $ticket->save(); $author = $response->person_id ? Agent::find($response->person_id) : null; $sender = $author ?: $person; do_action('fluent_support/response_added_by_' . $sender->person_type, $response, $ticket, $sender); return $response; } /** * @deprecated 2.4.1 Replies no longer carry the signature. Email templates add it * with {{agent.signature}}, so the template decides the sign-off. */ public function maybeAppendSignature($content, $person) { $signature = self::getAppendedSignature($person); if ($signature === '') { return $content; } return $content . '