Passed
Push — develop ( b2a56a...21c7e9 )
by Портнов
06:39
created

WorkerCallEvents::updateDataInDbM()   B

Complexity

Conditions 9
Paths 26

Size

Total Lines 55
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 36
c 1
b 0
f 0
dl 0
loc 55
rs 8.0555
cc 9
nc 26
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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, 10 2020
7
 */
8
9
namespace MikoPBX\Core\Workers;
10
require_once 'Globals.php';
11
12
use MikoPBX\Common\Models\{CallDetailRecords, CallDetailRecordsTmp};
13
use MikoPBX\Core\Asterisk\CdrDb;
14
use MikoPBX\Core\System\{BeanstalkClient, MikoPBXConfig, Storage, Util};
15
use Phalcon\Di;
16
use Throwable;
17
18
class WorkerCallEvents extends WorkerBase
19
{
20
    // Максимальное количество экземпляров данныого класса.
21
    protected int $maxProc = 1;
22
23
    protected array $mixMonitorChannels = [];
24
    protected bool  $record_calls       = true;
25
    protected bool  $split_audio_thread = false;
26
    protected array $checkChanHangupTransfer = [];
27
28
    /**
29
     * Инициирует запись разговора на канале.
30
     *
31
     * @param string    $channel
32
     * @param ?string   $file_name
33
     * @param ?string   $sub_dir
34
     * @param ?string   $full_name
35
     *
36
     * @return string
37
     */
38
    public function MixMonitor($channel, $file_name = null, $sub_dir = null, $full_name = null): string
39
    {
40
        $resFile = $this->mixMonitorChannels[$channel]??'';
41
        if($resFile !== ''){
42
            return $resFile;
43
        }
44
        $resFile           = '';
45
        $file_name = str_replace('/', '_', $file_name);
46
        if ($this->record_calls) {
47
            [$f, $options] = $this->setMonitorFilenameOptions($full_name, $sub_dir, $file_name);
48
            $arr = $this->am->GetChannels(false);
49
            if(!in_array($channel, $arr, true)){
50
                return '';
51
            }
52
            $srcFile = "{$f}.wav";
53
            $resFile = "{$f}.mp3";
54
            $this->am->MixMonitor($channel, $srcFile, $options);
55
            $this->mixMonitorChannels[$channel] = $resFile;
56
            $this->am->UserEvent('StartRecording', ['recordingfile' => $resFile, 'recchan' => $channel]);
57
        }
58
        return $resFile;
59
    }
60
61
    /**
62
     * @param string|null $full_name
63
     * @param string|null $sub_dir
64
     * @param string|null $file_name
65
     * @return array
66
     */
67
    private function setMonitorFilenameOptions(?string $full_name, ?string $sub_dir, ?string $file_name): array{
68
        if (!file_exists($full_name)) {
69
            $monitor_dir = Storage::getMonitorDir();
70
            if ($sub_dir === null) {
71
                $sub_dir = date('Y/m/d/H/');
72
            }
73
            $f = "{$monitor_dir}/{$sub_dir}{$file_name}";
74
        } else {
75
            $f = Util::trimExtensionForFile($full_name);
76
        }
77
        if ($this->split_audio_thread) {
78
            $options = "abSr({$f}_in.wav)t({$f}_out.wav)";
79
        } else {
80
            $options = 'ab';
81
        }
82
        return array($f, $options);
83
    }
84
85
    /**
86
     * Останавливает запись разговора на канале.
87
     * @param string $channel
88
     */
89
    public function StopMixMonitor($channel): void
90
    {
91
        if(isset($this->mixMonitorChannels[$channel])){
92
            unset($this->mixMonitorChannels[$channel]);
93
        }else{
94
            return;
95
        }
96
        if ($this->record_calls) {
97
            $this->am->StopMixMonitor($channel);
98
        }
99
    }
100
101
    /**
102
     * Обработка события начала телефонного звонка.
103
     *
104
     * @param $data
105
     */
106
    public function Action_dial($data): void
107
    {
108
        self::insertDataToDbM($data);
109
        $this->Action_app_end($data);
110
    }
111
112
    /**
113
     * Завершение работы приложения.
114
     *
115
     * @param $data
116
     */
117
    public function Action_app_end($data): void
118
    {
119
        $filter = [
120
            'linkedid=:linkedid: AND is_app=1 AND endtime = ""',
121
            'bind' => [
122
                'linkedid' => $data['linkedid'],
123
            ],
124
        ];
125
        /** @var CallDetailRecordsTmp $m_data */
126
        /** @var CallDetailRecordsTmp $row */
127
        $m_data = CallDetailRecordsTmp::find($filter);
128
        foreach ($m_data as $row) {
129
            $row->writeAttribute('endtime', $data['start']);
130
            $res = $row->update();
131
            if ( ! $res) {
132
                Util::sysLogMsg('Action_app_end', implode(' ', $row->getMessages()));
133
            }
134
        }
135
    }
136
137
    /**
138
     * Обработка события создания канала - пары, при начале телефонного звонка.
139
     *
140
     * @param $data
141
     */
142
    public function Action_dial_create_chan($data): void
143
    {
144
        if (isset($data['org_id'])) {
145
            // Вероятно необходимо переопределить искать по двум ID.
146
            // Применимо только для Originate, когда в качестве звонящего используем два канала
147
            // мобильный и внутренний номер.
148
            $filter = [
149
                '(UNIQUEID=:UNIQUEID: OR UNIQUEID=:org_id:) AND endtime = ""',
150
                'bind' => ['UNIQUEID' => $data['UNIQUEID'], 'org_id' => $data['org_id'],],
151
            ];
152
        } else {
153
            $filter = [
154
                'UNIQUEID=:UNIQUEID: AND answer = "" AND endtime = ""',
155
                'bind' => [
156
                    'UNIQUEID' => $data['UNIQUEID'],
157
                ],
158
            ];
159
        }
160
161
        $row_create = false;
162
        /** @var CallDetailRecordsTmp $m_data */
163
        /** @var CallDetailRecordsTmp $row */
164
        $m_data = CallDetailRecordsTmp::find($filter);
165
        foreach ($m_data as $row) {
166
            if ( ! is_object($row)) {
167
                continue;
168
            }
169
            ///
170
            // Проверим, если более одного канала SIP/256 при входящем.
171
            $column_chan_name = ('ORIGINATE' === $row->dialstatus) ? 'src_chan' : 'dst_chan';
172
173
            if ( ! empty($row->$column_chan_name) && $data['dst_chan'] !== $row->$column_chan_name) {
174
                if ($row_create) {
175
                    continue;
176
                }
177
                // Необходимо дублировать строку звонка.
178
                $new_row = new CallDetailRecordsTmp();
179
                $f_list  = $row->toArray();
180
                foreach ($f_list as $attribute => $value) {
181
                    if ($attribute === 'id') {
182
                        continue;
183
                    }
184
                    $new_row->writeAttribute($attribute, $value);
185
                }
186
                $new_row->writeAttribute($column_chan_name, $data['dst_chan']);
187
                $new_row->writeAttribute('UNIQUEID', $data['UNIQUEID'] . '_' . $data['dst_chan']);
188
                // Подмена $row;
189
                $row        = $new_row;
190
                $row_create = true;
191
            }
192
            // конец проверки
193
            ///
194
            if ($row->dialstatus === 'ORIGINATE') {
195
                $account_col = 'from_account';
196
                // При оригинации меняется местами srs_chan в поле dst_chan.
197
                $row->writeAttribute('src_chan', $data['dst_chan']);
198
            } else {
199
                $account_col = 'to_account';
200
                $row->writeAttribute('dst_chan', $data['dst_chan']);
201
            }
202
203
            if (isset($data['to_account']) && ! empty($data['to_account'])) {
204
                $row->writeAttribute($account_col, $data['to_account']);
205
            }
206
            if (isset($data['dst_call_id']) && ! empty($data['dst_call_id'])) {
207
                $row->writeAttribute('dst_call_id', $data['dst_call_id']);
208
            }
209
            $res = $row->save();
210
            if ( ! $res) {
211
                Util::sysLogMsg('Action_dial_create_chan', implode(' ', $row->getMessages()));
212
            }
213
        }
214
    }
215
216
    /**
217
     * Обработка события ответа на звонок. Соединение абонентов.
218
     *
219
     * @param $data
220
     */
221
    public function Action_dial_answer($data): void
222
    {
223
        $mikoPBXConfig = new MikoPBXConfig();
224
        $pickupexten   = $mikoPBXConfig->getGeneralSettings('PBXFeaturePickupExten');
225
        if (trim($data['dnid']) === $pickupexten) {
226
            // Pickup / перехват вызова.
227
            // Событие возникает, когда мы пытаемся перехватить вызов на соседний телефон.
228
            $filter = [
229
                'UNIQUEID=:UNIQUEID:',
230
                'bind' => ['UNIQUEID' => $data['old_id'],],
231
            ];
232
            /** @var CallDetailRecordsTmp $m_data */
233
            $m_data = CallDetailRecordsTmp::find($filter);
234
            if (count($m_data->toArray()) === 1) {
235
                /** @var CallDetailRecordsTmp $m_row_data */
236
                $m_row_data                 = $m_data[0];
237
                $new_data                   = $m_row_data->toArray();
238
                $new_data['start']          = $data['answer'];
239
                $new_data['answer']         = $data['answer'];
240
                $new_data['endtime']        = '';
241
                $new_data['dst_chan']       = $data['agi_channel'];
242
                $new_data['dst_num']        = $data['dst_num'];
243
                $new_data['UNIQUEID']       = $data['id'];
244
                $new_data['recordingfile']  = $this->MixMonitor($new_data['dst_chan'],  'pickup_'.$new_data['UNIQUEID']);
245
246
                unset($new_data['id'], $new_data['end']);
247
                self::insertDataToDbM($new_data);
248
                /**
249
                 * Отправка UserEvent
250
                 */
251
                $new_data['action'] = 'answer_pickup_create_cdr';
252
                $AgiData            = base64_encode(json_encode($new_data));
253
                $this->am->UserEvent('CdrConnector', ['AgiData' => $AgiData]);
254
            }
255
        } else {
256
            if ( ! empty($data['ENDCALLONANSWER'])) {
257
                // Переменная ENDCALLONANSWER устанавливается при начале работы умной маршуртизации.
258
                // Как только произошел ответ на вызов, отметим вызов на приложение как завершенный.
259
                $filter = [
260
                    'UNIQUEID<>:UNIQUEID: AND is_app=1 AND endtime = "" AND src_chan=:src_chan:',
261
                    'bind' => [
262
                        'UNIQUEID' => $data['id'],
263
                        'src_chan' => $data['BRIDGEPEER'],
264
                    ],
265
                ];
266
                /** @var CallDetailRecordsTmp $m_data */
267
                /** @var CallDetailRecordsTmp $row */
268
                $m_data = CallDetailRecordsTmp::find($filter);
269
                foreach ($m_data as $row) {
270
                    $row->writeAttribute('endtime', $data['answer']);
271
                    $row->writeAttribute('is_app', 1);
272
                    $res = $row->save();
273
                    if ( ! $res) {
274
                        Util::sysLogMsg('ENDCALLONANSWER', implode(' ', $row->getMessages()));
275
                    }
276
                }
277
            }
278
279
            if (isset($data['org_id'])) {
280
                // Вероятно необходимо переопределить искать по двум ID.
281
                // Применимо только для Originate, когда в качестве звонящего используем два канала
282
                // мобильный и внутренний номер.
283
                $filter = [
284
                    '(UNIQUEID=:UNIQUEID: OR UNIQUEID=:org_id:) AND answer = "" AND endtime = ""',
285
                    'bind' => [
286
                        'UNIQUEID' => $data['id'],
287
                        'org_id'   => $data['org_id'],
288
                    ],
289
                ];
290
            } else {
291
                $filter = [
292
                    '(UNIQUEID=:UNIQUEID: OR UNIQUEID=:UNIQUEID_CHAN:) AND answer = "" AND endtime = ""',
293
                    'bind' => [
294
                        'UNIQUEID'      => $data['id'],
295
                        'UNIQUEID_CHAN' => $data['id'] . '_' . $data['agi_channel'],
296
                    ],
297
                ];
298
            }
299
            // Отмечаем вызов как отвеченный.
300
            $m_data = CallDetailRecordsTmp::find($filter);
301
            foreach ($m_data as $row) {
302
                if ($row->dialstatus === 'ORIGINATE') {
303
                    if ($row->src_chan !== $data['agi_channel']) {
304
                        // Ищем совпадающий канал
305
                        continue;
306
                    }
307
                    // Найдем все прочие CDR по данному originate и отметим как завершенные.
308
                    $filter      = [
309
                        'linkedid=:linkedid: AND endtime <> "" AND src_chan <> :src_chan:',
310
                        'bind' => [
311
                            'linkedid' => $row->linkedid,
312
                            'src_chan' => $data['agi_channel'],
313
                        ],
314
                    ];
315
                    $m_orgn_data = CallDetailRecordsTmp::find($filter);
316
                    /** @var CallDetailRecordsTmp $orgn_row */
317
                    foreach ($m_orgn_data as $orgn_row) {
318
                        if (empty($orgn_row->endtime)) {
319
                            $orgn_row->writeAttribute('endtime', $data['answer']);
320
                        }
321
                        $orgn_row->writeAttribute('dst_chan', '');
322
                        $orgn_row->writeAttribute('UNIQUEID', $data['id'] . '_' . $orgn_row->src_chan);
323
                        $orgn_row->update();
324
                    }
325
326
                    $row->writeAttribute('dst_chan', '');
327
                    $row->writeAttribute('dialstatus', '');
328
                    $row->writeAttribute('UNIQUEID', $data['id']);
329
                    $row->save();
330
                    break;
331
                }
332
333
                $row->writeAttribute('answer', $data['answer']);
334
                $recFile = $data['recordingfile']??'';
335
                if(!empty($recFile)){
336
                    $this->mixMonitorChannels[$data['agi_channel']] = $recFile;
337
                    $row->writeAttribute('recordingfile', $recFile);
338
                }
339
                $res = $row->save();
340
                if ( !$res) {
341
                    Util::sysLogMsg('Action_dial_answer', implode(' ', $row->getMessages()));
342
                }
343
            }
344
        }
345
    }
346
347
    /**
348
     * Завершение / уничтожение канала.
349
     *
350
     * @param $data
351
     *
352
     * @throws \Throwable
353
     */
354
    public function Action_hangup_chan($data): void
355
    {
356
        $channels       = [];
357
        $transfer_calls = [];
358
359
        $this->hangupChanEndCalls($data, $transfer_calls, $channels);
360
        // Проверим, возможно это обычный трансфер.
361
        $this->Action_CreateRowTransfer('hangup_chan', $data, $transfer_calls);
362
363
        $this->hangupChanCheckSipTrtansfer($data, $channels);
364
365
        // Очистим память.
366
        if(isset($this->checkChanHangupTransfer[$data['agi_channel']])){
367
            unset($this->checkChanHangupTransfer[$data['agi_channel']]);
368
        }
369
        if(isset($this->mixMonitorChannels[$data['agi_channel']])){
370
            unset($this->mixMonitorChannels[$data['agi_channel']]);
371
        }
372
    }
373
374
    /**
375
     * Проверяем на SIP трансфер.
376
     * @param $data
377
     * @param $channels
378
     */
379
    private function hangupChanCheckSipTrtansfer($data, $channels):void{
380
        $not_local = (stripos($data['agi_channel'], 'local/') === false);
381
        if($not_local === false || $data['OLD_LINKEDID'] !== $data['linkedid']) {
382
            return;
383
        }
384
        $active_chans = $this->am->GetChannels(false);
385
        // Намек на SIP трансфер.
386
        foreach ($channels as $data_chan) {
387
            if ( ! in_array($data_chan['chan'], $active_chans, true)) {
388
                // Вызов уже завершен. Не интересно.
389
                continue;
390
            }
391
            $BRIDGEPEER = $this->am->GetVar($data_chan['chan'], 'BRIDGEPEER', null, false);
392
            if ( ! in_array($BRIDGEPEER, $active_chans, true) || !is_string($BRIDGEPEER)) {
393
                // Вызов уже завершен. Не интересно.
394
                continue;
395
            }
396
397
            $linkedid = $this->am->GetVar($data_chan['chan'], 'CDR(linkedid)', null, false);
398
            if ( empty($linkedid) || $linkedid === $data['linkedid']) {
399
                continue;
400
            }
401
402
            $CALLERID = $this->am->GetVar($BRIDGEPEER, 'CALLERID(num)', null, false);
403
            $n_data['action']        = 'sip_transfer';
404
            $n_data['src_chan']      = $data_chan['out'] ? $data_chan['chan'] : $BRIDGEPEER;
405
            $n_data['src_num']       = $data_chan['out'] ? $data_chan['num'] : $CALLERID;
406
            $n_data['dst_chan']      = $data_chan['out'] ? $BRIDGEPEER : $data_chan['chan'];
407
            $n_data['dst_num']       = $data_chan['out'] ? $CALLERID : $data_chan['num'];
408
            $n_data['start']         = date('Y-m-d H:i:s');
409
            $n_data['answer']        = date('Y-m-d H:i:s');
410
            $n_data['linkedid']      = $linkedid;
411
            $n_data['UNIQUEID']      = $data['linkedid'] . Util::generateRandomString();
412
            $n_data['transfer']      = '0';
413
            $n_data['recordingfile'] = $this->MixMonitor($n_data['dst_chan'], $n_data['UNIQUEID']);
414
            $n_data['did']           = $data_chan['did'];
415
416
            Util::logMsgDb('call_events', json_encode($n_data));
417
            self::insertDataToDbM($n_data);
418
            $filter = [
419
                'linkedid=:linkedid:',
420
                'bind' => ['linkedid' => $data['linkedid']],
421
            ];
422
            $m_data = CallDetailRecordsTmp::find($filter);
423
            foreach ($m_data as $row) {
424
                $row->writeAttribute('linkedid', $linkedid);
425
                $row->save();
426
            }
427
428
            /**
429
             * Отправка UserEvent
430
             */
431
            $AgiData = base64_encode(json_encode($n_data));
432
            $this->am->UserEvent('CdrConnector', ['AgiData' => $AgiData]);
433
        } // Обход текущих каналов.
434
    }
435
436
    /**
437
     * Обработка события уничтожения канала.
438
     * @param array $data
439
     * @param array $transfer_calls
440
     * @param array $channels
441
     */
442
    private function hangupChanEndCalls(array $data, array &$transfer_calls, array &$channels):void{
443
        $filter         = [
444
            'linkedid=:linkedid: AND endtime = "" AND (src_chan=:src_chan: OR dst_chan=:dst_chan:)',
445
            'bind' => [
446
                'linkedid' => $data['linkedid'],
447
                'src_chan' => $data['agi_channel'],
448
                'dst_chan' => $data['agi_channel'],
449
            ],
450
        ];
451
        /** @var CallDetailRecordsTmp $m_data */
452
        /** @var CallDetailRecordsTmp $row */
453
        $m_data = CallDetailRecordsTmp::find($filter);
454
        foreach ($m_data as $row) {
455
            if ($row->transfer == 1) {
456
                $transfer_calls[] = $row->toArray();
457
            }
458
            if ($row->dialstatus === 'ORIGINATE') {
459
                $row->writeAttribute('dialstatus', '');
460
                if($row->answer === ''){
461
                    $newId = $row->linkedid.'_'.$row->src_num.'_'.substr($row->src_chan, strpos($row->src_chan,'-') +1);
462
                    $row->writeAttribute('UNIQUEID', $newId);
463
                }
464
            }
465
            $row->writeAttribute('endtime', $data['end']);
466
            $row->writeAttribute('transfer', 0);
467
            if ($data['dialstatus'] !== '') {
468
                if ($data['dialstatus'] === 'ORIGINATE') {
469
                    $row->writeAttribute('dst_chan', '');
470
                }
471
                $row->writeAttribute('dialstatus', $data['dialstatus']);
472
            }
473
            $res = $row->update();
474
            if ( ! $res) {
475
                Util::sysLogMsg('Action_hangup_chan', implode(' ', $row->getMessages()));
476
            }
477
478
            if ($row->src_chan !== $data['agi_channel']) {
479
                $channels[] = [
480
                    'chan' => $row->src_chan,
481
                    'did'  => $row->did,
482
                    'num'  => $row->src_num,
483
                    'out'  => true,
484
                ];
485
            } else {
486
                $this->StopMixMonitor($row->dst_chan);
487
                $channels[] = [
488
                    'chan' => $row->dst_chan,
489
                    'did'  => $row->did,
490
                    'num'  => $row->dst_num,
491
                    'out'  => false,
492
                ];
493
            }
494
        }
495
    }
496
497
    /**
498
     * Логирование истории звонков при трасфере.
499
     *
500
     * @param       $action
501
     * @param       $data
502
     * @param ?array $calls_data
503
     */
504
    public function Action_CreateRowTransfer($action, $data, $calls_data = null): void
505
    {
506
        if( isset($this->checkChanHangupTransfer[$data['agi_channel']]) ) {
507
            return;
508
        }
509
510
        $this->checkChanHangupTransfer[$data['agi_channel']] = $action;
511
512
        if (null === $calls_data) {
513
            $filter     = [
514
                'linkedid=:linkedid: AND endtime = "" AND transfer=1 AND (src_chan=:chan: OR dst_chan=:chan:)',
515
                'bind'  => [
516
                    'linkedid' => $data['linkedid'],
517
                    'chan'     => $data['agi_channel'],
518
                ],
519
                'order' => 'is_app',
520
            ];
521
            $m_data     = CallDetailRecordsTmp::find($filter);
522
            $calls_data = $m_data->toArray();
523
        }
524
525
        if (count($calls_data) === 2) {
526
            $insert_data = [];
527
            foreach ($calls_data as $row_data) {
528
                if ($row_data['src_chan'] === $data['agi_channel']) {
529
                    $fname_chan = isset($insert_data['dst_chan']) ? 'src_chan' : 'dst_chan';
530
                    $fname_num  = isset($insert_data['dst_num']) ? 'src_num' : 'dst_num';
531
532
                    $insert_data[$fname_chan] = $row_data['dst_chan'];
533
                    $insert_data[$fname_num]  = $row_data['dst_num'];
534
                } else {
535
                    $fname_chan = ! isset($insert_data['src_chan']) ? 'src_chan' : 'dst_chan';
536
                    $fname_num  = ! isset($insert_data['src_num']) ? 'src_num' : 'dst_num';
537
538
                    $insert_data[$fname_chan] = $row_data['src_chan'];
539
                    $insert_data[$fname_num]  = $row_data['src_num'];
540
                }
541
            }
542
            // Запись разговора.
543
            $this->StopMixMonitor($insert_data['src_chan']);
544
            $this->StopMixMonitor($insert_data['dst_chan']);
545
            $recordingfile = $this->MixMonitor(
546
                $insert_data['dst_chan'],
547
                'transfer_' . $insert_data['src_num'] . '_' . $insert_data['dst_num'] . '_' . $data['linkedid']
548
            );
549
            //
550
            $insert_data['action']        = "{$action}_end_trasfer";
551
            $insert_data['recordingfile'] = $recordingfile;
552
            $insert_data['start']         = $data['end'];
553
            $insert_data['answer']        = $data['end'];
554
            $insert_data['linkedid']      = $data['linkedid'];
555
            $insert_data['UNIQUEID']      = $data['agi_threadid'];
556
            $insert_data['did']           = $data['did'];
557
            $insert_data['transfer']      = '0';
558
559
            /**
560
             * Отправка UserEvent
561
             */
562
            $insert_data['action'] = 'transfer_dial_create_cdr';
563
564
            $AgiData               = base64_encode(json_encode($insert_data));
565
            $this->am->UserEvent('CdrConnector', ['AgiData' => $AgiData]);
566
567
            self::insertDataToDbM($insert_data);
568
        } elseif (empty($calls_data[0]['answer']) && count($calls_data) === 1 && ! empty($calls_data[0]['recordingfile'])) {
569
            // Возобновление записи разговора при срыве переадресации.
570
            $row_data = $calls_data[0];
571
            $chan     = ($data['agi_channel'] === $row_data['src_chan']) ? $row_data['dst_chan'] : $row_data['src_chan'];
572
            $filter   = [
573
                'linkedid=:linkedid: AND endtime = ""',
574
                'bind'  => [
575
                    'linkedid' => $data['linkedid'],
576
                ],
577
                'order' => 'is_app',
578
            ];
579
            /** @var CallDetailRecordsTmp $not_ended_cdr */
580
            $cdr = CallDetailRecordsTmp::find($filter);
581
            /** @var CallDetailRecordsTmp $row */
582
            $not_ended_cdr = null;
583
            $transferNotComplete = false;
584
            foreach ($cdr as $row){
585
                if($row->transfer === '1' && ($row->src_chan === $chan || $row->dst_chan === $chan) ){
586
                    $not_ended_cdr = $row;
587
                }
588
                if($row->answer === '' && $row->endtime === ''
589
                    && ($row->src_chan === $chan || $row->dst_chan === $chan) ){
590
                    $transferNotComplete = true;
591
                }
592
            }
593
594
            if ($not_ended_cdr !== null && !$transferNotComplete) {
595
                $this->StopMixMonitor($not_ended_cdr->src_chan);
596
                $this->MixMonitor($not_ended_cdr->dst_chan, '', '', $not_ended_cdr->recordingfile);
597
            }
598
        }
599
    }
600
601
    /**
602
     * Обработка начала переадресации.
603
     *
604
     * @param $data
605
     */
606
    public function Action_transfer_dial($data): void
607
    {
608
        // Завершаем предыдущий неудачный Dial.
609
        // Необходимо в том случае, если не был создан канал назначения.
610
        $this->transferDialEndFailCdr($data);
611
612
        $this->Action_transfer_check($data);
613
        self::insertDataToDbM($data);
614
        $this->Action_app_end($data);
615
    }
616
617
    private function transferDialEndFailCdr($data):void{
618
        $filter = [
619
            'transfer=1 AND endtime = "" AND dst_chan="" AND linkedid=:linkedid:',
620
            'bind' => [
621
                'linkedid' => $data['linkedid']
622
            ],
623
        ];
624
        $m_data = CallDetailRecordsTmp::find($filter);
625
        /** @var CallDetailRecordsTmp $row */
626
        foreach ($m_data as $row) {
627
            // Установим признак переадресации.
628
            $row->writeAttribute('endtime', $data['start']);
629
            $row->save();
630
        }
631
    }
632
633
    /**
634
     * Проверка на транфер
635
     *
636
     * @param $data
637
     */
638
    public function Action_transfer_check($data): void
639
    {
640
        $filter = [
641
            'linkedid=:linkedid: AND endtime = "" AND transfer=0 AND (src_chan=:src_chan: OR dst_chan=:src_chan:)',
642
            'bind' => [
643
                'linkedid' => $data['linkedid'],
644
                'src_chan' => $data['src_chan'],
645
            ],
646
        ];
647
        /** @var CallDetailRecordsTmp $m_data */
648
        /** @var CallDetailRecordsTmp $row */
649
        $m_data = CallDetailRecordsTmp::find($filter);
650
        foreach ($m_data as $row) {
651
            // Пробуем остановить записть разговора.
652
            $this->StopMixMonitor($row->dst_chan);
653
            $this->StopMixMonitor($row->src_chan);
654
            // Установим признак переадресации.
655
            $row->writeAttribute('transfer', 1);
656
            $row->save();
657
        }
658
    }
659
660
    /**
661
     * Обработка события создания канала - пары, при начале переадресации звонка.
662
     *
663
     * @param $data
664
     */
665
    public function Action_transfer_dial_create_chan($data): void
666
    {
667
        $filter     = [
668
            'UNIQUEID=:UNIQUEID: AND endtime = "" AND answer = ""',
669
            'bind' => [
670
                'UNIQUEID' => $data['transfer_UNIQUEID'],
671
            ],
672
        ];
673
        $row_create = false;
674
        /** @var CallDetailRecordsTmp $m_data */
675
        /** @var CallDetailRecordsTmp $row */
676
        $m_data = CallDetailRecordsTmp::find($filter);
677
        foreach ($m_data as $row) {
678
            ///
679
            // Проверим, если более одного канала SIP/256 при входящем.
680
            if ( ! empty($row->dst_chan) && $data['dst_chan'] !== $row->dst_chan) {
681
                if ($row_create) {
682
                    continue;
683
                }
684
                // Необходимо дублировать строку звонка.
685
                $new_row = new CallDetailRecordsTmp();
686
                $f_list  = $row->toArray();
687
                foreach ($f_list as $attribute => $value) {
688
                    if ($attribute === 'id') {
689
                        continue;
690
                    }
691
                    $new_row->writeAttribute($attribute, $value);
692
                }
693
                $new_row->writeAttribute('dst_chan', $data['dst_chan']);
694
                $new_row->writeAttribute('UNIQUEID', $data['transfer_UNIQUEID'] . '_' . $data['dst_chan']);
695
                // $new_row->save();
696
                // Подмена $row;
697
                $row        = $new_row;
698
                $row_create = true;
699
            }
700
701
            // конец проверки
702
            ///
703
704
            $row->writeAttribute('dst_chan', $data['dst_chan']);
705
            $row->writeAttribute('recordingfile', $data['recordingfile']);
706
            if (isset($data['dst_call_id']) && ! empty($data['dst_call_id'])) {
707
                $row->writeAttribute('dst_call_id', $data['dst_call_id']);
708
            }
709
            $res = $row->save();
710
            if ( ! $res) {
711
                Util::sysLogMsg('Action_transfer_dial_create_chan', implode(' ', $row->getMessages()));
712
            }
713
        }
714
    }
715
716
    /**
717
     * Обработка события ответа на переадресацию. Соединение абонентов.
718
     *
719
     * @param $data
720
     */
721
    public function Action_transfer_dial_answer($data): void
722
    {
723
        $filter = [
724
            '(UNIQUEID=:UNIQUEID: OR UNIQUEID=:UNIQUEID_CHAN:) AND answer = "" AND endtime = ""',
725
            'bind' => [
726
                'UNIQUEID'      => $data['transfer_UNIQUEID'],
727
                'UNIQUEID_CHAN' => $data['transfer_UNIQUEID'] . '_' . $data['agi_channel'],
728
            ],
729
        ];
730
731
        /** @var CallDetailRecordsTmp $m_data */
732
        /** @var CallDetailRecordsTmp $row */
733
        $m_data = CallDetailRecordsTmp::find($filter);
734
        foreach ($m_data as $row) {
735
            $row->writeAttribute('answer', $data['answer']);
736
            $recFile = $data['recordingfile']??'';
737
            if(!empty($recFile)){
738
                $this->mixMonitorChannels[$data['agi_channel']] = $recFile;
739
                $row->writeAttribute('recordingfile', $recFile);
740
            }
741
            $res = $row->save();
742
            if ( !$res) {
743
                Util::sysLogMsg('Action_transfer_dial_answer', implode(' ', $row->getMessages()));
744
            }
745
        }
746
    }
747
748
    /**
749
     * Завершение канала при прееадресации.
750
     *
751
     * @param $data
752
     */
753
    public function Action_transfer_dial_hangup($data): void
754
    {
755
        $pos = stripos($data['agi_channel'], 'local/');
756
        if ($pos === false) {
757
            // Это НЕ локальный канал.
758
            // Если это завершение переадресации (консультативной). Создадим новую строку CDR.
759
            $this->Action_CreateRowTransfer('transfer_dial_hangup', $data);
760
761
            // Найдем записанные ранее строки.
762
            $filter = [
763
                'linkedid=:linkedid: AND endtime = "" AND (src_chan=:chan: OR dst_chan=:chan:)',
764
                'bind' => [
765
                    'linkedid' => $data['linkedid'],
766
                    'chan'     => $data['agi_channel'],
767
                ],
768
            ];
769
            /** @var CallDetailRecordsTmp $m_data */
770
            /** @var CallDetailRecordsTmp $row */
771
            $m_data = CallDetailRecordsTmp::find($filter);
772
            foreach ($m_data as $row) {
773
                // Завершим вызов в CDR.
774
                $row->writeAttribute('endtime', $data['end']);
775
                $row->writeAttribute('transfer', 0);
776
                if ( ! $row->save()) {
777
                    Util::sysLogMsg('Action_transfer_dial_answer', implode(' ', $row->getMessages()));
778
                }
779
            }
780
            // Попробуем начать запись разговора.
781
            $filter = [
782
                'linkedid=:linkedid: AND endtime = "" AND transfer=1',
783
                'bind' => [
784
                    'linkedid' => $data['linkedid'],
785
                ],
786
            ];
787
            /** @var CallDetailRecordsTmp $res */
788
            $res = CallDetailRecordsTmp::findFirst($filter);
789
            if ($res !== null) {
790
                $info      = pathinfo($res->recordingfile);
791
                $data_time = (empty($res->answer)) ? $res->start : $res->answer;
792
                $subdir    = date('Y/m/d/H/', strtotime($data_time));
793
                $this->MixMonitor($res->src_chan, $info['filename'], $subdir);
794
            }
795
        } elseif ('' === $data['ANSWEREDTIME']) {
796
            $filter = [
797
                'linkedid=:linkedid: AND endtime = "" AND (src_chan=:src_chan: OR dst_chan=:dst_chan:)',
798
                'bind' => [
799
                    'linkedid' => $data['linkedid'],
800
                    'src_chan' => $data['TRANSFERERNAME'],
801
                    'dst_chan' => $data['dst_chan'],
802
                ],
803
            ];
804
            /** @var CallDetailRecordsTmp $m_data */
805
            /** @var CallDetailRecordsTmp $row */
806
            $m_data = CallDetailRecordsTmp::find($filter);
807
            foreach ($m_data as $row) {
808
                // Ответа не было. Переадресация отменена.
809
                $row->writeAttribute('endtime', $data['end']);
810
                $row->writeAttribute('transfer', 0);
811
                if ( ! $row->save()) {
812
                    Util::sysLogMsg('Action_transfer_dial_answer', implode(' ', $row->getMessages()));
813
                }
814
            }
815
816
            // Попробуем возобновить запись разговора.
817
            $filter = [
818
                'linkedid=:linkedid: AND endtime = ""',
819
                'bind' => [
820
                    'linkedid' => $data['linkedid'],
821
                ],
822
            ];
823
            $m_data = CallDetailRecordsTmp::find($filter);
824
            foreach ($m_data as $row) {
825
                $info      = pathinfo($row->recordingfile);
826
                $data_time = ($row->answer == null) ? $row->start : $row->answer;
827
                $subdir    = date('Y/m/d/H/', strtotime($data_time));
828
                $this->MixMonitor($row->src_chan, $info['filename'], $subdir);
829
                // Снимем со строк признак переадресации.
830
                $row->writeAttribute('transfer', 0);
831
                if ( ! $row->save()) {
832
                    Util::sysLogMsg('Action_transfer_dial_answer', implode(' ', $row->getMessages()));
833
                }
834
            }
835
        }
836
    }
837
838
    /**
839
     * Начало работы приложения.
840
     *
841
     * @param $data
842
     */
843
    public function Action_dial_app($data): void
844
    {
845
        $this->Action_app_end($data);
846
        self::insertDataToDbM($data);
847
    }
848
849
    /**
850
     * Вызов в нерабочее время.
851
     *
852
     * @param $data
853
     */
854
    public function Action_dial_outworktimes($data): void
855
    {
856
        self::insertDataToDbM($data);
857
    }
858
859
    /**
860
     * Старт очереди.
861
     *
862
     * @param $data
863
     */
864
    public function Action_queue_start($data): void
865
    {
866
        if ($data['transfer'] === '1') {
867
            // Если это трансфер выполним поиск связанных данных.
868
            $this->Action_transfer_check($data);
869
        }
870
        if (isset($data['start'])) {
871
            // Это новая строка.
872
            self::insertDataToDbM($data);
873
        } else {
874
            // Требуется только обновление данных.
875
            $this->updateDataInDbM($data);
876
        }
877
        $this->Action_app_end($data);
878
    }
879
880
    /**
881
     * Событие входа в конференцкомнату.
882
     *
883
     * @param $data
884
     */
885
    public function Action_meetme_dial($data): void
886
    {
887
        $this->StopMixMonitor($data['src_chan']);
888
889
        if (strpos($data['src_chan'], 'internal-originate') !== false) {
890
            // Уточним канал и ID записи;
891
            $filter = [
892
                'linkedid=:linkedid: AND src_num=:src_num:',
893
                'bind' => [
894
                    'linkedid' => $data['linkedid'],
895
                    'src_num'  => $data['src_num'],
896
                ],
897
            ];
898
            /** @var CallDetailRecordsTmp $m_data */
899
            /** @var CallDetailRecordsTmp $row */
900
            $m_data = CallDetailRecordsTmp::findFirst($filter);
901
            if ($m_data !== null) {
902
                $data['src_chan'] = $m_data->src_chan;
903
                $m_data->UNIQUEID = $data['UNIQUEID'];
904
905
                $f_list = $m_data->toArray();
906
                foreach ($data as $attribute => $value) {
907
                    if ( ! array_key_exists($attribute, $f_list)) {
908
                        continue;
909
                    }
910
                    $m_data->writeAttribute($attribute, $value);
911
                }
912
                // Переопределяем идентификатор, это Originate на конференцию.
913
                $m_data->save();
914
            }
915
        } else {
916
            self::insertDataToDbM($data);
917
            $this->Action_app_end($data);
918
        }
919
    }
920
921
    /**
922
     * Снятие вызова с парковки.
923
     *
924
     * @param $data
925
     */
926
    public function Action_unpark_call($data): void
927
    {
928
        $data['recordingfile'] = $this->MixMonitor($data['dst_chan'], $data['UNIQUEID']);
929
        self::insertDataToDbM($data);
930
        if (is_array($data['data_parking'])) {
931
            self::insertDataToDbM($data['data_parking']);
932
        }
933
        $filter = [
934
            "linkedid=:linkedid: AND src_chan=:src_chan:",
935
            'bind' => [
936
                'linkedid' => $data['linkedid_old'],
937
                'src_chan' => $data['agi_channel'],
938
            ],
939
        ];
940
        /** @var CallDetailRecordsTmp $m_data */
941
        /** @var CallDetailRecordsTmp $row */
942
        $m_data = CallDetailRecordsTmp::find($filter);
943
        foreach ($m_data as $row) {
944
            $row->delete();
945
        }
946
    }
947
948
    /**
949
     * Возвращаем вызов с парковки по таймауту.
950
     *
951
     * @param $data
952
     */
953
    public function Action_unpark_call_timeout($data): void
954
    {
955
        self::insertDataToDbM($data);
956
    }
957
958
    /**
959
     * Ответ агента очереди.
960
     *
961
     * @param $data
962
     */
963
    public function Action_queue_answer($data): void
964
    {
965
        $filter = [
966
            'UNIQUEID=:UNIQUEID: AND answer = ""',
967
            'bind' => [
968
                'UNIQUEID' => $data['id'],
969
            ],
970
        ];
971
        /** @var CallDetailRecordsTmp $m_data */
972
        /** @var CallDetailRecordsTmp $row */
973
        $m_data = CallDetailRecordsTmp::find($filter);
974
        foreach ($m_data as $row) {
975
            $row->writeAttribute('answer', $data['answer']);
976
            $row->writeAttribute('endtime', $data['answer']);
977
            $res = $row->save();
978
            if ( ! $res) {
979
                Util::sysLogMsg('Action_queue_answer', implode(' ', $row->getMessages()));
980
            }
981
        }
982
    }
983
984
    /**
985
     * Завершение работы очереди.
986
     *
987
     * @param $data
988
     */
989
    public function Action_queue_end($data): void
990
    {
991
        $filter = [
992
            "UNIQUEID=:UNIQUEID:",
993
            'bind' => [
994
                'UNIQUEID' => $data['id'],
995
            ],
996
        ];
997
        /** @var CallDetailRecordsTmp $m_data */
998
        /** @var CallDetailRecordsTmp $row */
999
        $m_data = CallDetailRecordsTmp::find($filter);
1000
        foreach ($m_data as $row) {
1001
            $row->writeAttribute('endtime', $data['end']);
1002
            $row->writeAttribute('is_app', 1);
1003
            if ($data['dialstatus'] != '') {
1004
                $row->writeAttribute('dialstatus', $data['dialstatus']);
1005
            }
1006
            $res = $row->save();
1007
            if ( ! $res) {
1008
                Util::sysLogMsg('Action_queue_end', implode(' ', $row->getMessages()));
1009
            }
1010
        }
1011
    }
1012
1013
    /**
1014
     * Завершение / уничтожение канала.
1015
     *
1016
     * @param $data
1017
     */
1018
    public function Action_hangup_chan_meetme($data): void
1019
    {
1020
        clearstatcache();
1021
        $recordingfile = '';
1022
        $dest_chan     = "MeetMe:{$data['conference']}";
1023
        // Отбираем все строки по текущей конференции. Не завершенные вызовы.
1024
        $filter = [
1025
            'dst_chan=:dst_chan: OR linkedid=:linkedid:',
1026
            'bind' => [
1027
                'linkedid' => $data['linkedid'],
1028
                'dst_chan' => $dest_chan,
1029
            ],
1030
        ];
1031
        $m_data = CallDetailRecordsTmp::find($filter);
1032
        /** @var CallDetailRecordsTmp $row */
1033
        foreach ($m_data as $row) {
1034
            if ($dest_chan === $row->dst_chan) {
1035
                $is_local        = (stripos($data['src_chan'], 'local/') !== false);
1036
                $is_stored_local = (stripos($row->src_chan, 'local/') !== false);
1037
                if ($row->UNIQUEID === $data['UNIQUEID'] && $is_local && ! $is_stored_local) {
1038
                    $data['src_chan'] = $row->src_chan;
1039
                }
1040
                if (file_exists($row->recordingfile) || file_exists(
1041
                        Util::trimExtensionForFile($row->recordingfile) . '.wav'
1042
                    )) {
1043
                    // Переопределим путь к файлу записи разговора. Для конферецнии файл один.
1044
                    $recordingfile = $row->recordingfile;
1045
                }
1046
            }
1047
            if ($row->linkedid === $data['meetme_id']) {
1048
                continue;
1049
            }
1050
            // Подменим ID на идентификатор конференции.
1051
            $row->writeAttribute('linkedid', $data['meetme_id']);
1052
            $res = $row->save();
1053
            if ( ! $res) {
1054
                Util::sysLogMsg('Action_hangup_chan_meetme', implode(' ', $row->getMessages()));
1055
            }
1056
        }
1057
1058
        /** @var CallDetailRecordsTmp $m_data */
1059
        /** @var CallDetailRecordsTmp $row */
1060
        /** @var CallDetailRecordsTmp $rec_data */
1061
        foreach ($m_data as $row) {
1062
            if ($row->src_chan !== $data['src_chan']) {
1063
                continue;
1064
            }
1065
            // Заполняем данные для текущего оповещения.
1066
            $row->writeAttribute('endtime', $data['end']);
1067
            $row->writeAttribute('transfer', 0);
1068
            $row->writeAttribute('linkedid', $data['meetme_id']);
1069
1070
            if ( ! empty($recordingfile)) {
1071
                $row->writeAttribute('recordingfile', $recordingfile);
1072
            }
1073
            $res = $row->save();
1074
            if ( ! $res) {
1075
                Util::sysLogMsg('Action_hangup_chan_meetme', implode(' ', $row->getMessages()));
1076
            }
1077
        }
1078
    }
1079
1080
    /**
1081
     *
1082
     * @param $argv
1083
     */
1084
    public function start($argv): void
1085
    {
1086
        $this->mixMonitorChannels       = [];
1087
        $this->checkChanHangupTransfer  = [];
1088
        $mikoPBXConfig            = new MikoPBXConfig();
1089
        $this->record_calls       = $mikoPBXConfig->getGeneralSettings('PBXRecordCalls') === '1';
1090
        $this->split_audio_thread = $mikoPBXConfig->getGeneralSettings('PBXSplitAudioThread') === '1';
1091
        $this->am                 = Util::getAstManager('off');
1092
1093
        // PID сохраняем при начале работы Worker.
1094
        $client = new BeanstalkClient(self::class);
1095
        $client->subscribe(self::class, [$this, 'callEventsWorker']);
1096
        $client->subscribe(WorkerCdr::SELECT_CDR_TUBE, [$this, 'selectCDRWorker']);
1097
        $client->subscribe(WorkerCdr::UPDATE_CDR_TUBE, [$this, 'updateCDRWorker']);
1098
        $client->subscribe($this->makePingTubeName(self::class), [$this, 'pingCallBack']);
1099
        $client->setErrorHandler([$this, 'errorHandler']);
1100
1101
        while (true) {
1102
            $client->wait();
1103
        }
1104
    }
1105
1106
    /**
1107
     * Обработчик событий изменения состояния звонка.
1108
     *
1109
     * @param array | BeanstalkClient $tube
1110
     */
1111
    public function callEventsWorker($tube): void
1112
    {
1113
        $data      = json_decode($tube->getBody(), true);
1114
        $funcName = "Action_".$data['action']??'';
1115
        if ( method_exists($this, $funcName) ) {
1116
            $this->$funcName($data);
1117
        }
1118
        $tube->reply(json_encode(true));
1119
    }
1120
1121
1122
    /**
1123
     * Получения CDR к обработке.
1124
     *
1125
     * @param array | BeanstalkClient $tube
1126
     */
1127
    public function updateCDRWorker($tube): void
1128
    {
1129
        $q    = $tube->getBody();
1130
        $data = json_decode($q, true);
1131
        $res  = $this->updateDataInDbM($data);
1132
        $tube->reply(json_encode($res));
1133
    }
1134
1135
    /**
1136
     * Обновление данных в базе.
1137
     *
1138
     * @param $data
1139
     *
1140
     * @return bool
1141
     */
1142
    public function updateDataInDbM($data): bool
1143
    {
1144
        if (empty($data['UNIQUEID'])) {
1145
            Util::sysLogMsg(__FUNCTION__, 'UNIQUEID is empty ' . json_encode($data));
1146
1147
            return false;
1148
        }
1149
1150
        $filter = [
1151
            "UNIQUEID=:id:",
1152
            'bind' => ['id' => $data['UNIQUEID'],],
1153
        ];
1154
        /** @var CallDetailRecordsTmp $m_data */
1155
        $m_data = CallDetailRecordsTmp::findFirst($filter);
1156
        if ($m_data === null) {
1157
            return true;
1158
        }
1159
        $f_list = $m_data->toArray();
1160
        foreach ($data as $attribute => $value) {
1161
            if ( ! array_key_exists($attribute, $f_list)) {
1162
                continue;
1163
            }
1164
            if ('UNIQUEID' === $attribute) {
1165
                continue;
1166
            }
1167
            $m_data->writeAttribute($attribute, $value);
1168
        }
1169
        $res = $m_data->save();
1170
        if ( ! $res) {
1171
            Util::sysLogMsg(__FUNCTION__, implode(' ', $m_data->getMessages()));
1172
        }
1173
1174
        /**
1175
         * Отправка UserEvent
1176
         */
1177
        $insert_data = $m_data->toArray();
1178
        if ($insert_data['work_completed'] == 1) {
1179
            $insert_data['action']        = "hangup_update_cdr";
1180
            $insert_data['GLOBAL_STATUS'] = isset($data['GLOBAL_STATUS']) ? $data['GLOBAL_STATUS'] : $data['disposition'];
1181
            unset($insert_data['src_chan']);
1182
            unset($insert_data['dst_chan']);
1183
            unset($insert_data['work_completed']);
1184
            unset($insert_data['did']);
1185
            unset($insert_data['id']);
1186
            unset($insert_data['from_account']);
1187
            unset($insert_data['to_account']);
1188
            unset($insert_data['appname']);
1189
            unset($insert_data['is_app']);
1190
            unset($insert_data['transfer']);
1191
1192
            $AgiData = base64_encode(json_encode($insert_data));
1193
            $this->am->UserEvent('CdrConnector', ['AgiData' => $AgiData]);
1194
        }
1195
1196
        return $res;
1197
    }
1198
1199
    /**
1200
     * Помещаем данные в базу используя модели.
1201
     *
1202
     * @param array $data
1203
     *
1204
     * @return bool
1205
     */
1206
    public static function insertDataToDbM($data): bool
1207
    {
1208
        if (empty($data['UNIQUEID'])) {
1209
            Util::sysLogMsg(__FUNCTION__, 'UNIQUEID is empty ' . json_encode($data));
1210
1211
            return false;
1212
        }
1213
1214
        $is_new = false;
1215
        /** @var CallDetailRecordsTmp $m_data */
1216
        $m_data = CallDetailRecordsTmp::findFirst(
1217
            [
1218
                "UNIQUEID=:id:",
1219
                'bind' => ['id' => $data['UNIQUEID'],],
1220
            ]
1221
        );
1222
        if ($m_data === null) {
1223
            $m_data = new CallDetailRecordsTmp();
1224
            $is_new = true;
1225
        } elseif (isset($data['IS_ORGNT']) && $data['IS_ORGNT'] !== false && $data['action'] === 'dial') {
1226
            if (empty($m_data->endtime)) {
1227
                // Если это оригинация dial может прийти дважды.
1228
                if(!empty($m_data->src_num) && $m_data->src_num === $data['dst_num']){
1229
                    $m_data->dst_num = $data['src_num'];
1230
                    $m_data->save();
1231
                }
1232
                return true;
1233
            }
1234
            // Предыдущие звонки завершены. Текущий вызов новый, к примеру через резервного провайдера.
1235
            // Меняем идентификатор предыдущих звонков.
1236
            $m_data->UNIQUEID .= Util::generateRandomString(5);
1237
            // Чистим путь к файлу записи.
1238
            $m_data->recordingfile = "";
1239
            $m_data->save();
1240
1241
            $new_m_data               = new CallDetailRecordsTmp();
1242
            $new_m_data->UNIQUEID     = $data['UNIQUEID'];
1243
            $new_m_data->start        = $data['start'];
1244
            $new_m_data->src_chan     = $m_data->src_chan;
1245
            $new_m_data->src_num      = $m_data->src_num;
1246
            $new_m_data->dst_num      = $data['src_num'];
1247
            $new_m_data->did          = $data['did'];
1248
            $new_m_data->from_account = $data['from_account'];
1249
            $new_m_data->linkedid     = $data['linkedid'];
1250
            $new_m_data->transfer     = $data['transfer'];
1251
1252
            $res = $new_m_data->save();
1253
            if ( ! $res) {
1254
                Util::sysLogMsg(__FUNCTION__, implode(' ', $m_data->getMessages()));
1255
            }
1256
1257
            return $res;
1258
        }
1259
1260
        $f_list = $m_data->toArray();
1261
        foreach ($data as $attribute => $value) {
1262
            if ( ! array_key_exists($attribute, $f_list)) {
1263
                continue;
1264
            }
1265
            if ($is_new === false && 'UNIQUEID' === $attribute) {
1266
                continue;
1267
            }
1268
            $m_data->writeAttribute($attribute, $value);
1269
        }
1270
        $res = $m_data->save();
1271
        if ( ! $res) {
1272
            Util::sysLogMsg(__FUNCTION__, implode(' ', $m_data->getMessages()));
1273
        }
1274
1275
        return $res;
1276
    }
1277
1278
    /**
1279
     * @param array | BeanstalkClient $tube
1280
     */
1281
    public function selectCDRWorker($tube): void
1282
    {
1283
        $q      = $tube->getBody();
1284
        $filter = json_decode($q, true);
1285
1286
        if($this->filterNotValid($filter)){
1287
            $tube->reply('[]');
1288
            return;
1289
        }
1290
1291
        $res    = null;
1292
        try {
1293
            if (isset($filter['miko_tmp_db'])) {
1294
                $res = CallDetailRecordsTmp::find($filter);
1295
            } else {
1296
                $res = CallDetailRecords::find($filter);
1297
            }
1298
            $res_data = json_encode($res->toArray());
1299
        } catch (Throwable $e) {
1300
            $res_data = '[]';
1301
        }
1302
1303
        if ($res && isset($filter['add_pack_query'])) {
1304
            $arr = [];
1305
            foreach ($res->toArray() as $row) {
1306
                $arr[] = $row[$filter['columns']];
1307
            }
1308
            $filter['add_pack_query']['bind'][$filter['columns']] = $arr;
1309
1310
            if($this->filterNotValid($filter['add_pack_query'])){
1311
                $tube->reply('[]');
1312
                return;
1313
            }
1314
1315
            try {
1316
                $res      = CallDetailRecords::find($filter['add_pack_query']);
1317
                $res_data = json_encode($res->toArray(), JSON_THROW_ON_ERROR);
1318
            } catch (Throwable $e) {
1319
                $res_data = '[]';
1320
            }
1321
        }
1322
1323
        if (isset($filter['miko_result_in_file'])) {
1324
            $di         = Di::getDefault();
1325
            $dirsConfig = $di->getShared('config');
1326
            $filename   = $dirsConfig->path('core.tempDir') . '/' . md5(microtime(true));
1327
            file_put_contents($filename, $res_data);
1328
            Util::addRegularWWWRights($filename);
1329
            $res_data = json_encode($filename);
1330
        }
1331
1332
        $tube->reply($res_data);
1333
    }
1334
1335
    /**
1336
     * Проверка фильтра на корректность bind параметров.
1337
     *
1338
     * @param $filter
1339
     *
1340
     * @return bool
1341
     */
1342
    private function filterNotValid($filter){
1343
        $haveErrors = false;
1344
        if(isset($filter['bind'])){
1345
            if(is_array($filter)){
1346
                foreach ($filter['bind'] as $bindValue) {
1347
                    if(empty($bindValue)){
1348
                        $haveErrors = true;
1349
                    }
1350
                }
1351
            }else{
1352
                $haveErrors = true;
1353
            }
1354
        }
1355
        return $haveErrors;
1356
    }
1357
1358
    public function errorHandler($m): void
1359
    {
1360
        Util::sysLogMsg(self::class . '_ERROR', $m);
1361
    }
1362
}
1363
1364
1365
// Start worker process
1366
$workerClassname = WorkerCallEvents::class;
1367
$action = $argv[1] ?? '';
1368
if ($action === 'start') {
1369
    cli_set_process_title($workerClassname);
1370
    try {
1371
        /** @var WorkerCallEvents $worker */
1372
        $worker = new $workerClassname();
1373
        $worker->start($argv);
1374
    } catch (Throwable $e) {
1375
        global $errorLogger;
1376
        $errorLogger->captureException($e);
1377
        Util::sysLogMsg("{$workerClassname}_EXCEPTION", $e->getMessage());
1378
    }
1379
}
1380
1381
1382
1383
1384
1385