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

ExtensionsConf::generatePublicContext()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 20
rs 9.8666
cc 4
nc 6
nop 1
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, 9 2020
7
 */
8
9
namespace MikoPBX\Core\Asterisk\Configs;
10
11
use MikoPBX\Common\Models\{Iax, IncomingRoutingTable, OutgoingRoutingTable, OutWorkTimes, Providers, Sip, SoundFiles};
12
use MikoPBX\Modules\Config\ConfigClass;
13
use MikoPBX\Core\System\{MikoPBXConfig, Util};
14
use Phalcon\Di;
15
16
class ExtensionsConf extends ConfigClass
17
{
18
    protected string $description = 'extensions.conf';
19
20
    /**
21
     * Основной генератор extensions.conf
22
     */
23
    protected function generateConfigProtected(): void
24
    {
25
        /** @scrutinizer ignore-call */
26
        $additionalModules = $this->di->getShared('pbxConfModules');
27
        $conf              = "[globals] \n";
28
        $conf              .= "TRANSFER_CONTEXT=internal-transfer; \n";
29
        foreach ($additionalModules as $appClass) {
30
            $addition = $appClass->extensionGlobals();
31
            if (!empty($addition)){
32
                $conf .=$appClass->confBlockWithComments($addition);
33
            }
34
        }
35
        $conf .= "\n";
36
        $conf .= "\n";
37
        $conf .= "[general] \n";
38
        $conf .= "\n";
39
40
        // Создаем диалплан внутренних учеток.
41
        $this->generateOtherExten($conf);
42
        // Контекст для внутренних вызовов.
43
        $this->generateInternal($conf);
44
        // Контекст для внутренних переадресаций.
45
        $this->generateInternalTransfer($conf);
46
        // Создаем контекст хинтов.
47
        $this->generateSipHints($conf);
48
        // Создаем контекст (исходящие звонки).
49
        $this->generateOutContextPeers($conf);
50
        // Описываем контекст для публичных входящих.
51
        $this->generatePublicContext($conf);
52
        // Переключатель по времени.
53
        $this->generateOutWorkTimes($conf);
54
55
        Util::fileWriteContent($this->config->path('asterisk.astetcdir') . '/extensions.conf', $conf);
56
    }
57
58
    /**
59
     * Генератор прочих контекстов.
60
     *
61
     * @param $conf
62
     */
63
    private function generateOtherExten(&$conf): void
64
    {
65
        $extension = 'X!';
66
        // Контекст для AMI originate. Без него отображается не корректный CallerID.
67
        $conf .= '[sipregistrations]' . "\n\n";
68
69
        $conf .= '[messages]' . "\n" .
70
            'exten => _' . $extension . ',1,MessageSend(sip:${EXTEN},"${CALLERID(name)}"${MESSAGE(from)})' . "\n\n";
71
72
        $conf .= '[internal-originate]' . " \n";
73
        $conf .= 'exten => _' . $extension . ',1,NoOP(Hint ${HINT} exten ${EXTEN} )' . " \n";
74
        $conf .= '; Если это originate, то скроем один CDR.' . " \n\t";
75
        $conf .= 'same => n,ExecIf($["${pt1c_cid}x" != "x"]?Set(CALLERID(num)=${pt1c_cid}))' . " \n\t";
76
77
        $conf .= 'same => n,ExecIf($["${CUT(CHANNEL,\;,2)}" == "2"]?Set(__PT1C_SIP_HEADER=${SIPADDHEADER}))' . " \n\t";
78
        $conf .= 'same => n,ExecIf($["${peer_mobile}x" != "x"]?Set(ADDITIONAL_PEER=&Local/${peer_mobile}@outgoing/n))' . " \n\t";
79
80
        // Описываем возможность прыжка в пользовательский sub контекст.
81
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)' . "\n\t";
82
        $conf .= 'same => n,Dial(Local/${EXTEN}@internal-users/n${ADDITIONAL_PEER},60,cTteKkHhb(originate_create_chan,s,1))' . " \n\n";
83
84
        $conf .= '[originate_create_chan]' . " \n";
85
        $conf .= 'exten => s,1,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . "\n\t";
86
        $conf .= 'same => n,return' . " \n\n";
87
88
        $conf .= '[dial_create_chan]' . " \n";
89
        $conf .= 'exten => s,1,Gosub(lua_${ISTRANSFER}dial_create_chan,${EXTEN},1)' . "\n\t";
90
        $conf .= 'same => n,Set(pt1c_is_dst=1)' . " \n\t";
91
        $conf .= 'same => n,ExecIf($["${PT1C_SIP_HEADER}x" != "x"]?Set(PJSIP_HEADER(add,${CUT(PT1C_SIP_HEADER,:,1)})=${CUT(PT1C_SIP_HEADER,:,2)}))' . " \n\t";
92
        $conf .= 'same => n,Set(__PT1C_SIP_HEADER=${UNDEFINED})' . " \n\t";
93
        $conf .= 'same => n,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . " \n\t";
94
        $conf .= 'same => n,return' . " \n\n";
95
96
        $conf .= '[hangup_handler]' . "\n";
97
        $conf .= 'exten => s,1,NoOp(--- hangup - ${CHANNEL} ---)' . "\n\t";
98
        $conf .= 'same => n,Gosub(hangup_chan,${EXTEN},1)' . "\n\t";
99
100
        $conf .= 'same => n,return' . "\n\n";
101
102
        $conf .= '[set_orign_chan]' . "\n";
103
        $conf .= 'exten => s,1,Wait(0.2)' . "\n\t";
104
        $conf .= 'same => n,Set(pl=${IF($["${CHANNEL:-1}" == "1"]?2:1)})' . "\n\t";
105
        $conf .= 'same => n,Set(orign_chan=${IMPORT(${CUT(CHANNEL,\;,1)}\;${pl},BRIDGEPEER)})' . "\n\t";
106
        $conf .= 'same => n,ExecIf($[ "${orign_chan}x" == "x" ]?Set(orign_chan=${IMPORT(${CUT(CHANNEL,\;,1)}\;${pl},FROM_CHAN)}))' . "\n\t";
107
        $conf .= 'same => n,ExecIf($[ "${QUEUE_SRC_CHAN}x" != "x" ]?Set(__QUEUE_SRC_CHAN=${orign_chan}))' . "\n\t";
108
        $conf .= 'same => n,ExecIf($[ "${QUEUE_SRC_CHAN:0:5}" == "Local" ]?Set(__QUEUE_SRC_CHAN=${FROM_CHAN}))' . "\n\t";
109
        $conf .= 'same => n,ExecIf($[ "${FROM_CHAN}x" == "x" ]?Set(__FROM_CHAN=${IMPORT(${CUT(CHANNEL,\;,1)}\;${pl},BRIDGEPEER)}))' . "\n\t";
110
        $conf .= 'same => n,return' . "\n\n";
111
112
        $conf .= '[playback]' . "\n";
113
        $conf .= 'exten => s,1,Playback(hello_demo,noanswer)' . "\n\t";
114
        $conf .= 'same => n,ExecIf($["${SRC_BRIDGE_CHAN}x" == "x"]?Wait(30))' . "\n\t";
115
        $conf .= 'same => n,Wait(0.3)' . "\n\t";
116
        $conf .= 'same => n,Bridge(${SRC_BRIDGE_CHAN},kKTthH)' . "\n\n";
117
118
        $conf .= 'exten => h,1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\n";
119
120
        // TODO / Добавление / удаление префиксов на входящий callerid.
121
        $conf .= '[add-trim-prefix-clid]' . "\n";
122
        $conf .= 'exten => _.!,1,NoOp(--- Incoming call from ${CALLERID(num)} ---)' . "\n\t";
123
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)' . "\n\t";
124
        // Отсекаем "+".
