freescout-custom-workflow-user/Workflows/Entities/Workflow.php
2024-01-22 01:06:15 +01:00

1829 lines
71 KiB
PHP

<?php
namespace Modules\Workflows\Entities;
use Carbon\Carbon;
use Modules\Workflows\Entities\ConversationWorkflow;
use App\Conversation;
use App\Mailbox;
use App\Thread;
use App\User;
use Illuminate\Database\Eloquent\Model;
use Watson\Rememberable\Rememberable;
class Workflow extends Model
{
use Rememberable;
// This is obligatory.
public $rememberCacheDriver = 'array';
public static $userRunningTheWorkflow = null;
const TYPE_AUTOMATIC = 1;
const TYPE_MANUAL = 2;
// Action type.
const ACTION_TYPE_AUTOMATIC_WORKFLOW = 201;
const ACTION_TYPE_MANUAL_WORKFLOW = 202;
// Workflow user email.
const WF_USER_EMAIL = 'fsworkflow@example.org';
const ASSIGNEE_CURRENT = -10;
// User permission.
const PERM_EDIT_WORKFLOWS = 6;
public $validateErrors = null;
protected $fillable = [
'name', 'type', 'max_executions', 'apply_to_prev', 'active', 'conditions', 'actions'
];
protected $attributes = [
'type' => self::TYPE_AUTOMATIC,
'max_executions' => 1,
];
protected $casts = [
'conditions' => 'array',
'actions' => 'array',
];
protected $dates = [
'created_at',
];
public static $date_conditions = [
'created', 'waiting', 'user_reply', 'customer_reply'
];
public static $conditions_config = [];
public static $actions_config = [];
/**
* We have to store the last thread, as while processing workflows,
* new last thread may be added.
*/
public static $cond_last_thread = [];
/**
* Cached user.
*/
public static $wf_user = null;
/**
* Cached communication channels.
*/
public static $channels = null;
public static function conditionsConfig($mailbox_id)
{
if (!empty(self::$conditions_config[$mailbox_id])) {
return self::$conditions_config[$mailbox_id];
}
self::$conditions_config[$mailbox_id] = [
'people' => [
'title' => __('People'),
'items' => [
'customer_name' => [
'title' => __('Customer Name'),
'operators' => [
'equal' => __('Is equal to'),
'contains' => __('Contains'),
'not_contains' => __('Does not contain'),
'not_equal' => __('Is not equal to'),
'starts' => __('Starts with'),
'ends' => __('Ends with'),
'regex' => __('Matches regex pattern'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.moved',
]
],
'customer_email' => [
'title' => __('Customer Email'),
'operators' => [
'equal' => __('Is equal to'),
'contains' => __('Contains'),
'not_contains' => __('Does not contain'),
'not_equal' => __('Is not equal to'),
'starts' => __('Starts with'),
'ends' => __('Ends with'),
'regex' => __('Matches regex pattern'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.moved',
]
],
'user_action' => [
'title' => __('User Action'),
'operators' => [
'replied' => __('Replied'),
'noted' => __('Added a note'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_replied',
'conversation.note_added',
]
],
]
],
'conversation' => [
'title' => __('Conversation'),
'items' => [
'type' => [
'title' => __('Type'),
'operators' => [
'equal' => __('Is equal to'),
'not_equal' => __('Is not equal to'),
],
'values' => [
Conversation::TYPE_EMAIL => __('Email'),
Conversation::TYPE_PHONE => __('Phone'),
Conversation::TYPE_CHAT => __('Chat'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.moved',
]
],
'status' => [
'title' => __('Status'),
'operators' => [
'equal' => __('Is equal to'),
'not_equal' => __('Is not equal to'),
],
'values' => [
Conversation::STATUS_ACTIVE => __('Active'),
Conversation::STATUS_PENDING => __('Pending'),
Conversation::STATUS_CLOSED => __('Closed'),
Conversation::STATUS_SPAM => __('Spam'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.status_changed',
'conversation.moved',
]
],
'state' => [
'title' => __('State'),
'operators' => [
'equal' => __('Is equal to'),
'not_equal' => __('Is not equal to'),
],
'values' => [
Conversation::STATE_DRAFT => __('Draft'),
Conversation::STATE_PUBLISHED => __('Published'),
Conversation::STATE_DELETED => __('Deleted'),
],
'triggers' => [
'conversation.state_changed',
]
],
'user' => [
'title' => __('Assigned to User'),
'operators' => [
'equal' => __('Is equal to'),
'not_equal' => __('Is not equal to'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.user_changed',
'conversation.moved',
]
],
'to' => [
'title' => __('To'),
'operators' => [
'equal' => __('Is equal to'),
'contains' => __('Contains'),
'not_contains' => __('Does not contain'),
'not_equal' => __('Is not equal to'),
'starts' => __('Starts with'),
'ends' => __('Ends with'),
'regex' => __('Matches regex pattern'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.moved',
]
],
'cc' => [
'title' => __('Cc'),
'operators' => [
'equal' => __('Is equal to'),
'contains' => __('Contains'),
'not_contains' => __('Does not contain'),
'not_equal' => __('Is not equal to'),
'starts' => __('Starts with'),
'ends' => __('Ends with'),
'regex' => __('Matches regex pattern'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.moved',
]
],
'subject' => [
'title' => __('Subject'),
'operators' => [
'equal' => __('Is equal to'),
'contains' => __('Contains'),
'not_contains' => __('Does not contain'),
'not_equal' => __('Is not equal to'),
'starts' => __('Starts with'),
'ends' => __('Ends with'),
'regex' => __('Matches regex pattern'),
],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.moved',
]
],
'body' => [
'title' => __('Body'),
'operators' => [
'customer' => __('Customer message contains'),
'note' => __('Note contains'),
'regex' => __('Matches regex pattern'),
],
'triggers' => [
'conversation.created_by_customer',
'conversation.customer_replied',
'conversation.note_added',
'conversation.moved',
]
],
'headers' => [
'title' => __('Headers'),
'operators' => [
'contains' => __('Contains'),
'not_contains' => __('Does not contain'),
'regex' => __('Matches regex pattern'),
],
'triggers' => [
'conversation.created_by_customer',
'conversation.customer_replied',
]
],
'attachment' => [
'title' => __('Attachment'),
'operators' => [
'yes' => __('Has an attachment'),
'no' => __('Does not have an attachment'),
],
'values' => [],
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.user_replied',
'conversation.customer_replied',
'conversation.moved',
]
],
'bounce' => [
'title' => __('Is Bounce'),
'operators' => [
'yes' => __('Yes'),
'no' => __('No'),
],
'values' => [],
'triggers' => [
'conversation.created_by_customer',
]
],
'customer_viewed' => [
'title' => __('Customer Viewed'),
'operators' => [
'yes' => __('Yes'),
'no' => __('No'),
],
'values' => [],
'triggers' => [
'thread.opened',
]
],
'new_or_reply' => [
'title' => __('New / Reply / Moved'),
'operators' => [
'new' => __('New conversation created'),
'reply' => __('User or customer replied'),
'moved' => __('Conversation moved from another mailbox'),
],
'values' => [],
'triggers' => [
'conversation.created_by_customer',
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.customer_replied',
'conversation.user_replied',
'conversation.moved',
]
],
'channel' => [
'title' => __('Communication Channel'),
'operators' => [
'equal' => __('Is equal to'),
'not_equal' => __('Is not equal to'),
],
'values' => self::getChannels(),
'triggers' => [
'conversation.created_by_user',
'conversation.user_forwarded',
'conversation.created_by_customer',
'conversation.customer_replied',
'conversation.user_replied',
'conversation.moved',
]
],
],
],
'dates' => [
'title' => __('Dates'),
'items' => [
// 'exact_date' => [
// 'title' => __('Exact Date'),
// 'operators' => [
// 'before' => __('Is before'),
// 'after' => __('Is after'),
// //'between' => __('Is between'),
// ]
// ],
'waiting' => [
'title' => __('Waiting Since'),
'operators' => [
'longer' => __('Is longer than'),
'not_longer' => __('Is not longer than'),
]
],
'user_reply' => [
'title' => __('Last User Reply'),
'operators' => [
'in_last' => __('Is in the last'),
'not_in_last' => __('Is not in the last'),
],
'triggers' => [
'conversation.user_replied',
]
],
'customer_reply' => [
'title' => __('Last Customer Reply'),
'operators' => [
'in_last' => __('Is in the last'),
'not_in_last' => __('Is not in the last'),
]
],
'created' => [
'title' => __('Date Created'),
'operators' => [
'in_last' => __('Is in the last'),
'not_in_last' => __('Is not in the last'),
]
],
]
],
];
self::$conditions_config[$mailbox_id] = \Eventy::filter('workflows.conditions_config', self::$conditions_config[$mailbox_id], $mailbox_id);
return self::$conditions_config[$mailbox_id];
}
public static function actionsConfig($mailbox_id)
{
if (!empty(self::$actions_config[$mailbox_id])) {
return self::$actions_config[$mailbox_id];
}
self::$actions_config[$mailbox_id] = [
'dummy' => [
'items' => [
'notification' => [
'title' => __('Send Email Notification'),
// 'operators' => [
// 'assignee' => __('Current Assignee'),
// 'last' => __('Last User to Reply'),
// ],
// 'values' => []
],
'reply' => [
'title' => __('Reply to Conversation'),
],
'email_customer' => [
'title' => __('Email the Customer'),
],
'no_autoreply' => [
'title' => __('Disable Auto Reply'),
'values' => []
],
'forward' => [
'title' => __('Forward'),
],
'note' => [
'title' => __('Add a Note'),
],
'status' => [
'title' => __('Change Status'),
'values' => [
Conversation::STATUS_ACTIVE => __('Active'),
Conversation::STATUS_PENDING => __('Pending'),
Conversation::STATUS_CLOSED => __('Closed'),
Conversation::STATUS_SPAM => __('Spam'),
]
],
'assign' => [
'title' => __('Assign to User'),
'values' => [
Conversation::USER_UNASSIGNED => __('Unassigned'),
]
],
'move' => [
'title' => __('Move to Mailbox'),
],
'delete' => [
'title' => __('Move to Deleted Folder'),
'values' => []
],
'delete_forever' => [
'title' => __('Delete Forever'),
'values' => []
],
],
],
];
self::$actions_config[$mailbox_id] = \Eventy::filter('workflows.actions_config', self::$actions_config[$mailbox_id], $mailbox_id);
return self::$actions_config[$mailbox_id];
}
/**
* Get the mailbox to which conversation belongs.
*/
public function mailbox()
{
return $this->belongsTo('App\Mailbox');
}
public function setSortOrderLast()
{
$this->sort_order = (int)Workflow::max('sort_order')+1;
}
/**
* Get URL for editing user.
*
* @return string
*/
public function url()
{
return route('mailboxes.workflows.update', ['mailbox_id' => $this->mailbox_id, 'id' => $this->id]);
}
public function isComplete()
{
return $this->complete;
}
public function isAutomatic()
{
return $this->type == self::TYPE_AUTOMATIC;
}
public function isManual()
{
return $this->type == self::TYPE_MANUAL;
}
public static function formatConditions($conditions, $mailbox_id)
{
$result = [];
$row = 0;
foreach ($conditions as $list_i => $list) {
foreach ($list as $condition_i => $condition) {
$config = self::getConditionConfig($condition['type'], $mailbox_id);
if (empty($condition['type']) || empty($condition['operator'])
|| (!isset($condition['value'])
&& $config
&& empty($config['values_visible_if'])
&& (!isset($config['values']) || $config['values'] != [])
)
|| (!isset($condition['value'])
&& $config
&& !empty($config['values_visible_if'])
&& in_array($condition['operator'], $config['values_visible_if'])
)
) {
// Miss condition.
} else {
if (!isset($result[$row])) {
$result[$row] = [];
}
$result[$row][] = $condition;
}
}
if (empty($conditions[$list_i])) {
//unset($conditions[$list_i]);
} else {
$row++;
}
}
return $result;
}
public function hasDateConditions()
{
foreach ($this->conditions as $ands) {
foreach ($ands as $row) {
if (!empty($row['type']) && in_array($row['type'], self::$date_conditions)) {
return true;
}
}
}
return false;
}
/**
* Run all workflows for the mailbox.
*/
public static function processWorkflowsForMailbox($mailbox_id)
{
$workflows = Workflow::where('mailbox_id', $mailbox_id)
->where('active', true)
->where('type', self::TYPE_AUTOMATIC)
->orderBy('sort_order')
->get();
// Leave only date conditions, as others are triggered when action happens.
foreach ($workflows as $i => $workflow) {
if (!$workflow->hasDateConditions() && !$workflow->apply_to_prev) {
$workflows->forget($i);
}
}
if (!count($workflows)) {
return;
}
//$wf_ids = $workflows->pluck('id')->toArray();
// Select unprocessed conversations.
$executed_num = 0;
// Fetch conversations for each workflow.
foreach ($workflows as $workflow) {
$executed_num += $workflow->processForMailbox();
}
return $executed_num;
}
public function processForMailbox()
{
$workflow = $this;
$mailbox_id = $workflow->mailbox_id;
$executed_num = 0;
// Process conersations before or after workflow is created.
$page = 0;
do {
$conversations_query = Conversation::select('conversations.*')
->where('mailbox_id', $mailbox_id)
->where('state', '!=', Conversation::STATE_DELETED)
//->where('status', '!=', Conversation::STATUS_SPAM)
->leftJoin('conversation_workflow', function ($join) use ($workflow) {
$join->on('conversations.id', '=', 'conversation_workflow.conversation_id');
//$join->whereIn('conversation_workflow.workflow_id', $wf_ids);
$join->where('conversation_workflow.workflow_id', $workflow->id);
});
if ($workflow->max_executions <= 1) {
$conversations_query->where('conversation_workflow.workflow_id', null);
} else {
$conversations_query->where(function ($query) use ($workflow) {
$query->where('conversation_workflow.workflow_id', null)
->orWhere('conversation_workflow.counter', '<', $workflow->max_executions);
});
}
$conversations_query->skip($page*1000)->take(1000);
if (!$workflow->apply_to_prev) {
$conversations_query->where('created_at', '>=', $workflow->created_at);
}
$conversations = $conversations_query->get();
foreach ($conversations as $conversation) {
if ($workflow->checkPrevious($conversation) && $workflow->checkConditions($conversation)) {
$workflow->performActions($conversation);
$executed_num++;
}
}
$page++;
} while (count($conversations));
return $executed_num;
}
public static function runAutomaticForConversation($conversation, $trigger = '')
{
$workflows = Workflow::where('mailbox_id', $conversation->mailbox_id)
->where('active', true)
->where('type', self::TYPE_AUTOMATIC)
->orderBy('sort_order')
->get();
$conv_workflows = ConversationWorkflow::select('workflow_id', 'counter')
->whereIn('workflow_id', $workflows->pluck('id')->toArray())
->where('conversation_id', $conversation->id)
->get();
$processed_ids = $conv_workflows->pluck('workflow_id')->toArray();
// In performActions other workflows may be triggered, for example tag can be added.
$clean_last_thread = false;
if (empty(self::$cond_last_thread[$conversation->id])) {
$clean_last_thread = true;
}
foreach ($workflows as $workflow) {
if (!in_array($workflow->id, $processed_ids)
|| ($workflow->max_executions > 1
&& $conv_workflows->firstWhere('workflow_id', $workflow->id)->counter < $workflow->max_executions
)
) {
if ($workflow->checkConditions($conversation, $trigger)) {
$workflow->performActions($conversation);
}
}
}
if ($clean_last_thread) {
self::$cond_last_thread[$conversation->id] = [];
}
}
// public function processForAllMailboxes()
// {
// $mailboxes = Mailbox::getActiveMailboxes();
// foreach ($mailboxes as $mailbox) {
// $this->processForMailbox($mailbox->id);
// }
// }
/**
* Run manually.
*/
public function runManually($conversation, $userRunningTheWorkflow=null)
{
// $workflow = Workflow::find($workflow_id);
// if (!$workflow) {
// return false;
// }
if ($userRunningTheWorkflow)
self::$userRunningTheWorkflow = $userRunningTheWorkflow;
if (!$conversation) {
return false;
}
$this->performActions($conversation, $userRunningTheWorkflow);
self::$userRunningTheWorkflow = null;
}
public static function getConditionConfig($type, $mailbox_id)
{
$conditions_config = self::conditionsConfig($mailbox_id);
foreach ($conditions_config as $group) {
foreach ($group['items'] as $item_type => $item) {
if ($item_type == $type) {
return $item;
}
}
}
}
public function checkConditions($conversation, $action = '')
{
$and_true = true;
// Check trigger.
if ($action) {
$valid_trigger = false;
foreach ($this->conditions as $ands) {
foreach ($ands as $row) {
$config = self::getConditionConfig($row['type'], $conversation->mailbox_id);
if (empty($config['triggers'])) {
continue;
}
// Check conversation.created.
// if ($action = 'thread.created'
// && in_array('conversation.created', $config['triggers'])
// && $is_new_conversation
// ) {
// $valid_trigger = true;
// break 2;
// }
if (in_array($action, $config['triggers'])) {
$valid_trigger = true;
break 2;
}
}
}
// If conversation has been moved from another mailbox,
// among conditions there should be "Moved" condition.
if ($action == 'conversation.moved') {
$valid_trigger = false;
foreach ($this->conditions as $ands) {
foreach ($ands as $row) {
if ($row['type'] == 'new_or_reply' && $row['operator'] == 'moved') {
$valid_trigger = true;
break 2;
}
}
}
}
if (!$valid_trigger) {
return false;
}
}
foreach ($this->conditions as $ands) {
$or_true = false;
foreach ($ands as $row) {
if (empty($row['type'])) {
continue;
}
// if ($only_dates && !in_array($row['type'], self::$date_conditions)) {
// continue;
// }
$operator = $row['operator'] ?? '';
$value = $row['value'] ?? [];
switch ($row['type']) {
// People.
case 'customer_name':
$customer = $conversation->customer;
if ($customer) {
$or_true = self::compareText($customer->getFullName(), $value, $operator);
}
break;
case 'customer_email':
$customer = $conversation->customer;
if ($customer) {
foreach ($customer->emails as $email) {
$or_true = self::compareText($email->email, $value, $operator);
if ($or_true) {
break;
}
}
}
break;
case 'user_action':
if (empty(self::$cond_last_thread[$conversation->id]['any_type'])) {
$last_thread = $conversation->getLastThread();
self::$cond_last_thread[$conversation->id]['any_type'] = $last_thread;
} else {
$last_thread = self::$cond_last_thread[$conversation->id]['any_type'];
}
if ($last_thread && $last_thread->state == Thread::STATE_PUBLISHED) {
if (($operator == 'replied' && $last_thread->type == Thread::TYPE_MESSAGE)
|| ($operator == 'noted' && $last_thread->type == Thread::TYPE_NOTE)
) {
if ($value == Conversation::USER_UNASSIGNED
|| $last_thread->created_by_user_id == $value
) {
$or_true = true;
}
}
}
break;
// Conversation.
case 'type':
if ($operator == 'equal') {
$or_true = ($conversation->type == $value);
} else {
$or_true = ($conversation->type != $value);
}
break;
case 'status':
if ($operator == 'equal') {
$or_true = ($conversation->status == $value);
} else {
$or_true = ($conversation->status != $value);
}
break;
case 'state':
if ($operator == 'equal') {
$or_true = ($conversation->state == $value);
} else {
$or_true = ($conversation->state != $value);
}
break;
case 'user':
if ($value == Conversation::USER_UNASSIGNED) {
$value = 0;
}
if ($operator == 'equal') {
$or_true = ($conversation->user_id == $value);
} else {
$or_true = ($conversation->user_id != $value);
}
break;
case 'subject':
$or_true = self::compareText($conversation->subject."", $value, $operator);
// https://github.com/freescout-helpdesk/freescout/issues/1919
if (!$or_true) {
// Subject may contain symbols (for example é) encoded as 2 symbols.
// In this case imap_utf8 is needed.
$value_imap = \imap_utf8('=?UTF-8?q?'.str_replace(' ', '_', quoted_printable_encode($value)).'?=');
if ($value_imap && $value_imap != $value && !\Str::startsWith($value_imap, '=?UTF-8?q?')) {
$or_true = self::compareText($conversation->subject."", $value_imap, $operator);
}
}
break;
case 'to':
$last_thread = $conversation->getLastReply();
if ($last_thread) {
$or_true = self::compareArray($last_thread->getToArray(), $value, $operator);
}
break;
case 'cc':
$last_thread = $conversation->getLastReply();
if ($last_thread) {
$or_true = self::compareArray($last_thread->getCcArray(), $value, $operator);
}
break;
case 'body':
if ($action) {
if (empty(self::$cond_last_thread[$conversation->id]['any_type'])) {
$last_thread = $conversation->getLastThread();
self::$cond_last_thread[$conversation->id]['any_type'] = $last_thread;
} else {
$last_thread = self::$cond_last_thread[$conversation->id]['any_type'];
}
if ($last_thread) {
if ($last_thread->source_via == Thread::PERSON_CUSTOMER) {
if ($operator == 'customer') {
$or_true = self::compareText($last_thread->body, $value, 'contains');
}
if ($operator == 'regex') {
$or_true = self::compareText($last_thread->body, $value, 'regex');
}
}
if ($operator == 'note') {
if ($last_thread->source_via == Thread::PERSON_USER
&& $last_thread->type == Thread::TYPE_NOTE
) {
$or_true = self::compareText($last_thread->body, $value, 'contains');
}
}
}
} else {
// We have to check all messages bodies.
$threads = $conversation->getThreads(null, null, [Thread::TYPE_CUSTOMER, Thread::TYPE_NOTE]);
foreach ($threads as $thread) {
if ($thread->source_via == Thread::PERSON_CUSTOMER) {
if ($operator == 'customer') {
$or_true = self::compareText($thread->body, $value, 'contains');
}
if ($operator == 'regex') {
$or_true = self::compareText($thread->body, $value, 'regex');
}
}
if ($operator == 'note') {
if ($thread->source_via == Thread::PERSON_USER
&& $thread->type == Thread::TYPE_NOTE
) {
$or_true = self::compareText($thread->body, $value, 'contains');
}
}
if ($or_true) {
break;
}
}
}
break;
case 'headers':
if ($action) {
if (empty(self::$cond_last_thread[$conversation->id]['any_type'])) {
$last_thread = $conversation->getLastThread();
self::$cond_last_thread[$conversation->id]['any_type'] = $last_thread;
} else {
$last_thread = self::$cond_last_thread[$conversation->id]['any_type'];
}
if ($last_thread && $last_thread->source_via == Thread::PERSON_CUSTOMER) {
$or_true = self::compareText($last_thread->headers, $value, $operator);
}
} else {
// We have to check all messages bodies.
$threads = $conversation->getThreads(null, null, [Thread::TYPE_CUSTOMER, Thread::TYPE_NOTE]);
foreach ($threads as $thread) {
if ($thread && $thread->source_via == Thread::PERSON_CUSTOMER) {
$or_true = self::compareText($thread->headers, $value, $operator);
}
if ($or_true) {
break;
}
}
}
break;
case 'attachment':
if ($operator == 'yes') {
$or_true = $conversation->has_attachments;
} else {
$or_true = !$conversation->has_attachments;
}
break;
case 'bounce':
if (empty(self::$cond_last_thread[$conversation->id][Thread::TYPE_CUSTOMER])) {
$last_customer_thread = $conversation->getLastThread([Thread::TYPE_CUSTOMER]);
self::$cond_last_thread[$conversation->id][Thread::TYPE_CUSTOMER] = $last_customer_thread;
} else {
$last_customer_thread = self::$cond_last_thread[$conversation->id][Thread::TYPE_CUSTOMER];
}
if ($last_customer_thread) {
$or_true = $last_customer_thread->isBounce();
if ($operator == 'no') {
$or_true = !$or_true;
}
}
break;
case 'customer_viewed':
if (empty(self::$cond_last_thread[$conversation->id][Thread::TYPE_MESSAGE])) {
$last_user_thread = $conversation->getLastThread([Thread::TYPE_MESSAGE]);
self::$cond_last_thread[$conversation->id][Thread::TYPE_MESSAGE] = $last_user_thread;
} else {
$last_user_thread = self::$cond_last_thread[$conversation->id][Thread::TYPE_MESSAGE];
}
if ($last_user_thread) {
if ($operator == 'yes') {
$or_true = !empty($last_user_thread->opened_at);
} else {
$or_true = empty($last_user_thread->opened_at);
}
}
break;
case 'new_or_reply':
if (!in_array($action, [
'conversation.created_by_customer',
'conversation.created_by_user',
'conversation.customer_replied',
'conversation.user_replied',
'conversation.moved',
])) {
continue 2;
}
$actions_by_operator = [
'new' => ['conversation.created_by_customer', 'conversation.created_by_user'],
'reply' => ['conversation.customer_replied', 'conversation.user_replied'],
'moved' => ['conversation.moved'],
];
$or_true = in_array($action, $actions_by_operator[$operator]);
break;
case 'channel':
if ($operator == 'equal') {
$or_true = ((int)$conversation->channel == (int)$value);
} else {
$or_true = ((int)$conversation->channel != (int)$value);
}
break;
case 'waiting':
$number = $value['number'] ?? '';
$metric = $value['metric'] ?? '';
if (!$number || !$metric || !in_array($conversation->status, [Conversation::STATUS_ACTIVE, Conversation::STATUS_PENDING]) || $conversation->last_reply_from == Conversation::PERSON_USER) {
continue 2;
}
$now = Carbon::now();
if ($conversation->last_reply_at) {
if ($operator == 'longer') {
$or_true = ($conversation->last_reply_at < self::subTime($now, $metric, $number));
} else {
// not_longer
$or_true = ($conversation->last_reply_at > self::subTime($now, $metric, $number));
}
} else {
$or_true = false;
}
break;
case 'user_reply':
$number = $value['number'] ?? '';
$metric = $value['metric'] ?? '';
if (!$number || !$metric || $conversation->last_reply_from != Conversation::PERSON_USER) {
continue 2;
}
$now = Carbon::now();
if ($conversation->last_reply_at) {
if ($operator == 'in_last') {
$or_true = ($conversation->last_reply_at > self::subTime($now, $metric, $number));
} else {
// not_in_last
$or_true = ($conversation->last_reply_at < self::subTime($now, $metric, $number));
}
} else {
$or_true = false;
}
break;
case 'customer_reply':
$number = $value['number'] ?? '';
$metric = $value['metric'] ?? '';
if (!$number || !$metric || $conversation->last_reply_from != Conversation::PERSON_CUSTOMER) {
continue 2;
}
$now = Carbon::now();
if ($conversation->last_reply_at) {
if ($operator == 'in_last') {
$or_true = ($conversation->last_reply_at > self::subTime($now, $metric, $number));
} else {
// not_in_last
$or_true = ($conversation->last_reply_at < self::subTime($now, $metric, $number));
}
} else {
$or_true = false;
}
break;
case 'created':
$number = $value['number'] ?? '';
$metric = $value['metric'] ?? '';
if (!$number || !$metric) {
continue 2;
}
$now = Carbon::now();
if ($operator == 'in_last') {
$or_true = ($conversation->created_at > self::subTime($now, $metric, $number));
} else {
// Not in last
$or_true = ($conversation->created_at < self::subTime($now, $metric, $number));
}
break;
default:
$or_true = \Eventy::filter('workflow.check_condition', false, $row['type'], $operator, $value, $conversation, $this);
break;
}
if ($or_true) {
break;
}
}
if (!$or_true) {
$and_true = false;
break;
}
}
return $and_true;
}
public function checkPrevious($conversation)
{
if ($this->apply_to_prev || $conversation->created_at > $this->created_at ) {
return true;
} else {
return false;
}
}
public function performActions($conversation/*, $mark_processed = true*/)
{
$executed = false;
foreach ($this->actions as $ands) {
foreach ($ands as $action) {
$value = $action['value'] ?? '';
$operator = $action['operator'] ?? '';
switch ($action['type']) {
case 'notification':
if (!is_array($value)) {
continue 2;
}
$users = [];
foreach ($value as $user_id) {
if ($user_id == 'assignee') {
if ($conversation->user_id) {
$users[] = $conversation->user;
}
} elseif ($user_id == 'last_user') {
// Last User to Reply.
$user_replies = $conversation->getThreads(0, 1, [Thread::TYPE_MESSAGE]);
if (count($user_replies) && $user_replies->first()->created_by_user_id) {
$users[] = $user_replies->first()->created_by_user;
}
} else {
$user = User::findNonDeleted($user_id, true);
if ($user && $user->invite_state == User::INVITE_STATE_ACTIVATED) {
$users[] = $user;
}
}
}
if ($users) {
\App\Jobs\SendNotificationToUsers::dispatch(\Eventy::filter('users.unpack', $users), $conversation, $conversation->getThreads())
->onQueue('emails');
$executed = true;
}
break;
case 'reply':
try {
$value = json_decode($action['value'] ?? '', true);
} catch (\Exception $e) {
continue 2;
}
$body = $value['body'] ?? '';
$cc = $value['cc'] ?? '';
$bcc = $value['bcc'] ?? '';
if ($body) {
$user = self::getUser();
$body = $conversation->replaceTextVars($body, ['user' => $user]);
$conversation->createUserThread(self::getUser(), $body, [
'cc' => $cc,
'bcc' => $bcc,
'meta' => [
'workflow_id' => $this->id,
]
]);
$executed = true;
}
break;
case 'email_customer':
try {
$value = json_decode($action['value'] ?? '', true);
} catch (\Exception $e) {
continue 2;
}
$body = $value['body'] ?? '';
$cc = $value['cc'] ?? '';
$bcc = $value['bcc'] ?? '';
$subject = $value['subject'] ?? '';
if ($body) {
$user = self::getUser();
$body = $conversation->replaceTextVars($body, ['user' => $user]);
$meta = [
'workflow_id' => $this->id,
Thread::META_CONVERSATION_HISTORY => 'none',
];
if ($subject) {
$meta['wf_subj'] = $subject;
}
$conversation->createUserThread(self::getUser(), $body, [
'cc' => $cc,
'bcc' => $bcc,
'meta' => $meta,
]);
$executed = true;
}
break;
case 'no_autoreply':
$conversation->setMeta('ar_off', true);
$conversation->save();
$executed = true;
break;
case 'forward':
$user = self::getUser();
if ($conversation->created_by_user_id == $user->id) {
continue 2;
}
try {
$value = json_decode($action['value'] ?? '', true);
} catch (\Exception $e) {
continue 2;
}
$body = $value['body'] ?? '';
$to = $value['to'] ?? '';
$cc = $value['cc'] ?? '';
$bcc = $value['bcc'] ?? '';
if ($body && $to) {
$body = $conversation->replaceTextVars($body, ['user' => $user]);
$conversation->forward($user, $body, $to, [], true);
$executed = true;
}
break;
case 'note':
try {
$value = json_decode($action['value'] ?? '', true);
} catch (\Exception $e) {
continue 2;
}
$body = $value['body'] ?? '';
if ($body) {
$user = self::getUser();
$body = $conversation->replaceTextVars($body, ['user' => $user]);
$conversation->createUserThread(self::getUser(), $body, [
'type' => Thread::TYPE_NOTE,
'meta' => [
'workflow_id' => $this->id
]
]);
$executed = true;
}
break;
case 'status':
if ($conversation->status != $action['value']) {
$conversation->changeStatus($action['value'], self::getUser(), false);
$executed = true;
}
break;
case 'assign':
if ($conversation->user != $action['value']) {
$new_user = $action['value'];
if ($new_user == self::ASSIGNEE_CURRENT) {
$auth_user = auth()->user();
if ($auth_user) {
$new_user = $auth_user->id;
} else {
$new_user = null;
}
}
if ($new_user) {
$conversation->changeUser($new_user, self::getUser(), false);
$executed = true;
}
}
break;
case 'move':
$mailbox = Mailbox::find($action['value']);
if ($mailbox) {
$conversation->moveToMailbox($mailbox, self::getUser());
$executed = true;
}
break;
case 'delete':
if ($conversation->state != Conversation::STATE_DELETED) {
$conversation->deleteToFolder(self::getUser());
$executed = true;
}
break;
case 'delete_forever':
$mailbox = $conversation->mailbox;
$conversation->deleteForever();
// Recalculate only old and new folders
$mailbox->updateFoldersCounters();
return;
break;
default:
\Eventy::filter('workflow.perform_action', $performed = false, $action['type'], $operator, $value, $conversation, $this);
break;
}
}
}
// This may lead to infinite actions execution.
// if (!$executed) {
// return;
// }
// Create line item thread.
$action_type = self::ACTION_TYPE_AUTOMATIC_WORKFLOW;
$created_by_user_id = self::getUser()->id;
if ($this->isManual()) {
$action_type = self::ACTION_TYPE_MANUAL_WORKFLOW;
$auth_user = auth()->user();
if ($auth_user) {
$created_by_user_id = $auth_user->id;
}
}
Thread::create($conversation, Thread::TYPE_LINEITEM, '', [
'user_id' => $conversation->user_id,
'created_by_user_id' => $created_by_user_id,
'action_type' => $action_type,
'source_via' => Thread::PERSON_USER,
'source_type' => Thread::SOURCE_TYPE_WEB,
'meta' => [
'workflow_id' => $this->id
]
]);
//if ($mark_processed) {
if ($this->isAutomatic()) {
$this->markProcessed($conversation->id);
}
}
/**
* Get or create deleted user WorkfFlow.
*/
public static function getUser()
{
if (self::$userRunningTheWorkflow)
return self::$userRunningTheWorkflow;
$email = config('workflows.user_email');
if ($email)
return User::where('email', $email)->first();
if (!empty(self::$wf_user)) {
return self::$wf_user;
}
self::$wf_user = User::where('email', self::WF_USER_EMAIL)->first();
if (!self::$wf_user) {
self::$wf_user = User::create([
'first_name' => config('workflows.user_full_name'),
'last_name' => '',
'email' => self::WF_USER_EMAIL,
'password' => bcrypt(\Str::random(25)),
'status' => User::STATUS_DELETED,
'type' => User::TYPE_ROBOT,
]);
} else {
// Set name if needed.
if (self::$wf_user->first_name != config('workflows.user_full_name')) {
self::$wf_user->first_name = config('workflows.user_full_name');
self::$wf_user->save();
}
}
return self::$wf_user;
}
public function markProcessed($conversation_id)
{
try {
$conversation_workflow = new ConversationWorkflow();
$conversation_workflow->conversation_id = $conversation_id;
$conversation_workflow->workflow_id = $this->id;
$conversation_workflow->save();
} catch (\Exception $e) {
$conversation_workflow = ConversationWorkflow::where('conversation_id', $conversation_id)
->where('workflow_id', $this->id)
->first();
if ($conversation_workflow) {
$conversation_workflow->counter++;
$conversation_workflow->save();
}
}
}
public function countConversationsApplied()
{
return ConversationWorkflow::where('workflow_id', $this->id)->count();
}
public function countConversationsToApply()
{
return Conversation::where('mailbox_id', $this->mailbox_id)
->where('state', '!=', Conversation::STATE_DELETED)
//->where('created_at', '>=', $workflow->created_at);
//->where('status', '!=', Conversation::STATUS_SPAM)
->count();
}
public static function compareArray($array, $text, $operator)
{
switch ($operator) {
case 'equal':
case 'contains':
case 'starts':
case 'ends':
case 'regex':
foreach ($array as $item) {
if (self::compareText($item, $text, $operator)) {
return true;
}
}
break;
case 'not_contains':
foreach ($array as $item) {
if (self::compareText($item, $text, 'contains')) {
return false;
}
}
return true;
break;
default:
return false;
break;
}
return false;
}
public static function subTime($date, $metric, $number)
{
if ($metric == 'h') {
return $date->subHours($number);
} elseif ($metric == 'i') {
return $date->subMinutes($number);
} else {
return $date->subDays($number);
}
}
public static function compareText($text1, $text2, $operator)
{
if (!is_string($text1)) {
return false;
}
// For operators having $text2.
if (!in_array($operator, ['empty', 'not_empty'])) {
if (!is_string($text2)) {
return false;
}
if ($operator != 'regex') {
$text1 = mb_strtolower($text1);
$text2 = mb_strtolower($text2);
}
}
switch ($operator) {
case 'equal':
return $text1 == $text2;
break;
case 'not_equal':
return $text1 != $text2;
break;
case 'contains':
return \Str::contains($text1, $text2);
break;
case 'not_contains':
return !\Str::contains($text1, $text2);
break;
case 'starts':
return \Str::startsWith($text1, $text2);
break;
case 'ends':
return \Str::endsWith($text1, $text2);
break;
case 'regex':
try {
// if (preg_match("#^/.*/$#", $text2)) {
// $regex = $text2;
// } else {
// $regex = '/'.$text2.'/';
// }
return preg_match($text2, $text1);
} catch (\Exception $e) {
\Log::error('Invalid Workflow conditions regex: '.$text2.'. '.$e->getMessage());
}
break;
case 'empty':
return $text1 === '';
break;
case 'not_empty':
return $text1 !== '';
break;
default:
return false;
break;
}
return false;
}
public static function maybeProcessInBackground($workflow)
{
if (!$workflow->active || !$workflow->apply_to_prev || !$workflow->id) {
return false;
}
\Helper::backgroundAction('workflow.do_process', [$workflow->id]);
}
public static function findCached($id)
{
return Workflow::where('id', $id)->rememberForever()->first();
}
/**
* Check if workflow is complete and deactivate if needed.
*/
public function checkComplete($save = false)
{
$deactivated = false;
$errors = $this->validate();
if (count($errors)) {
$this->complete = false;
if ($this->active) {
$deactivated = true;
}
$this->active = false;
} else {
$this->complete = true;
}
if ($save) {
$this->save();
}
return $deactivated;
}
public static function canEditWorkflows($user = null, $mailbox_id = null)
{
if (!$user) {
$user = auth()->user();
}
if (!$user) {
return false;
}
if ($mailbox_id) {
return $user->isAdmin() || ($user->hasAccessToMailbox($mailbox_id) && $user->hasPermission(\Workflow::PERM_EDIT_WORKFLOWS));
} else {
return $user->isAdmin() || $user->hasPermission(\Workflow::PERM_EDIT_WORKFLOWS);
}
}
public static function checkAll($flash = true)
{
$workflows = Workflow::where('active', true)
->get();
foreach ($workflows as $workflow) {
$deactivated = $workflow->checkComplete(true);
if ($deactivated && self::canEditWorkflows()) {
\Helper::addFloatingFlash(__('Workflow :name deactivated', ['name' => $workflow->name]));
}
}
}
/**
* Validate workflow.
*/
public function validate()
{
$errors = [];
if ($this->type == self::TYPE_MANUAL) {
if (!$this->actions) {
$errors['actions'] = [];
}
$errors = $this->validateActions($errors);
} else {
if (!$this->conditions) {
$errors['conditions'] = [];
}
if (!$this->actions) {
$errors['actions'] = [];
}
$errors = $this->validateConditions($errors);
$errors = $this->validateActions($errors);
}
return $errors;
}
public function validateConditions($errors)
{
if (!is_array($this->conditions)) {
$errors['conditions'] = [];
return $errors;
}
foreach ($this->conditions as $and_i => $ands) {
foreach ($ands as $or_i => $condition) {
$has_error = false;
if (empty($condition['type']) || empty($condition['operator'])) {
$has_error = true;
}
if (!$has_error) {
switch ($condition['type']) {
case 'customer_name':
case 'customer_email':
case 'to':
case 'cc':
case 'subject':
case 'body':
if (empty($condition['value'])) {
$has_error = true;
}
break;
case 'user_action':
case 'user':
if (empty($condition['value'])) {
$has_error = true;
} elseif ($condition['value'] != Conversation::USER_UNASSIGNED) {
$user = User::nonDeleted(true)->where('id' , $condition['value'])->first();
if (!$user) {
$has_error = true;
}
}
break;
case 'waiting':
case 'user_reply':
case 'created':
if (empty($condition['value']) || empty($condition['value']['number'])) {
$has_error = true;
}
break;
default:
$has_error = \Eventy::filter('workflow.validate_condition', $has_error, $condition, $this);
break;
}
}
if ($has_error) {
$errors['conditions'][$and_i.'_'.$or_i] = 1;
}
}
}
return $errors;
}
public function validateActions($errors)
{
if (!is_array($this->actions)) {
$errors['actions'] = [];
return $errors;
}
foreach ($this->actions as $and_i => $ands) {
foreach ($ands as $or_i => $action) {
$has_error = false;
if (empty($action['type'])) {
$has_error = true;
}
if (!$has_error) {
switch ($action['type']) {
case 'email_customer':
case 'forward':
case 'note':
if (empty($action['value'])) {
$has_error = true;
}
break;
case 'assign':
if (empty($action['value'])) {
$has_error = true;
} elseif ($action['value'] != Conversation::USER_UNASSIGNED
&& $action['value'] != self::ASSIGNEE_CURRENT
) {
$user = User::where('id' , $action['value'])->first();
if (!$user || !\Eventy::filter('workflow.is_user_valid', !$user->isDeleted(), $user, $this)) {
$has_error = true;
}
}
break;
case 'notification':
if (empty($action['value'])) {
$has_error = true;
} elseif (is_array($action['value'])) {
$user_ids = [];
foreach ($action['value'] as $user_id) {
if ((int)$user_id) {
$user_ids[] = $user_id;
}
}
if ($user_ids) {
$count = User::nonDeleted(true)->whereIn('id' , $user_ids)->count();
if ($count != count($user_ids)) {
$has_error = true;
}
}
}
break;
case 'move':
if (empty($action['value'])) {
$has_error = true;
} else {
$mailbox = Mailbox::where('id' , $action['value'])->first();
if (!$mailbox) {
$has_error = true;
}
}
break;
default:
$has_error = \Eventy::filter('workflow.validate_action', $has_error, $action, $this);
break;
}
}
if ($has_error) {
$errors['actions'][$and_i.'_'.$or_i] = 1;
}
}
}
return $errors;
}
public function errors($cache = true)
{
if ($cache && $this->validateErrors !== null) {
return $this->validateErrors;
}
$this->validateErrors = $this->validate();
return $this->validateErrors;
}
public function setMaxExecutionsAttribute($value)
{
if ($value < 0) {
$this->attributes['max_executions'] = 1;
} else {
$this->attributes['max_executions'] = $value;
}
}
public static function getChannels()
{
if (self::$channels !== null) {
return self::$channels;
} else {
self::$channels = \Eventy::filter('channels.list', []);
return self::$channels;
}
}
public static function getThreadWorkflow($thread)
{
$meta = $thread->getMetas();
$workflow_id = $meta['workflow_id'] ?? '';
return Workflow::find($workflow_id);
}
}