├── .gitmodules ├── LICENSE ├── out.php ├── telnyx.php ├── dialplan └── extensions_custom.conf └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "php-sip"] 2 | path = php-sip 3 | url = https://github.com/simontelephonics/php-sip 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bill Simon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /out.php: -------------------------------------------------------------------------------- 1 | "+1" . $_GET["from"], 24 | "to" => "+1" . $_GET["to"], 25 | "text" => $postdata 26 | )); 27 | $httpopts = array('http' => 28 | array( 29 | 'method' => 'POST', 30 | 'header' => array( 31 | 'Content-type: application/json', 32 | 'Accept: application/json', 33 | 'Authorization: Bearer '. $telnyxKey 34 | ), 35 | 'timeout' => '3', 36 | 'content' => $telnyxBody 37 | )); 38 | $httpcontext = stream_context_create($httpopts); 39 | $httpresult = file_get_contents($telnyxUrl, false, $httpcontext); 40 | 41 | // Respond to Asterisk 42 | echo $httpresult; 43 | 44 | ?> 45 | -------------------------------------------------------------------------------- /telnyx.php: -------------------------------------------------------------------------------- 1 | $v) { 27 | if (preg_match("/\+1([2-9][0-9][0-9][2-9][0-9]{6})/", $v['phone_number'] , $matches)) { 28 | $to = $matches[1]; 29 | $output = shell_exec('/usr/sbin/asterisk -rx "database showkey accountcode"'); 30 | $count = preg_match_all("#AMPUSER/([0-9]+)/accountcode.*: $to\s*$#m", $output, $exts); 31 | if ($count) { 32 | require_once('php-sip/PhpSIP.class.php'); 33 | $smsout = new PhpSIP('127.0.0.1', '5099'); 34 | foreach ($exts[1] as $ext) { 35 | $smsout->newCall(); 36 | $smsout->setMethod('MESSAGE'); 37 | $smsout->setFrom('sip:' . $payload['from']['phone_number'] . '@127.0.0.1'); 38 | $smsout->setContentType('text/plain; charset=UTF-8'); 39 | $smsout->setBody($payload['text']); 40 | $smsout->setUri('sip:' . $ext . '@127.0.0.1:' . $localUdpPort); 41 | $res = $smsout->send(); 42 | } 43 | } else { 44 | $fh = fopen($logfile, "a"); 45 | fwrite($fh, date($payload['received_at']) . " - Nowhere to send " . $payload['id'] . "\n\n"); 46 | fclose($fh); 47 | } 48 | } 49 | } 50 | ?> 51 | -------------------------------------------------------------------------------- /dialplan/extensions_custom.conf: -------------------------------------------------------------------------------- 1 | [messages-in] 2 | ; Deliver to local 4-digit extension. If you use 3, 5 or other length extensions, adjust accordingly. 3 | exten => _XXXX,1,Set(FROMUSER=${CUT(MESSAGE(from),<,2)}) 4 | same => n,Set(FROMUSER=${CUT(FROMUSER,@,1)}) 5 | same => n,Set(FROMUSER=${CUT(FROMUSER,:,2)}) 6 | same => n,Set(DID=${DB(AMPUSER/${EXTEN}/accountcode)}) 7 | same => n,Macro(get-vmcontext,${AMPUSER}) 8 | same => n,Set(EMAIL=${VM_INFO(${EXTEN}@${VMCONTEXT},email)}) 9 | same => n,Set(TODEVICE=${DB(DEVICE/${EXTEN}/dial)}) 10 | same => n,Set(TODEVICE=${TOLOWER(${STRREPLACE(TODEVICE,"/",":")})}) 11 | same => n,MessageSend(${TODEVICE},${FROMUSER}) 12 | same => n,ExecIf($["${MESSAGE_SEND_STATUS}" == "FAILURE"]?Goto(mail-${EXTEN},1)) 13 | same => n,Hangup() 14 | 15 | ; This could be improved. Any undeliverable SMS just gets sent to a catch-all email address. You 16 | ; could look up the extension user's email and send the message to their specific address instead. 17 | exten => _mail-X.,1,NoOp(Sending mail) 18 | same => n,System(echo "Text message from ${MESSAGE(from)} to ${EXTEN:5} - ${MESSAGE(body)}" | mail -s "New text to ${DID} received while offline" ${EMAIL}) 19 | same => n,Hangup() 20 | 21 | [messages-out] 22 | ; This is a local 4-digit extension so we just want to send it internally 23 | exten => _XXXX,1,Goto(messages-in,${EXTEN},1) 24 | ; Deliver to PSTN - adjust pattern to match your needs 25 | ; These are normalized so that we are working with 10-digit US/CAN numbers and then reformatted 26 | ; to +1 E164 in the outbound script. Rework it according to your preferences. 27 | exten => _+1NXXNXXXXXX,1,Goto(messages-out,${EXTEN:2},1) 28 | exten => _1NXXNXXXXXX,1,Goto(messages-out,${EXTEN:1},1) 29 | exten => _NXXNXXXXXX,1,NoOp(Sending SMS to ${EXTEN} from ${MESSAGE(from)}) 30 | same => n,Set(FROMUSER=${CUT(MESSAGE(from),<,2)}) 31 | same => n,Set(FROMUSER=${CUT(FROMUSER,@,1)}) 32 | same => n,Set(FROMUSER=${CUT(FROMUSER,:,2)}) 33 | same => n,Set(CALLERID(num)=${FROMUSER}) 34 | same => n,Set(SMSCID=${DB(AMPUSER/${CALLERID(num)}/accountcode)}) 35 | same => n,ExecIf($["foo${SMSCID}" == "foo"]?Goto(messages-out,nocid,1):Set(FROM=${SMSCID})) 36 | same => n,NoOp(Using external caller ID of ${FROM}) 37 | same => n,Set(CURLOPT(conntimeout)=4) 38 | same => n,Set(CURLOPT(httptimeout)=4) 39 | same => n,NoOp(${CURL(https://yourpbx/path/to/out.php?to=${EXTEN}&from=${FROM},${MESSAGE(body)})}) 40 | same => n,Hangup() 41 | ; 42 | exten => nocid,1,Set(MESSAGE(body)=Cannot send SMS. Extension must have valid SMS CID set in the accountcode field.) 43 | same => n,MessageSend(pjsip:${CALLERID(num)},${MESSAGE(from)}) 44 | same => n,Hangup() 45 | ; 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freepbx-telnyx-sms 2 | Scripts for sending and receiving SMS between FreePBX and Telnyx 3 | 4 | ## Installation 5 | 6 | Add the dialplan blocks to `/etc/asterisk/extensions_custom.conf`, adjusting them according to your environment: 7 | * extension length 8 | * catch-all e-mail to receive undeliverable texts 9 | * number normalization 10 | 11 | Make a directory under (wwwroot) called `sms` and ensure it can be reached by HTTPS from outside. 12 | 13 | Place the `out.php` and `telnyx.php` scripts into `sms` and adjust them according to your environment. 14 | 15 | out.php: 16 | * add your Telnyx APIv2 API key 17 | 18 | telnyx.php: 19 | * note the instructions in the comments at the top of the file 20 | 21 | Place the included `php-sip` library in a directory of that name under `sms`. 22 | 23 | ## FreePBX configuration 24 | 25 | ### Trunk 26 | Set up a PJSIP trunk for 127.0.0.1 as follows: 27 | 28 | ![PJSIP trunk](https://user-images.githubusercontent.com/5303782/105723214-85843f80-5ef4-11eb-94e1-6e38e35e448b.png) 29 | 30 | Set the Message Context: 31 | 32 | ![PJSIP advanced tab](https://user-images.githubusercontent.com/5303782/105723266-96cd4c00-5ef4-11eb-856c-a9640a2f7a1e.png) 33 | 34 | ... 35 | 36 | ![PJSIP message context field](https://user-images.githubusercontent.com/5303782/105723305-a0ef4a80-5ef4-11eb-82ba-1be9766a9e9e.png) 37 | 38 | ### Extensions 39 | For each extension that will participate in SMS, set the Account Code to the normalized DID this extension will send and receive as, and set the Message Context: 40 | 41 | ![Extension settings](https://user-images.githubusercontent.com/5303782/105723337-ab114900-5ef4-11eb-99d0-333328a07479.png) 42 | 43 | ... 44 | 45 | ![Extension account code](https://user-images.githubusercontent.com/5303782/105723364-b2385700-5ef4-11eb-9332-f533f1317dc7.png) 46 | 47 | ... 48 | 49 | ![Extension message context](https://user-images.githubusercontent.com/5303782/105723387-b8c6ce80-5ef4-11eb-887c-34201324a265.png) 50 | 51 | 52 | ## Telnyx configuration 53 | 54 | Set up a Messaging Profile (APIv2) in Telnyx: 55 | 56 | ![Telnyx messaging profile](https://user-images.githubusercontent.com/5303782/224144132-896646c3-c875-4918-843b-aa05344f1edc.png) 57 | 58 | Specify the path to the `telnyx.php` script in Inbound Settings: 59 | 60 | ![Messaging profile inbound settings](https://user-images.githubusercontent.com/5303782/105724385-cf215a00-5ef5-11eb-8b28-0cfd1feaa182.png) 61 | 62 | Copy the Profile Secret from the Outbound Settings section and use it as the token in your `out.php` script. 63 | 64 | Save this profile and assign it to the DIDs you want to enable for SMS. 65 | 66 | ## Usage 67 | 68 | SMS from Telnyx will be delivered to the `telnyx.php` script specifying a DID. Any extension whose Account Code has that DID will receive the SMS. 69 | 70 | SMS from extensions will be sent to Telnyx using the caller ID in the extension's Account Code field. You can only send from numbers that are on your account. 71 | 72 | Extensions can text among themselves through the Asterisk dialplan without engaging Telnyx. 73 | 74 | These scripts are normalized for US/CAN 10-digit DIDs and would need to be adjusted for international SMS or handling of short codes. 75 | --------------------------------------------------------------------------------