125
        // $conf.= 'same => n,ExecIf( $["${CALLERID(num):0:1}" == "+"]?Set(CALLERID(num)=${CALLERID(num):1}))'."\n\t";
126
        // Отсекаем "7" и добавляем "8".
127
        // $conf.= 'same => n,ExecIf( $["${REGEX("^7[0-9]+" ${CALLERID(num)})}" == "1"]?Set(CALLERID(num)=8${CALLERID(num):1}))'."\n\t";
128
        $conf .= 'same => n,return' . "\n\n";
129
    }
130
131
    /**
132
     * Генератор контекста для внутренних вызовов.
133
     *
134
     * @param $conf
135
     */
136
    private function generateInternal(&$conf): void
137
    {
138
        $extension  = 'X!';
139
        $technology = SIPConf::getTechnology();
140
141
        $additionalModules = $this->di->getShared('pbxConfModules');
142
        foreach ($additionalModules as $appClass) {
143
            $addition = $appClass->extensionGenContexts();
144
            if (!empty($addition)){
145
                $conf .=$appClass->confBlockWithComments($addition);
146
            }
147
        }
148
        $conf .= "\n";
149
        $conf .= "[internal-num-undefined] \n";
150
        $conf .= 'exten => _' . $extension . ',1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\t";
151
        $conf .= 'same => n,ExecIf($["${BLINDTRANSFER}x" != "x"]?AGI(check_redirect.php,${BLINDTRANSFER}))' . "\n\t";
152
        $conf .= "same => n,Playback(pbx-invalid,noanswer) \n\n";
153
154
        $conf .= "[internal-fw]\n";
155
        $conf .= 'exten => _' . $extension . ',1,NoOp(DIALSTATUS - ${DIALSTATUS})' . "\n\t";
156
        // CANCEL - вызов был отменен, к примеру *0, не нужно дальше искать адресат.
157
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" == "CANCEL"]?Hangup())' . "\n\t";
158
        // BUSY - занято. К примру абонент завершил вызов или DND.
159
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" == "BUSY"]?Set(dstatus=FW_BUSY))' . "\n\t";
160
        // CHANUNAVAIL - канал не доступен. К примеру телефон не зарегистрирован или не отвечает.
161
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" == "CHANUNAVAIL"]?Set(dstatus=FW_UNAV))' . "\n\t";
162
        // NOANSWER - не ответили по таймауту.
163
        $conf .= 'same => n,ExecIf($["${dstatus}x" == "x"]?Set(dstatus=FW))' . "\n\t";
164
        $conf .= 'same => n,Set(fw=${DB(${dstatus}/${EXTEN})})' . "\n\t";
165
        $conf .= 'same => n,ExecIf($["${fw}x" != "x"]?Set(__pt1c_UNIQUEID=${UNDEFINED})' . "\n\t";
166
        $conf .= 'same => n,ExecIf($["${fw}x" != "x"]?Goto(internal,${fw},1))' . "\n\t";
167
        $conf .= 'same => n,ExecIf($["${BLINDTRANSFER}x" != "x"]?AGI(check_redirect.php,${BLINDTRANSFER}))' . "\n\t";
168
        $conf .= 'same => n,Hangup() ' . "\n\n";
169
170
        $conf .= "[all_peers]\n";
171
        $conf .= 'include => internal-hints' . "\n";
172
        $conf .= 'exten => failed,1,Hangup()' . "\n";
173
174
        $conf .= 'exten => _.!,1,ExecIf($[ "${EXTEN}" == "h" ]?Hangup())' . "\n\t";
175
        // Фильтр спецсимволов. Разершаем только цифры.
176
        $conf .= 'same => n,Set(cleanNumber=${FILTER(\*\#1234567890,${EXTEN})})' . "\n\t";
177
        $conf .= 'same => n,ExecIf($["${EXTEN}" != "${cleanNumber}"]?Goto(${CONTEXT},${cleanNumber},$[${PRIORITY} + 1]))' . "\n\t";
178
179
        $conf .= 'same => n,Set(__FROM_CHAN=${CHANNEL})' . "\n\t";
180
        $conf .= 'same => n,ExecIf($["${OLD_LINKEDID}x" == "x"]?Set(__OLD_LINKEDID=${CDR(linkedid)}))' . "\n\t";
181
        $conf .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" != "Local"]?Gosub(set_from_peer,s,1))' . "\n\t";
182
        $conf .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local"]?Gosub(set_orign_chan,s,1))' . "\n\t";
183
184
        $conf .= 'same => n,ExecIf($["${CALLERID(num)}x" == "x"]?Set(CALLERID(num)=${FROM_PEER}))' . "\n\t";
185
        $conf .= 'same => n,ExecIf($["${CALLERID(num)}x" == "x"]?Set(CALLERID(name)=${FROM_PEER}))' . "\n\t";
186
187
        $conf .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local" && "${FROM_PEER}x" == "x"]?Set(__FROM_PEER=${CALLERID(num)}))' . "\n\t";
188
        $conf .= 'same => n,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . "\n\t";
189
        $conf .= 'same => n,Gosub(${ISTRANSFER}dial,${EXTEN},1)' . "\n\t";
190
191
        // Описываем возможность прыжка в пользовательский sub контекст.
192
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)' . "\n\t";
193
194
        $conf .= 'same => n,Goto(peer_${FROM_PEER},${EXTEN},1)' . "\n\n";
