mirror of
https://github.com/freescout-helpdesk/freescout.git
synced 2024-11-24 19:33:07 +01:00
577 lines
16 KiB
PHP
577 lines
16 KiB
PHP
<?php
|
||
|
||
namespace App;
|
||
|
||
use App\Attachment;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
|
||
class Thread extends Model
|
||
{
|
||
/**
|
||
* By whom action performed (source_via).
|
||
*/
|
||
const PERSON_CUSTOMER = 1;
|
||
const PERSON_USER = 2;
|
||
|
||
public static $persons = [
|
||
self::PERSON_CUSTOMER => 'customer',
|
||
self::PERSON_USER => 'user',
|
||
];
|
||
|
||
/**
|
||
* Thread types.
|
||
*/
|
||
// Email from customer
|
||
const TYPE_CUSTOMER = 1;
|
||
// Thead created by user
|
||
const TYPE_MESSAGE = 2;
|
||
const TYPE_NOTE = 3;
|
||
// Thread status change
|
||
const TYPE_LINEITEM = 4;
|
||
const TYPE_PHONE = 5;
|
||
// Forwarded threads
|
||
const TYPE_FORWARDPARENT = 6;
|
||
const TYPE_FORWARDCHILD = 7;
|
||
const TYPE_CHAT = 8;
|
||
|
||
public static $types = [
|
||
// Thread by customer
|
||
self::TYPE_CUSTOMER => 'customer',
|
||
// Thread by user
|
||
self::TYPE_MESSAGE => 'message',
|
||
self::TYPE_NOTE => 'note',
|
||
// lineitem represents a change of state on the conversation. This could include, but not limited to, the conversation was assigned, the status changed, the conversation was moved from one mailbox to another, etc. A line item won’t have a body, to/cc/bcc lists, or attachments.
|
||
self::TYPE_LINEITEM => 'lineitem',
|
||
self::TYPE_PHONE => 'phone',
|
||
// When a conversation is forwarded, a new conversation is created to represent the forwarded conversation.
|
||
// forwardparent is the type set on the thread of the original conversation that initiated the forward event.
|
||
self::TYPE_FORWARDPARENT => 'forwardparent',
|
||
// forwardchild is the type set on the first thread of the new forwarded conversation.
|
||
self::TYPE_FORWARDCHILD => 'forwardchild',
|
||
self::TYPE_CHAT => 'chat',
|
||
];
|
||
|
||
/**
|
||
* Statuses (code must be equal to conversations statuses).
|
||
*/
|
||
const STATUS_ACTIVE = 1;
|
||
const STATUS_PENDING = 2;
|
||
const STATUS_CLOSED = 3;
|
||
const STATUS_SPAM = 4;
|
||
const STATUS_NOCHANGE = 6;
|
||
|
||
public static $statuses = [
|
||
self::STATUS_ACTIVE => 'active',
|
||
self::STATUS_CLOSED => 'closed',
|
||
self::STATUS_NOCHANGE => 'nochange',
|
||
self::STATUS_PENDING => 'pending',
|
||
self::STATUS_SPAM => 'spam',
|
||
];
|
||
|
||
/**
|
||
* States.
|
||
*/
|
||
const STATE_DRAFT = 1;
|
||
const STATE_PUBLISHED = 2;
|
||
const STATE_HIDDEN = 3;
|
||
// A state of review means the thread has been stopped by Traffic Cop and is waiting
|
||
// to be confirmed (or discarded) by the person that created the thread.
|
||
const STATE_REVIEW = 4;
|
||
|
||
public static $states = [
|
||
self::STATE_DRAFT => 'draft',
|
||
self::STATE_PUBLISHED => 'published',
|
||
self::STATE_HIDDEN => 'hidden',
|
||
self::STATE_REVIEW => 'review',
|
||
];
|
||
|
||
/**
|
||
* Action associated with the line item.
|
||
*/
|
||
// Conversation's status changed
|
||
const ACTION_TYPE_STATUS_CHANGED = 1;
|
||
// Conversation's assignee changed
|
||
const ACTION_TYPE_USER_CHANGED = 2;
|
||
// The conversation was moved from another mailbox
|
||
const ACTION_TYPE_MOVED_FROM_MAILBOX = 3;
|
||
// Another conversation was merged with this conversation
|
||
const ACTION_TYPE_MERGED = 4;
|
||
// The conversation was imported (no email notifications were sent)
|
||
const ACTION_TYPE_IMPORTED = 5;
|
||
// A workflow was run on this conversation (either automatic or manual)
|
||
const ACTION_TYPE_WORKFLOW_MANUAL = 6;
|
||
const ACTION_TYPE_WORKFLOW_AUTO = 7;
|
||
// The ticket was imported from an external Service
|
||
const ACTION_TYPE_IMPORTED_EXTERNAL = 8;
|
||
// Conversation customer changed
|
||
const ACTION_TYPE_CUSTOMER_CHANGED = 9;
|
||
// The ticket was deleted
|
||
const ACTION_TYPE_DELETED_TICKET = 10;
|
||
// The ticket was restored
|
||
const ACTION_TYPE_RESTORE_TICKET = 11;
|
||
|
||
// Describes an optional action associated with the line item
|
||
// todo: values need to be checked via HelpScout API
|
||
public static $action_types = [
|
||
self::ACTION_TYPE_STATUS_CHANGED => 'changed-ticket-status',
|
||
self::ACTION_TYPE_USER_CHANGED => 'changed-ticket-assignee',
|
||
self::ACTION_TYPE_MOVED_FROM_MAILBOX => 'moved-from-mailbox',
|
||
self::ACTION_TYPE_MERGED => 'merged',
|
||
self::ACTION_TYPE_IMPORTED => 'imported',
|
||
self::ACTION_TYPE_WORKFLOW_MANUAL => 'manual-workflow',
|
||
self::ACTION_TYPE_WORKFLOW_AUTO => 'automatic-workflow',
|
||
self::ACTION_TYPE_IMPORTED_EXTERNAL => 'imported-external',
|
||
self::ACTION_TYPE_CUSTOMER_CHANGED => 'changed-ticket-customer',
|
||
self::ACTION_TYPE_DELETED_TICKET => 'deleted-ticket',
|
||
self::ACTION_TYPE_RESTORE_TICKET => 'restore-ticket',
|
||
];
|
||
|
||
/**
|
||
* Source types (equal to thread source types).
|
||
*/
|
||
const SOURCE_TYPE_EMAIL = 1;
|
||
const SOURCE_TYPE_WEB = 2;
|
||
const SOURCE_TYPE_API = 3;
|
||
|
||
public static $source_types = [
|
||
self::SOURCE_TYPE_EMAIL => 'email',
|
||
self::SOURCE_TYPE_WEB => 'web',
|
||
self::SOURCE_TYPE_API => 'api',
|
||
];
|
||
|
||
/**
|
||
* The user assigned to this thread (assignedTo).
|
||
*/
|
||
public function user()
|
||
{
|
||
return $this->belongsTo('App\User');
|
||
}
|
||
|
||
/**
|
||
* The user assigned to this thread (cached).
|
||
*/
|
||
public function user_cached()
|
||
{
|
||
return $this->user()->rememberForever();
|
||
}
|
||
|
||
/**
|
||
* Get the thread customer.
|
||
*/
|
||
public function customer()
|
||
{
|
||
return $this->belongsTo('App\Customer');
|
||
}
|
||
|
||
/**
|
||
* Get the thread customer (cached).
|
||
*/
|
||
public function customer_cached()
|
||
{
|
||
return $this->customer()->rememberForever();
|
||
}
|
||
|
||
/**
|
||
* Get conversation.
|
||
*/
|
||
public function conversation()
|
||
{
|
||
return $this->belongsTo('App\Conversation');
|
||
}
|
||
|
||
/**
|
||
* Get thread attachmets.
|
||
*/
|
||
public function attachments()
|
||
{
|
||
return $this->hasMany('App\Attachment')->where('embedded', false);
|
||
//return $this->hasMany('App\Attachment');
|
||
}
|
||
|
||
/**
|
||
* Get thread embedded attachments.
|
||
*/
|
||
public function embeds()
|
||
{
|
||
return $this->hasMany('App\Attachment')->where('embedded', true);
|
||
}
|
||
|
||
/**
|
||
* All kinds of attachments including embedded.
|
||
*/
|
||
public function all_attachments()
|
||
{
|
||
return $this->hasMany('App\Attachment');
|
||
}
|
||
|
||
/**
|
||
* Get user who created the thread.
|
||
*/
|
||
public function created_by_user()
|
||
{
|
||
return $this->belongsTo('App\User');
|
||
}
|
||
|
||
/**
|
||
* Get user who created the thread (cached)
|
||
*/
|
||
public function created_by_user_cached()
|
||
{
|
||
return $this->created_by_user()->rememberForever();
|
||
}
|
||
|
||
/**
|
||
* Get customer who created the thread.
|
||
*/
|
||
public function created_by_customer()
|
||
{
|
||
return $this->belongsTo('App\Customer');
|
||
}
|
||
|
||
/**
|
||
* Get user who edited draft.
|
||
*/
|
||
public function edited_by_user()
|
||
{
|
||
return $this->belongsTo('App\User');
|
||
}
|
||
|
||
/**
|
||
* Get sanitized body HTML.
|
||
*
|
||
* @return string
|
||
*/
|
||
public function getCleanBody()
|
||
{
|
||
$body = \Purifier::clean($this->body);
|
||
|
||
// Remove all kinds of spaces after tags
|
||
// https://stackoverflow.com/questions/3230623/filter-all-types-of-whitespace-in-php
|
||
$body = preg_replace("/^(.*)>[\r\n]*\s+/mu", '$1>', $body);
|
||
|
||
return $body;
|
||
}
|
||
|
||
/**
|
||
* Get thread recipients.
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getToArray($exclude_array = [])
|
||
{
|
||
if ($this->to) {
|
||
$to_array = json_decode($this->to);
|
||
if ($to_array && $exclude_array) {
|
||
$to_array = array_diff($to_array, $exclude_array);
|
||
}
|
||
return $to_array;
|
||
} else {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
public function getToString($exclude_array = [])
|
||
{
|
||
return implode(', ', $this->getToArray($exclude_array));
|
||
}
|
||
|
||
/**
|
||
* Get first address from the To list.
|
||
*/
|
||
public function getToFirst()
|
||
{
|
||
$to = $this->getToArray();
|
||
return array_shift($to);
|
||
}
|
||
|
||
/**
|
||
* Get type name.
|
||
*/
|
||
public function getTypeName()
|
||
{
|
||
return self::$types[$this->type];
|
||
}
|
||
|
||
/**
|
||
* Get thread CC recipients.
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getCcArray($exclude_array = [])
|
||
{
|
||
if ($this->cc) {
|
||
$cc_array = json_decode($this->cc);
|
||
if ($cc_array && $exclude_array) {
|
||
$cc_array = array_diff($cc_array, $exclude_array);
|
||
}
|
||
return $cc_array;
|
||
} else {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
public function getCcString($exclude_array = [])
|
||
{
|
||
return implode(', ', $this->getCcArray($exclude_array));
|
||
}
|
||
|
||
/**
|
||
* Get thread BCC recipients.
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getBccArray($exclude_array = [])
|
||
{
|
||
if ($this->bcc) {
|
||
$bcc_array = json_decode($this->bcc);
|
||
if ($bcc_array && $exclude_array) {
|
||
$bcc_array = array_diff($bcc_array, $exclude_array);
|
||
}
|
||
return $bcc_array;
|
||
} else {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
public function getBccString($exclude_array = [])
|
||
{
|
||
return implode(', ', $this->getBccArray($exclude_array));
|
||
}
|
||
|
||
/**
|
||
* Set to as JSON.
|
||
*/
|
||
public function setTo($emails)
|
||
{
|
||
$emails_array = Conversation::sanitizeEmails($emails);
|
||
if ($emails_array) {
|
||
$emails_array = array_unique($emails_array);
|
||
$this->to = json_encode($emails_array);
|
||
} else {
|
||
$this->to = null;
|
||
}
|
||
}
|
||
|
||
public function setCc($emails)
|
||
{
|
||
$emails_array = Conversation::sanitizeEmails($emails);
|
||
if ($emails_array) {
|
||
$emails_array = array_unique($emails_array);
|
||
$this->cc = json_encode($emails_array);
|
||
} else {
|
||
$this->cc = null;
|
||
}
|
||
}
|
||
|
||
public function setBcc($emails)
|
||
{
|
||
$emails_array = Conversation::sanitizeEmails($emails);
|
||
if ($emails_array) {
|
||
$emails_array = array_unique($emails_array);
|
||
$this->bcc = json_encode($emails_array);
|
||
} else {
|
||
$this->bcc = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get thread's status name.
|
||
*
|
||
* @return string
|
||
*/
|
||
public function getStatusName()
|
||
{
|
||
return self::statusCodeToName($this->status);
|
||
}
|
||
|
||
/**
|
||
* Get status name. Made as a function to allow status names translation.
|
||
*
|
||
* @param int $status
|
||
*
|
||
* @return string
|
||
*/
|
||
public static function statusCodeToName($status)
|
||
{
|
||
switch ($status) {
|
||
case self::STATUS_ACTIVE:
|
||
return __('Active');
|
||
break;
|
||
|
||
case self::STATUS_PENDING:
|
||
return __('Pending');
|
||
break;
|
||
|
||
case self::STATUS_CLOSED:
|
||
return __('Closed');
|
||
break;
|
||
|
||
case self::STATUS_SPAM:
|
||
return __('Spam');
|
||
break;
|
||
|
||
case self::STATUS_NOCHANGE:
|
||
return __('Not changed');
|
||
break;
|
||
|
||
default:
|
||
return '';
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get text for the assignee.
|
||
*
|
||
* @return string
|
||
*/
|
||
public function getAssigneeName($ucfirst = false, $user = null, $self = true)
|
||
{
|
||
if (!$this->user_id) {
|
||
if ($ucfirst) {
|
||
return __('Anyone');
|
||
} else {
|
||
return __('anyone');
|
||
}
|
||
} elseif (($user && $this->user_id == $user->id) || (!$user && auth()->user() && $this->user_id == auth()->user()->id)) {
|
||
|
||
// Not using mb_ucfirst to avoid possible problems with encoding
|
||
if ($ucfirst) {
|
||
if ($self) {
|
||
$name = __('Yourself');
|
||
} else {
|
||
$name = __('You');
|
||
}
|
||
} else {
|
||
if ($self) {
|
||
$name = __('yourself');
|
||
} else {
|
||
$name = __('you');
|
||
}
|
||
}
|
||
|
||
return $name;
|
||
} else {
|
||
return $this->user->getFullName();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get user or customer who created the thead.
|
||
*/
|
||
public function getCreatedBy()
|
||
{
|
||
if (!empty($this->created_by_user_id)) {
|
||
return $this->created_by_user;
|
||
} else {
|
||
return $this->created_by_customer;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get creator of the thread.
|
||
*/
|
||
public function getPerson($cached = false)
|
||
{
|
||
if ($this->type == Thread::TYPE_CUSTOMER) {
|
||
if ($cached) {
|
||
return $this->customer_cached;
|
||
} else {
|
||
return $this->customer;
|
||
}
|
||
} else {
|
||
if ($cached) {
|
||
return $this->created_by_user_cached;
|
||
} else {
|
||
return $this->created_by_user;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Description of what happened.
|
||
*/
|
||
public function getActionDescription($conversation_number, $escape = true)
|
||
{
|
||
$person = '';
|
||
$did_this = '';
|
||
|
||
// Person
|
||
if ($this->type == Thread::TYPE_CUSTOMER) {
|
||
$person = $this->customer->getFullName(true);
|
||
} elseif ($this->state == Thread::STATE_DRAFT && !empty($this->edited_by_user_id)) {
|
||
// Draft
|
||
if (auth()->user() && $this->edited_by_user_id == auth()->user()->id) {
|
||
$person = __("you");
|
||
} else {
|
||
$person = $this->edited_by_user->getFullName();
|
||
}
|
||
} else {
|
||
if ($this->created_by_user_id && auth()->user() && $this->created_by_user_cached->id == auth()->user()->id) {
|
||
$person = __("you");
|
||
} else {
|
||
$person = $this->created_by_user_cached->getFullName();
|
||
}
|
||
}
|
||
|
||
// Did this
|
||
if ($this->type == Thread::TYPE_LINEITEM) {
|
||
if ($this->action_type == Thread::ACTION_TYPE_STATUS_CHANGED) {
|
||
$did_this = __("marked as :status_name conversation #:conversation_number", ['status_name' => $this->getStatusName(), 'conversation_number' => $conversation_number]);
|
||
} elseif ($this->action_type == Thread::ACTION_TYPE_USER_CHANGED) {
|
||
$did_this = __("assigned :assignee convsersation #:conversation_number", ['assignee' => $this->getAssigneeName(false, null, false), 'conversation_number' => $conversation_number]);
|
||
} elseif ($this->action_type == Thread::ACTION_TYPE_CUSTOMER_CHANGED) {
|
||
$did_this = __("changed the customer to :customer in conversation #:conversation_number", ['customer' => $this->customer->getFullName(true), 'conversation_number' => $conversation_number]);
|
||
}
|
||
} elseif ($this->state == Thread::STATE_DRAFT) {
|
||
if (empty($this->edited_by_user_id)) {
|
||
$did_this = __("created a draft");
|
||
} else {
|
||
$did_this = __("edited :creator's draft", ['creator' => $this->created_by_user_cached->getFirstName()]);
|
||
}
|
||
} else {
|
||
if ($this->first) {
|
||
$did_this = __("started a new conversation #:conversation_number", ['conversation_number' => $conversation_number]);
|
||
} elseif ($this->type == Thread::TYPE_NOTE) {
|
||
$did_this = __("added a note to conversation #:conversation_number", ['conversation_number' => $conversation_number]);
|
||
} else {
|
||
$did_this = __("replied to conversation #:conversation_number", ['conversation_number' => $conversation_number]);
|
||
}
|
||
}
|
||
|
||
$description = ':person_tag_start:person:person_tag_end :did_this';
|
||
if ($escape) {
|
||
$description = htmlspecialchars($description);
|
||
}
|
||
|
||
return __($description, [
|
||
'person' => $person,
|
||
'person_tag_start' => '<strong>',
|
||
'person_tag_end' => '</strong>',
|
||
'did_this' => $did_this
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Get thread state name.
|
||
*/
|
||
public function getStateName()
|
||
{
|
||
return self::$states[$this->state];
|
||
}
|
||
|
||
public function deleteThread()
|
||
{
|
||
$this->deteleAttachments();
|
||
$this->delete();
|
||
}
|
||
|
||
/**
|
||
* Delete thread attachments.
|
||
*/
|
||
public function deteleAttachments()
|
||
{
|
||
Attachment::deleteByIds($this->all_attachments()->pluck('id')->toArray());
|
||
}
|
||
}
|