Passed
Push — develop ( 3c5942...636dd4 )
by Nikolay
04:57
created

WorkerCdr::notifyByEmail()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
1
<?php
2
/**
3
 * Copyright © MIKO LLC - All Rights Reserved
4
 * Unauthorized copying of this file, via any medium is strictly prohibited
5
 * Proprietary and confidential
6
 * Written by Alexey Portnov, 2 2020
7
 */
8
9
namespace MikoPBX\Core\Workers;
10
11
require_once 'Globals.php';
12
13
use MikoPBX\Common\Models\{CallDetailRecordsTmp, Users};
14
use MikoPBX\Core\System\{BeanstalkClient, Util};
15
use Phalcon\Exception as ExceptionAlias;
16
17
/**
18
 * Class WorkerCdr
19
 * Обработка записей CDR. Заполение длительности звонков.
20
 */
21
class WorkerCdr extends WorkerBase
22
{
23
24
    public const SELECT_CDR_TUBE = 'select_cdr_tube';
25
26
    public const UPDATE_CDR_TUBE = 'update_cdr_tube';
27
28
29
    private $client_queue;
30
    private $timeout = 10;
0 ignored issues
show
introduced by
The private property $timeout is not used, and could be removed.
Loading history...
31
    private $internal_numbers = [];
32
    private $no_answered_calls = [];
33
34
35
    /**
36
     * Entry point
37
     *
38
     * @param $argv
39
     *
40
     * @throws \Pheanstalk\Exception\DeadlineSoonException
41
     */
42
    public function start($argv): void
43
    {
44
        $filter = [
45
            '(work_completed<>1 OR work_completed IS NULL) AND endtime IS NOT NULL',
46
            'miko_tmp_db'         => true,
47
            'columns'             => 'start,answer,src_num,dst_num,dst_chan,endtime,linkedid,recordingfile,dialstatus,UNIQUEID',
48
            'miko_result_in_file' => true,
49
        ];
50
51
52
        $this->client_queue = new BeanstalkClient(self::SELECT_CDR_TUBE);
53
        $this->client_queue->subscribe($this->makePingTubeName(self::class), [$this, 'pingCallBack']);
54
55
        $this->initSettings();
56
57
        while (true) {
58
            $result = $this->client_queue->request(json_encode($filter), 10);
59
60
            if ($result !== false) {
61
                $this->updateCdr();
62
            }
63
            $this->client_queue->wait(5); // instead of sleep
64
        }
65
    }
66
67
    private function initSettings()
68
    {
69
        $this->internal_numbers  = [];
70
        $this->no_answered_calls = [];
71
        $users                   = Users::find();
72
        foreach ($users as $user) {
73
            if (empty($user->email)) {
74
                continue;
75
            }
76
77
            foreach ($user->Extensions as $exten) {
78
                $this->internal_numbers[$exten->number] = [
79
                    'email'    => $user->email,
80
                    'language' => $user->language,
81
                ];
82
            }
83
        }
84
    }
85
86
    /**
87
     * Обработчик результата запроса.
88
     *
89
     */
90
    private function updateCdr(): void
91
    {
92
        $this->initSettings();
93
        $result_data = $this->client_queue->getBody();
94
        // Получаем результат.
95
        $result = json_decode($result_data, true);
96
        if (file_exists($result)) {
97
            $file_data = json_decode(file_get_contents($result), true);
98
            unlink($result);
99
            $result = $file_data;
100
        }
101
        if ( ! is_array($result) && ! is_object($result)) {
102
            return;
103
        }
104
        if (count($result) < 1) {
105
            return;
106
        }
107
        $arr_update_cdr = [];
108
        // Получаем идентификаторы активных каналов.
109
        $channels_id = $this->getActiveIdChannels();
110
        foreach ($result as $row) {
111
            if (array_key_exists($row['linkedid'], $channels_id)) {
112
                // Цепочка вызовов еще не завершена.
113
                continue;
114
            }
115
            if (trim($row['recordingfile']) !== '') {
116
                // Если каналов не существует с ID, то можно удалить временные файлы.
117
                $p_info = pathinfo($row['recordingfile']);
118
                $fname  = $p_info['dirname'] . '/' . $p_info['filename'] . '.wav';
119
                if (file_exists($fname)) {
120
                    @unlink($fname);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

120
                    /** @scrutinizer ignore-unhandled */ @unlink($fname);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
121
                }
122
            }
123
            $start      = strtotime($row['start']);
124
            $answer     = strtotime($row['answer']);
125
            $end        = strtotime($row['endtime']);
126
            $dialstatus = trim($row['dialstatus']);
127
128
            $duration = max(($end - $start), 0);
129
            $billsec  = ($end != 0 && $answer != 0) ? ($end - $answer) : 0;
130
131
            $disposition = 'NOANSWER';
132
            if ($billsec > 0) {
133
                $disposition = 'ANSWERED';
134
            } elseif ('' !== $dialstatus) {
135
                $disposition = ($dialstatus === 'ANSWERED') ? $disposition : $dialstatus;
136
            }
137
138
            if ($billsec <= 0) {
139
                $row['answer'] = '';
140
                $billsec       = 0;
141
142
                if ( ! empty($row['recordingfile'])) {
143
                    $p_info    = pathinfo($row['recordingfile']);
144
                    $file_list = [
145
                        $p_info['dirname'] . '/' . $p_info['filename'] . '.mp3',
146
                        $p_info['dirname'] . '/' . $p_info['filename'] . '.wav',
147
                        $p_info['dirname'] . '/' . $p_info['filename'] . '_in.wav',
148
                        $p_info['dirname'] . '/' . $p_info['filename'] . '_out.wav',
149
                    ];
150
                    foreach ($file_list as $file) {
151
                        if ( ! file_exists($file)) {
152
                            continue;
153
                        }
154
                        @unlink($file);
155
                    }
156
                }
157
            }
158
159
            if ($disposition !== 'ANSWERED') {
160
                if (file_exists($row['recordingfile'])) {
161
                    @unlink($row['recordingfile']);
162
                }
163
            } elseif ( ! file_exists(Util::trimExtensionForFile($row['recordingfile']) . 'wav') && ! file_exists(
164
                    $row['recordingfile']
165
                )) {
166
                /** @var CallDetailRecordsTmp $rec_data */
167
                $rec_data = CallDetailRecordsTmp::findFirst(
168
                    "linkedid='{$row['linkedid']}' AND dst_chan='{$row['dst_chan']}'"
169
                );
170
                if ($rec_data !== null) {
171
                    $row['recordingfile'] = $rec_data->recordingfile;
172
                }
173
            }
174
175
            $data = [
176
                'work_completed' => 1,
177
                'duration'       => $duration,
178
                'billsec'        => $billsec,
179
                'disposition'    => $disposition,
180
                'UNIQUEID'       => $row['UNIQUEID'],
181
                'recordingfile'  => ($disposition === 'ANSWERED') ? $row['recordingfile'] : '',
182
                'tmp_linked_id'  => $row['linkedid'],
183
            ];
184
185
            $arr_update_cdr[] = $data;
186
            $this->checkNoAnswerCall(array_merge($row, $data));
187
        }
188
189
        foreach ($arr_update_cdr as $data) {
190
            $linkedid              = $data['tmp_linked_id'];
191
            $data['GLOBAL_STATUS'] = $data['disposition'];
192
            if (isset($this->no_answered_calls[$linkedid]) &&
193
                isset($this->no_answered_calls[$linkedid]['NOANSWER']) &&
194
                $this->no_answered_calls[$linkedid]['NOANSWER'] == false) {
195
                $data['GLOBAL_STATUS'] = 'ANSWERED';
196
            }
197
            unset($data['tmp_linked_id']);
198
            $this->client_queue->publish(json_encode($data), null, self::UPDATE_CDR_TUBE);
199
        }
200
201
        $this->notifyByEmail();
202
    }
203
204
    /**
205
     * Функция позволяет получить активные каналы.
206
     * Возвращает ассоциативный массив. Ключ - Linkedid, значение - массив каналов.
207
     *
208
     * @return array
209
     */
210
    private function getActiveIdChannels(): array
211
    {
212
        $am           = Util::getAstManager('off');
213
        $active_chans = $am->GetChannels(true);
214
        $am->Logoff();
215
216
        return $active_chans;
217
    }
218
219
    /**
220
     * Анализируем не отвеченные вызовы. Наполняем временный массив для дальнейшей обработки.
221
     *
222
     * @param $row
223
     */
224
    private function checkNoAnswerCall($row): void
225
    {
226
        if ($row['disposition'] === 'ANSWERED') {
227
            $this->no_answered_calls[$row['linkedid']]['NOANSWER'] = false;
228
229
            return;
230
        }
231
        if ( ! array_key_exists($row['dst_num'], $this->internal_numbers)) {
232
            // dst_num - не является номером сотрудника. Это исходящий.
233
            return;
234
        }
235
        $is_internal = false;
236
        if ((array_key_exists($row['src_num'], $this->internal_numbers))) {
237
            // Это внутренний вызов.
238
            $is_internal = true;
239
        }
240
241
        $this->no_answered_calls[$row['linkedid']][] = [
242
            'from_number' => $row['src_num'],
243
            'to_number'   => $row['dst_num'],
244
            'start'       => $row['start'],
245
            'answer'      => $row['answer'],
246
            'endtime'     => $row['endtime'],
247
            'email'       => $this->internal_numbers[$row['dst_num']]['email'],
248
            'language'    => $this->internal_numbers[$row['dst_num']]['language'],
249
            'is_internal' => $is_internal,
250
            'duration'    => $row['duration'],
251
        ];
252
    }
253
254
    /**
255
     * Постановка задачи в очередь на оповещение по email.
256
     */
257
    private function notifyByEmail(): void
258
    {
259
        foreach ($this->no_answered_calls as $call) {
260
            $this->client_queue->publish(json_encode($call), null, WorkerNotifyByEmail::class);
261
        }
262
        $this->no_answered_calls = [];
263
    }
264
265
}
266
267
// Start worker process
268
$workerClassname = WorkerCdr::class;
269
if (isset($argv) && count($argv) > 1 && $argv[1] === 'start') {
270
    cli_set_process_title($workerClassname);
271
    try {
272
        $worker = new $workerClassname();
273
        $worker->start($argv);
274
    } catch (\Exception $e) {
275
        global $errorLogger;
276
        $errorLogger->captureException($e);
277
        Util::sysLogMsg("{$workerClassname}_EXCEPTION", $e->getMessage());
278
    }
279
}