195
196
        $pickupexten =  $this->generalSettings['PickupExten'];
197
        $conf        .= 'exten => _' . $pickupexten . $extension . ',1,Set(PICKUPEER=' . $technology . '/${FILTER(0-9,${EXTEN:2})})' . "\n\t";
198
        $conf        .= 'same => n,Set(pt1c_dnid=${EXTEN})' . "\n\t";
199
        $conf        .= 'same => n,PickupChan(${PICKUPEER})' . "\n\t";
200
        $conf        .= 'same => n,Hangup()' . "\n\n";
201
202
        $voicemail_exten = $this->generalSettings['VoicemailExten'];
203
        $conf            .= 'exten => ' . $voicemail_exten . ',1,NoOp(NOTICE, Dialing out from ${CALLERID(all)} to VoiceMail)' . "\n\t";
204
        $conf            .= 'same => n,VoiceMailMain(admin@voicemailcontext,s)' . "\n\t";
205
        $conf            .= 'same => n,Hangup()' . "\n\n";
206
207
        $conf .= "[voice_mail_peer] \n";
208
        $conf .= 'exten => voicemail,1,Answer()' . "\n\t";
209
        $conf .= 'same => n,VoiceMail(admin@voicemailcontext)' . "\n\t";
210
        $conf .= 'same => n,Hangup()' . "\n\n";
211
212
        // Контекст для внутренних вызовов.
213
        $conf .= "[internal] \n";
214
215
        foreach ($additionalModules as $appClass) {
216
            $addition = $appClass->getIncludeInternal();
217
            if (!empty($addition)){
218
                $conf .=$appClass->confBlockWithComments($addition);
219
            }
220
        }
221
222
        foreach ($additionalModules as $appClass) {
223
            $addition = $appClass->extensionGenInternal();
224
            if (!empty($addition)){
225
                $conf .=$appClass->confBlockWithComments($addition);
226
            }
227
        }
228
229
        $conf .= 'exten => i,1,NoOp(-- INVALID NUMBER --)' . "\n\t";
230
        $conf .= 'same => n,Set(DIALSTATUS=INVALID_NUMBER)' . "\n\t";
231
        $conf .= 'same => n,Playback(privacy-incorrect,noanswer)' . "\n\t";
232
        $conf .= 'same => n,Hangup()' . "\n";
233
234
        $conf .= 'exten => h,1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\n";
235
236
        $conf .= "[internal-incoming]\n";
237
        $conf .= 'exten => _.!,1,ExecIf($["${MASTER_CHANNEL(M_TIMEOUT)}x" != "x"]?Set(TIMEOUT(absolute)=${MASTER_CHANNEL(M_TIMEOUT)}))' . " \n\t";
238
        $conf .= 'same => n,Set(MASTER_CHANNEL(M_TIMEOUT_CHANNEL)=${CHANNEL})' . " \n\t";
239
        $conf .= 'same => n,Set(MASTER_CHANNEL(M_TIMEOUT)=${EMPTY_VAR})' . " \n\t";
240
        $conf .= 'same => n,Goto(internal,${EXTEN},1)' . " \n\n";
241
242
        $conf .= "[internal-users] \n";
243
        $conf .= 'exten => _' . $extension . ',1,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . " \n\t";
244
        $conf .= 'same => n,ExecIf($["${ISTRANSFER}x" != "x"]?Set(SIPADDHEADER01=${EMPTY_VAR})' . " \n\t";
