Passed
Pull Request — master (#16)
by Nikolay
13:10 queued 02:12
created

WorkerCallEvents::hangupChanCheckSipTrtansfer()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 54
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 37
c 2
b 0
f 0
dl 0
loc 54
rs 6.2666
cc 14
nc 37
nop 2

How to fix   Long Method    Complexity   

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