245
        $conf .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local"]?Gosub(set_orign_chan,s,1))' . " \n\t";
246
247
        $conf .= 'same => n,Gosub(${ISTRANSFER}dial,${EXTEN},1)' . "\n\t";
248
        // Проверим, существует ли такой пир.
249
250
        $conf .= 'same => n,ExecIf($["${PJSIP_ENDPOINT(${EXTEN},auth)}x" == "x"]?Goto(internal-num-undefined,${EXTEN},1))' . " \n\t";
251
        $conf .= 'same => n,ExecIf($["${DEVICE_STATE(' . $technology . '/${EXTEN})}" == "BUSY"]?Set(DIALSTATUS=BUSY))' . " \n\t";
252
        $conf .= 'same => n,GotoIf($["${DEVICE_STATE(' . $technology . '/${EXTEN})}" == "BUSY"]?fw_start)' . " \n\t";
253
254
        // Как долго звонить пиру.
255
        $conf .= 'same => n,Set(ringlength=${DB(FW_TIME/${EXTEN})})' . " \n\t";
256
        $conf .= 'same => n,ExecIf($["${ringlength}x" == "x" || "${QUEUE_SRC_CHAN}x" != "x"]?Set(ringlength=600))' . " \n\t";
257
258
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1) ' . " \n\t";
259
        // Совершаем вызов пира.
260
        $conf .= 'same => n,Set(DST_CONTACT=${PJSIP_DIAL_CONTACTS(${EXTEN})})' . " \n\t";
261
        $conf .= 'same => n,ExecIf($["${FIELDQTY(DST_CONTACT,&)}" != "1"]?Set(__PT1C_SIP_HEADER=${EMPTY_VAR}))' . " \n\t";
262
        $conf .= 'same => n,ExecIf($["${DST_CONTACT}x" != "x"]?Dial(${DST_CONTACT},${ringlength},cTtekKHhU(${ISTRANSFER}dial_answer)b(dial_create_chan,s,1)):Set(DIALSTATUS=CHANUNAVAIL))' . " \n\t";
263
        $conf .= 'same => n(fw_start),NoOp(dial_hangup)' . " \n\t";
264
265
        // QUEUE_SRC_CHAN - установлена, если вызов сервершен агенту очереди.
266
        // Проверяем нужна ли переадресация
267
        $expression = '$["${DIALSTATUS}" != "ANSWER" && "${QUEUE_SRC_CHAN}x" == "x"]';
268
        $conf       .= 'same => n,ExecIf(' . $expression . '?Goto(internal-fw,${EXTEN},1))' . " \n\t";
269
        $conf       .= 'same => n,ExecIf($["${BLINDTRANSFER}x" != "x"]?AGI(check_redirect.php,${BLINDTRANSFER}))' . " \n\t";
270
        $conf       .= 'same => n,Hangup()' . "\n\n";
271
272
        $conf .= 'exten => h,1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\n";
273
    }
274
275
    /**
276
     * Генератор контекста для переадресаций.
277
     *
278
     * @param $conf
279
     */
280
    private function generateInternalTransfer(&$conf): void
281
    {
282
        $additionalModules = $this->di->getShared('pbxConfModules');
283
        $conf              .= "[internal-transfer] \n";
284
285
        foreach ($additionalModules as $appClass) {
286
            $addition= $appClass->getIncludeInternalTransfer();
287
            if (!empty($addition)){
288
                $conf .=$appClass->confBlockWithComments($addition);
289
            }
290
        }
291
292
        foreach ($additionalModules as $appClass) {
293
            $addition= $appClass->extensionGenInternalTransfer();
294
            if (!empty($addition)){
295
                $conf .=$appClass->confBlockWithComments($addition);
296
            }
297
        }
298
        $conf .= 'exten => h,1,Gosub(transfer_dial_hangup,${EXTEN},1)' . "\n\n";
299
    }
300
301
    /**
302
     * Генератор хинтов SIP.
303
     *
304
     * @param $conf
305
     */
306
    private function generateSipHints(&$conf): void
307
    {
308
        $additionalModules = $this->di->getShared('pbxConfModules');
309
        $conf              .= "[internal-hints] \n";
310
        foreach ($additionalModules as $appClass) {
311
            $addition = $appClass->extensionGenHints();
312
            if (!empty($addition)){
313
                $conf .=$appClass->confBlockWithComments($addition);
314
            }
315
        }
316
        $conf .= "\n\n";
317
    }
318
319
    /**
320
     * Генератор исходящих контекстов.
321
     *
322
     * @param $conf
323
     */
324
    private function generateOutContextPeers(&$conf): void
325
    {
326
        $additionalModules = $this->di->getShared('pbxConfModules');
327
        $conf              .= "[outgoing] \n";
328
329
        $conf .= 'exten => _+.!,1,NoOp(Strip + sign from number and convert it to +)' . " \n\t";
330
        $conf .= 'same => n,Set(ADDPLUS=+);' . " \n\t";
331
        $conf .= 'same => n,Goto(${CONTEXT},${EXTEN:1},1);' . " \n\n";
332
        $conf .= 'exten => _X!,1,NoOp(Start outgoing calling...)' . " \n\t";
333
        $conf .= 'same => n,Ringing()' . " \n\t";
334
335
        // Описываем возможность прыжка в пользовательский sub контекст.
336
        $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)' . "\n\t";
337
338
        /** @var \MikoPBX\Common\Models\OutgoingRoutingTable $routs */
339
        /** @var \MikoPBX\Common\Models\OutgoingRoutingTable $rout */
340
        $routs             = OutgoingRoutingTable::find(['order' => 'priority']);
341
        $provider_contexts = [];
342
343
        foreach ($routs as $rout) {
344
            $technology = $this->getTechByID($rout->providerid);
345
            if ($technology !== '') {
346
                    $rout_data                       = $rout->toArray();
347
                    $rout_data['technology']         = $technology;
348
                    $id_dialplan                     = $rout_data['providerid'] . '-' . $rout_data['id'] . '-outgoing';
349
                    $provider_contexts[$id_dialplan] = $rout_data;
350
                    $conf                            .= $this->generateOutgoingRegexPattern($rout_data);
351
                    continue;
352
            }
353
        }
354
        $conf .= 'same => n,ExecIf($["${peer_mobile}x" != "x"]?Hangup())' . " \n\t";
355
        $conf .= 'same => n,ExecIf($["${DIALSTATUS}" != "ANSWER" && "${BLINDTRANSFER}x" != "x" && "${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\t";
356
        $conf .= 'same => n,ExecIf($["${BLINDTRANSFER}x" != "x"]?AGI(check_redirect.php,${BLINDTRANSFER}))' . " \n\t";
357
        $conf .= 'same => n,ExecIf($["${ROUTFOUND}x" == "x"]?Gosub(dial,${EXTEN},1))' . "\n\t";
358
359
        $conf .= 'same => n,Playback(silence/2,noanswer)' . " \n\t";
360
        $conf .= 'same => n,ExecIf($["${ROUTFOUND}x" != "x"]?Playback(followme/sorry,noanswer):Playback(cannot-complete-as-dialed,noanswer))' . " \n\t";
361
        $conf .= 'same => n,Hangup()' . " \n\n";
362
        $conf .= 'exten => h,1,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\t";
363
364
        foreach ($provider_contexts as $id_dialplan => $rout) {
365
            $conf .= "\n[{$id_dialplan}]\n";
366
            if (isset($rout['trimfrombegin']) && $rout['trimfrombegin'] > 0) {
367
                // $exten_var = '${ADDPLUS}${EXTEN:'.$rout['trimfrombegin'].'}';
368
                $exten_var    = '${EXTEN:' . $rout['trimfrombegin'] . '}';
369
                $change_exten = 'same => n,ExecIf($["${EXTEN}" != "${number}"]?Goto(${CONTEXT},${number},$[${PRIORITY} + 1]))' . "\n\t";
370
            } else {
371
                $exten_var    = '${ADDPLUS}${EXTEN}';
372
                $change_exten = '';
373
            }
374
            $conf .= 'exten => _X!,1,Set(number=' . $rout['prepend'] . $exten_var . ')' . "\n\t";
375
            $conf .= $change_exten;
376
            foreach ($additionalModules as $appClass) {
377
                $addition = $appClass->generateOutRoutContext($rout);
378
                if (!empty($addition)){
379
                    $conf .=$appClass->confBlockWithComments($addition);
380
                }
381
            }
382
            $conf .= 'same => n,ExecIf($["${number}x" == "x"]?Hangup())' . "\n\t";
383
            $conf .= 'same => n,Set(ROUTFOUND=1)' . "\n\t";
384
            $conf .= 'same => n,Gosub(${ISTRANSFER}dial,${EXTEN},1)' . "\n\t";
385
386
            $conf .= 'same => n,ExecIf($["${EXTERNALPHONE}" == "${EXTEN}"]?Set(DOPTIONS=tk))' . "\n\t";
387
388
            // Описываем возможность прыжка в пользовательский sub контекст.
389
            $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(' . $rout['providerid'] . '-outgoing-custom,${EXTEN},1)}" == "1"]?' . $rout['providerid'] . '-outgoing-custom,${EXTEN},1)' . "\n\t";
390
391
            if ($rout['technology'] === IAXConf::TYPE_IAX2) {
392
                $conf .= 'same => n,Dial(' . $rout['technology'] . '/' . $rout['providerid'] . '/${number},600,${DOPTIONS}TKU(dial_answer)b(dial_create_chan,s,1))' . "\n\t";
393
            } else {
394
                $conf .= 'same => n,Dial(' . $rout['technology'] . '/${number}@' . $rout['providerid'] . ',600,${DOPTIONS}TKU(dial_answer)b(dial_create_chan,s,1))' . "\n\t";
395
            }
396
            foreach ($additionalModules as $appClass) {
397
                $addition = $appClass->generateOutRoutAfterDialContext($rout);
398
                if (!empty($addition)){
399
                    $conf .=$appClass->confBlockWithComments($addition);
400
                }
401
            }
402
            $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(' . $rout['providerid'] . '-outgoing-after-dial-custom,${EXTEN}),1}" == "1"]?' . $rout['providerid'] . '-outgoing-after-dial-custom,${EXTEN},1)' . "\n\t";
403
404
            $conf .= 'same => n,ExecIf($["${ISTRANSFER}x" != "x"]?Gosub(${ISTRANSFER}dial_hangup,${EXTEN},1))' . "\n\t";
405
            $conf .= 'same => n,ExecIf($["${DIALSTATUS}" = "ANSWER"]?Hangup())' . "\n\t";
406
            $conf .= 'same => n,Set(pt1c_UNIQUEID=${EMPTY_VALUE})' . "\n\t";
407
            $conf .= 'same => n,return' . "\n";
408
        }
409
    }
410
411
    /**
412
     * Генератор исходящего маршрута.
413
     *
414
     * @param $rout
415
     *
416
     * @return string
417
     */
418
    private function generateOutgoingRegexPattern($rout): string
419
    {
420
        $conf        = '';
421
        $restnumbers = '';
422
        if (isset($rout['restnumbers']) && $rout['restnumbers'] > 0) {
423
            $restnumbers = "[0-9]{" . $rout['restnumbers'] . "}$";
424
        } elseif ($rout['restnumbers'] == 0) {
425
            $restnumbers = "$";
426
        } elseif ($rout['restnumbers'] == -1) {
427
            $restnumbers = "";
428
        }
429
        $numberbeginswith = $rout['numberbeginswith'];
430
        $conf             .= 'same => n,ExecIf($["${REGEX("^' . $numberbeginswith . $restnumbers . '" ${EXTEN})}" == "1"]?Gosub(' . $rout['providerid'] . '-' . $rout['id'] . '-outgoing,${EXTEN},1))' . " \n\t";
431
432
        return $conf;
433
    }
434
435
    /**
436
     * Контекст для входящих внешних звонков без авторизации.
437
     *
438
     * @param $conf
439
     */
440
    public function generatePublicContext(&$conf): void
441
    {
442
        $additionalModules = $this->di->getShared('pbxConfModules');
443
        $conf              .= "\n";
444
        $conf              .= self::generateIncomingContextPeers('none', '', '');
445
        $conf              .= "[public-direct-dial] \n";
446
        foreach ($additionalModules as $appClass) {
447
            if ($appClass instanceof $this){
448
                continue;
449
            }
450
            $appClass->generatePublicContext($conf);
451
        }
452
        $filter = ["provider IS NULL AND priority<>9999"];
453
454
        /**
455
         * @var array
456
         */
457
        $m_data = IncomingRoutingTable::find($filter);
458
        if (count($m_data->toArray()) > 0) {
459
            $conf .= 'include => none-incoming';
460
        }
461
    }
462
463
    /**
464
     * Генератор входящих контекстов.
465
     *
466
     * @param string | array $provider
467
     * @param string | array $login
468
     * @param string         $uniqid
469
     *
470
     * @return string
471
     */
472
    public static function generateIncomingContextPeers($provider, $login = '', $uniqid = ''): string
473
    {
474
        $conf              = '';
475
        $dialplan          = [];
476
        $di = Di::getDefault();
477
        if ($di===null){
478
            return '';
479
        }
480
        $additionalModules = $di->getShared('pbxConfModules');
481
482
        if ('none' === $provider) {
483
            // Звонки по sip uri.
484
            $filter = [
485
                'provider IS NULL AND priority<>9999',
486
                'order' => 'provider,priority,extension',
487
            ];
488
        } elseif (is_array($provider)) {
489
            $filter = [
490
                'provider IN ({provider:array})',
491
                'bind'  => [
492
                    'provider' => array_keys($provider),
493
                ],
494
                'order' => 'provider,priority,extension',
495
            ];
496
        } else {
497
            // Звонки через провайдера.
498
            $filter = [
499
                "provider = '$provider'",
500
                'order' => 'provider,priority,extension',
501
            ];
502
        }
503
        /** @var \MikoPBX\Common\Models\IncomingRoutingTable $default_action */
504
        $default_action = IncomingRoutingTable::findFirst('priority = 9999');
505
        /** @var \MikoPBX\Common\Models\IncomingRoutingTable $m_data */
506
        $m_data = IncomingRoutingTable::find($filter);
507
        $data   = $m_data->toArray();
508
509
        $need_def_rout = true;
510
        foreach ($data as $rout) {
511
            $number = trim($rout['number']);
512
            if ($number === 'X!' || $number === '') {
513
                $need_def_rout = false;
514
                break;
515
            }
516
        }
517
        if ($need_def_rout === true && 'none' !== $provider) {
518
            $data[] = ['number' => '', 'extension' => '', 'timeout' => ''];
519
        }
520
        $config = new MikoPBXConfig();
521
        $lang   = str_replace('_', '-', $config->getGeneralSettings('PBXLanguage'));
522
523
        $rout_data_dial = [];
524
        foreach ($data as $rout) {
525
            $number      = trim($rout['number']);
526
            $timeout     = trim($rout['timeout']);
527
            $rout_number = ($number === '') ? 'X!' : $number;
528
            $rout_data   = &$dialplan[$rout_number];
529
            if (empty($rout_data)) {
530
                $ext_prefix = ('none' === $provider) ? '' : '_';
531
                $rout_data  .= "exten => {$ext_prefix}{$rout_number},1,NoOp(--- Incoming call ---)\n\t";
532
                $rout_data  .= 'same => n,Set(CHANNEL(language)=' . $lang . ')' . "\n\t";
533
                $rout_data  .= 'same => n,Set(CHANNEL(hangup_handler_wipe)=hangup_handler,s,1)' . "\n\t";
534
                $rout_data  .= 'same => n,Set(__FROM_DID=${EXTEN})' . "\n\t";
535
                $rout_data  .= 'same => n,Set(__FROM_CHAN=${CHANNEL})' . "\n\t";
536
537
                // Установка имени пира.
538
                $rout_data .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" != "Local"]?Gosub(set_from_peer,s,1))' . "\n\t";
539
                $rout_data .= 'same => n,ExecIf($["${CHANNEL(channeltype)}" == "Local"]?Set(__FROM_PEER=${CALLERID(num)}))' . "\n\t";
540
541
                // TODO / Подмена входящего callerid.
542
                $rout_data .= 'same => n,Gosub(add-trim-prefix-clid,${EXTEN},1)' . "\n\t";
543
544
                foreach ($additionalModules as $appClass) {
545
                     $addition = $appClass->generateIncomingRoutBeforeDial($rout_number);
546
                     if (!empty($addition)){
547
                         $rout_data .=$appClass->confBlockWithComments($addition);
548
                     }
549
                 }
550
551
                // Перехват на ответственного.
552
                $rout_data .= 'same => n,UserEvent(Interception,CALLERID: ${CALLERID(num)},chan1c: ${CHANNEL},FROM_DID: ${FROM_DID})' . "\n\t";
553
                // Проверим распискние для входящих внешних звонков.
554
                $rout_data .= 'same => n,Gosub(check-out-work-time,${EXTEN},1)';
555
                // Описываем возможность прыжка в пользовательский sub контекст.
556
                $rout_data .= " \n\t" . 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-custom,${EXTEN},1)';
557
            }
558
559
            if ( ! empty($rout['extension'])) {
560
                $rout_data = rtrim($rout_data);
561
                // Обязательно проверяем "DIALSTATUS", в случае с парковой через AMI вызова это необходимо.
562
                // При ответе может отработать следующий приоритет.
563
                if ( ! isset($rout_data_dial[$rout_number])) {
564
                    $rout_data_dial[$rout_number] = '';
565
                }
566
567
                $dial_command                 = " \n\t" . 'same => n,' . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Dial(Local/{$rout['extension']}@internal-incoming/n,{$timeout},cTKg));";
568
                $rout_data_dial[$rout_number] .= " \n\t" . "same => n,Set(M_TIMEOUT={$timeout})";
569
                $rout_data_dial[$rout_number] .= $dial_command;
570
571
                if (is_array($provider)) {
572
                    $key = $provider[$rout['provider']] ?? '';
573
                    if ( ! isset($rout_data_dial[$key])) {
574
                        $rout_data_dial[$key] = '';
575
                    }
576
                    if (empty($number)) {
577
                        $rout_data_dial[$key] .= $dial_command;
578
                    }
579
                }
580
            }
581
        }
582
583
        if (is_string($login)) {
584
            $add_login_pattern = ! empty($login);
585
            foreach ($data as $rout) {
586
                if ( ! $add_login_pattern) {
587
                    break;
588
                } // Логин не заполнен, обработка не требуется.
589
                $is_num = preg_match_all('/^\d+$/m', $login, $matches, PREG_SET_ORDER, 0);
590
                if ($is_num === 1) {
591
                    // Это числовой номер, потому, не требуется дополнительно описывать exten.
592
                    $add_login_pattern = false;
593
                    break;
594
                }
595
                if (trim($rout['number']) !== $login) {
596
                    // Совпадение exten не найдено. Идем дальше.
597
                    continue;
598
                }
599
                // Совпадение найдено, не требуется дополнительно описывать exten.
600
                $add_login_pattern = false;
601
                break;
602
            }
603
            if ($add_login_pattern && array_key_exists('X!', $rout_data_dial) && isset($dialplan['X!'])) {
604
                $dialplan[$login]       = str_replace('_X!,1', "{$login},1", $dialplan['X!']);
605
                $rout_data_dial[$login] = $rout_data_dial['X!'];
606
            }elseif($add_login_pattern === true && $need_def_rout === true && count($data) === 1){
607
                // Только маршрут "По умолчанию".
608
                $dialplan[$login]       = str_replace('_X!,1', "{$login},1", $dialplan['X!']);
609
            }
610
        } elseif (is_array($provider)) {
611
            foreach (array_values($provider) as $_login) {
612
                   $dialplan[$_login] = str_replace('_X!,1', "{$_login},1", $dialplan['X!']);
613
            }
614
        }
615
616
        foreach ($dialplan as $key => &$dpln) {
617
            if ( ! array_key_exists($key, $rout_data_dial)) {
618
                continue;
619
            }
620
            $dpln = rtrim($dpln);
621
            $dpln .= $rout_data_dial[$key];
622
        }
623
        unset($dpln);
624
625
        $uniqid = is_string($provider) ? $provider : $uniqid;
626
        $conf   .= "\n" . "[{$uniqid}-incoming]\n";
627
        foreach ($dialplan as $dpln) {
628
            $conf .= $dpln . "\n";
629
            if (null == $default_action && 'none' !== $provider) {
630
                continue;
631
            }
632
            if ('extension' === $default_action->action) {
633
                // Обязательно проверяем "DIALSTATUS", в случае с парковой через AMI вызова это необходимо.
634
                // При ответе может отработать следующий приоритет.
635
                $conf .= "\t" . 'same => n,Set(M_TIMEOUT=0)' . "\n";
636
                $conf .= "\t" . "same => n," . 'ExecIf($["${M_DIALSTATUS}" != "ANSWER"]?' . "Dial(Local/{$default_action->extension}@internal/n,,cTKg)); default action" . "\n";
637
638
                foreach ($additionalModules as $appClass) {
639
                    $addition = $appClass->generateIncomingRoutAfterDialContext($uniqid);
640
                    if (!empty($addition)){
641
                         $conf .=$appClass->confBlockWithComments($addition);
642
                    }
643
                }
644
                $conf .= " \t" . 'same => n,GosubIf($["${DIALPLAN_EXISTS(${CONTEXT}-after-dial-custom,${EXTEN},1)}" == "1"]?${CONTEXT}-after-dial-custom,${EXTEN},1)' . "\n";
645
            } elseif ('busy' === $default_action->action) {
646
                $conf .= "\t" . "same => n,Busy()" . "\n";
647
            }
648
            $conf .= "\t" . "same => n,Hangup()" . "\n";
649
        }
650
651
        return $conf;
652
    }
653
654
    /**
655
     * Описываем нерабочее время.
656
     *
657
     * @param $conf
658
     *
659
     * @return string
660
     */
661
    private function generateOutWorkTimes(&$conf): string
662
    {
663
        $conf .= "\n\n[playback-exit]\n";
664
        $conf .= 'exten => _.!,1,NoOp(check time)' . "\n\t";
665
        $conf .= 'same => n,Gosub(dial_outworktimes,${EXTEN},1)' . "\n\t";
666
        $conf .= 'same => n,Playback(${filename})' . "\n\t";
667
        $conf .= 'same => n,Hangup()' . "\n\n";
668
669
        $conf .= "[check-out-work-time]\n";
670
        $conf .= "exten => _.!,1,NoOp(check time)\n\t";
671
672
        $data = OutWorkTimes::find(['order' => 'date_from']);
673
674
        $conf_out_set_var = '';
675
676
        $now_year = 1 * date('Y', time());
677
        foreach ($data as $out_data) {
678
            /** @var \MikoPBX\Common\Models\OutWorkTimes $out_data */
679
            if ( ! empty($out_data->date_from) && ! empty($out_data->date_to)) {
680
                $year_from = 1 * date('Y', (int)$out_data->date_from);
681
                $year_to   = 1 * date('Y', (int)$out_data->date_to);
682
                if ($now_year < $year_from || $now_year > $year_to) {
683
                    // Правило не актуально для текущего года.
684
                    continue;
685
                }
686
            }
687
            $time_from = $out_data->time_from;
688
            $time_to   = $out_data->time_to;
689
            if (empty($time_from) && empty($time_to)) {
690
                $times = '*';
691
            } else {
692
                $time_to = (empty($time_to)) ? '23:59' : $time_to;
693
                $time_to = (strlen($time_to) === 4) ? "0{$time_to}" : $time_to;
694
695
                $time_from = (empty($time_from)) ? '00:00' : $time_from;
696
                $time_from = (strlen($time_from) === 4) ? "0{$time_from}" : $time_from;
697
698
                $times = "{$time_from}-{$time_to}";
699
            }
700
701
            $weekday_from = (string) $out_data->weekday_from;
702
            $weekday_to   = (string) $out_data->weekday_to;
703
            $arr_weekday  = [null, "mon", "tue", "wed", "thu", "fri", "sat", "sun"];
704
            if (empty($weekday_from) && empty($weekday_to)) {
705
                $weekdays = '*';
706
            } else {
707
                $weekday_from = (empty($weekday_from)) ? '1' : $weekday_from;
708
                $weekday_to   = (empty($weekday_to)) ? '7' : $weekday_to;
709
                $weekdays     = "{$arr_weekday[$weekday_from]}-{$arr_weekday[$weekday_to]}";
710
            }
711
712
            $date_from = $out_data->date_from;
713
            $date_to   = $out_data->date_to;
714
            if (empty($date_from)) {
715
                $mdays  = '*';
716
                $months = '*';
717
            } else {
718
                $mdays  = strtolower(date("j", (int)$date_from));
719
                $months = strtolower(date("M", (int)$date_from));
720
                if ( ! empty($date_to)) {
721
                    $mdays  .= "-" . strtolower(date("j", (int)$date_to));
722
                    $months .= "-" . strtolower(date("M", (int)$date_to));
723
                }
724
            }
725
726
727
            if ('extension' === $out_data->action) {
728
                $appname = 'GotoIfTime';
729
                $appdata = "internal,{$out_data->extension},1";
730
            } else {
731
                /** @var \MikoPBX\Common\Models\SoundFiles $res */
732
                $res           = SoundFiles::findFirst($out_data->audio_message_id);
733
                $audio_message = ($res === null) ? '' : Util::trimExtensionForFile($res->path);
734
735
                $conf_out_set_var .= "[work-time-set-var-{$out_data->id}]\n" .
736
                    'exten => _.!,1,Set(filename=' . $audio_message . ')' . "\n\t" .
737
                    'same => n,Goto(playback-exit,${EXTEN},1)' . "\n\n";
738
739
                $appname = 'ExecIfTime';
740
                $appdata = 'Goto(work-time-set-var-' . $out_data->id . ',${EXTEN},1)';
741
            }
742
            $conf .= "same => n,{$appname}($times,$weekdays,$mdays,$months?{$appdata})\n\t";
743
        }
744
745
        $conf .= "same => n,return\n\n";
746
747
        $conf .= $conf_out_set_var;
748
749
        return $conf;
750
    }
751
752
    /**
753
     * Генератор extension для контекста outgoing.
754
     *
755
     * @param string $uniqueID
756
     *
757
     * @return null|string
758
     */
759
    public function getTechByID(string $uniqueID): string
760
    {
761
        $technology = '';
762
        $provider   = Providers::findFirstByUniqid($uniqueID);
763
        if ($provider !== null) {
764
            if ($provider->type === 'SIP') {
765
                $account    = Sip::findFirst('disabled="0" AND uniqid = "'.$uniqueID.'"');
766
                $technology = ($account === null)?'':SIPConf::getTechnology();
767
            } elseif ($provider->type === 'IAX') {
768
                $account    = Iax::findFirst('disabled="0" AND uniqid = "'.$uniqueID.'"');
769
                $technology = ($account === null)?'':'IAX2';
770
            }
771
        }
772
773
        return $technology;
774
    }
775
}