simontelephonics / smsconnector

SMS Connector module for FreePBX 16 and 17
https://simon.tel/open-source/
GNU General Public License v3.0
43 stars 16 forks source link

Add provider DIDWW #30

Open ajtel opened 1 year ago

ajtel commented 1 year ago

Can you add DIDWW?

lgnch commented 1 week ago

Disclaimer : THIS WILL NOT WORK PROPERLY YET IT IS A ROUGH DRAFT DO NOT USE EXCEPT FOR DEVELOPMENT

I just put it together today and much of the to and from fields still needs properly coding from the template and it has not yet been tested. The aim is to get the community started and then correct as we go. DIDWW needs Base64 in its message content but they change it from in to outbound so this needs testing which is why it is not in a pull request as I don't want it accidentally combined.

<?php
namespace FreePBX\modules\Smsconnector\Provider;

class DIDWW extends providerBase
{
    public function __construct()
    {
        parent::__construct();
        $this->name     = _('DIDWW');
        $this->nameRaw  = 'didww';
        $this->APIUrlInfo = 'https://doc.didww.com/sms/sms-trunks/technical-data/http-specification.html';
        $this->APIVersion = 'v1.0';

        $this->configInfo = array(
            'trunk_url' => array(
                'type'        => 'string',
                'label'       => _('HTTP Trunk URL'),
                'help'        => _("Enter the DIDWW HTTP Trunk URL for sending SMS."),
                'default'     => 'https://us.sms-out.didww.com/outbound_messages',
                'required'    => true,
                'placeholder' => _('Enter the DIDWW HTTP Trunk URL'),
            ),
            'api_key' => array(
                'type'        => 'string',
                'label'       => _('Username'),
                'help'        => _("Enter the DIDWW Username"),
                'default'     => '',
                'required'    => true,
                'placeholder' => _('Enter Username'),
            ),
            'api_secret' => array(
                'type'        => 'string',
                'label'       => _('Password'),
                'help'        => _("Enter the Password"),
                'default'     => '',
                'required'    => true,
                'class'       => 'confidential',
                'placeholder' => _('Enter Password'),
            ),
        );
    }

        // parameters from DIDWW
        // Available variables for Path, Headers, and Request Body:
        // from https://doc.didww.com/sms/sms-trunks/technical-data/http-specification.html

        //
        // NOTE DIDWW changes parameters from inbound to outbound.
        //
        // Inbound Standard Headers
        // {SMS_UUID} - The ID of the received message.
        // {SMS_SRC_ADDR} - Source number of SMS sender. RFC 3986 encoded.
        // {SMS_DST_ADDR} - Destination number to which SMS is received. RFC 3986 encoded.
        // {SMS_TEXT} - The SMS Message body raw format.
        // {SMS_TEXT_BASE64_ENCODED} - The SMS Message body. Encrypted using Base 64 encoding.
        // {SMS_TIME} - Message receive date and time. RFC 1123 encoded.

        // Accepted Body types
        // RAW - the request body will be sent to customers system without changes.
        // JSON - the request body will be sent to customers system in JSON format.
        // HTML URL encoded - the request body will be sent to customers system in HTML URL Encoded format.
        // HTML Multipart - the request body will be sent to customers system in HTML Multipart format.

        // Outbound headers change from inbound headers as follows

        // [SMS_SRC_ADDR} changes to {source}
        // {SMS_DST_ADDR} chnages to {destination}
        // {SMS_TEXT} changes to {content}

        // Passing username and password is undertaken as  follows

        //      curl \
        //              -u username:password \
        //              -X POST \
        //              -H "Content-Type: application/vnd.api+json" \
        //              https://sms-out.didww.com/outbound_messages

        // Example curl string
        // curl -i -X POST https://sms-out.didww.com/outbound_messages -H "Content-Type: application/vnd.api+json" -H "Authorization: Basic" --data-raw '{"data": {"attributes": {"content": "Hello World!", "destination": "37041654321", "source": "37041123456"}, "type": "outbound_messages"}}'

        // Example Outbound Post
        // POST /outbound_messages HTTP/1.1
        //  Host: sms-out.didww.com
        // Content-Type: application/vnd.api+json
        // Authorization: Basic
                //{
                //    "data": {
                //    "type": "outbound_messages",
                //    "attributes": {
                //        "destination": "37041654321",
                //        "source": "37041123456",
                //        "content": "Hello World!"
                //        }
                //    }
                //}

        // Example callback request

        //      Notification payload is sent by using HTTP POST method:

        //      {
        //          "data": {
        //              "type": "outbound_message_callbacks",
        //              "id": "550e8400-e29b-41d4-a716-446655440000",
        //              "attributes": {
        //                  "time_start": "1997-07-16T19:20:30.45Z",
        //                  "end_time": "2001-07-16T19:20:30.45Z",
        //                  "destination": "37041654321",
        //                  "source": "37041123456",
        //                  "status": "Success",
        //                  "code_id": null,
        //                  "fragments_sent": 7,
        //                  "price": 0.001
        //              }
        //          }
        //      }

    public function sendMedia($id, $to, $from, $message='')
    {
        // We have to send media items as base64-encoded form fields

        $base64media = array();
        $sql = 'SELECT raw FROM sms_media WHERE mid = :mid';
        $stmt = $this->Database->prepare($sql);
        $stmt->bindParam(':mid', $id, \PDO::PARAM_INT);
        $stmt->execute();
        $media = $stmt->fetchAll(\PDO::FETCH_ASSOC);
        foreach ($media as $media_item)
        {
            $base64media[] = base64_encode($media_item['raw']);
        }

        $req = array(
            'source'         => $from,
            'destination'    => $to,
            'content'       => $message,
        );
        $this->sendDIDWWms($req, $from, $id);
        return true;
    }

    public function sendMessage($id, $to, $from, $message='')
    {
        $req = array(
            'to'     => $to,
            'text'   => $message,
        );
        if (strlen($message) <= 160)
        {
            $req['method'] = 'sendSMS';
        }
        else
        {
            $req['method'] = 'sendMMS';
        }
        $this->sendDIDWWms($req, $from, $id);
        return true;
    }

    private function sendDIDWWms($payload, $from, $mid): void
    {
        if ((strlen($from) == 11) && (strpos($from, '1') === 0))
        {
            $from = ltrim($from, '1');
        }
        if ((strlen($payload['to']) == 11) && (strpos($payload['to'], '1') === 0))
        {
            $payload['to'] = ltrim($payload['to'], '1');
        }
        $config = $this->getConfig($this->nameRaw);
        $fields = array(
            'api_username' => $config['api_key'],
            'api_password' => $config['api_secret'],
            'method'       => $payload['method'],
            'did'          => $from,
            'dst'          => $payload['to'],
        );
        if (! empty($payload['text']))
        {
            $fields['message'] = $payload['text'];
        }
        if (! empty($payload['media']))
        {
            $counter = 1;
            foreach ($payload['media'] as $media_item)
            {
                $fields['media'.$counter] = $media_item;
                $counter++;
            }
        }

        // build a multipart/form-data request
        $crlf = "\r\n";
        $mimeBoundary = '----' . md5(time());
        $reqbody = '';

        foreach ($fields as $key => $value)
        {
            $reqbody .= '--' . $mimeBoundary . $crlf;
            $reqbody .= 'Content-Disposition: form-data; name="' . $key . '"' . $crlf . $crlf;
            $reqbody .= $value . $crlf;
        }
        $reqbody .= '--' . $mimeBoundary . '--' . $crlf . $crlf;

        $headers = array("Content-Type" => "multipart/form-data; boundary=$mimeBoundary");
        $url = sprintf('https://sms-out.didww.com/outbound_messages', $this->APIVersion);

        $session = \FreePBX::Curl()->requests($url);
        try
        {
            $DIDWWmsResponse = $session->post('', $headers, $reqbody, array());
            freepbx_log(FPBX_LOG_INFO, sprintf(_("%s responds: HTTP %s, %s"), $this->nameRaw, $DIDWWmsResponse->status_code, $DIDWWmsResponse->body));
            if (! $DIDWWmsResponse->success)
            {
                throw new \Exception(sprintf(_("HTTP %s, %s"), $DIDWWmsResponse->status_code, $DIDWWmsResponse->body));
            }
            $this->setDelivered($mid);
        }
        catch (\Exception $e)
        {
            throw new \Exception(sprintf(_('Unable to send message: %s'), $e->getMessage()));
        }
    }

    public function callPublic($connector)
    {
        $return_code = 405;
        if ($_SERVER['REQUEST_METHOD'] === "GET")
        {
            if (strstr($_SERVER['QUERY_STRING'], ';') !== FALSE) // using ; as separator
            {
                $qs = str_replace(';', '&', $_SERVER['QUERY_STRING']);
                parse_str($qs, $sms);
            } else {
                $sms = $_GET;
            }
            freepbx_log(FPBX_LOG_INFO, sprintf(_("Webhook (%s) in: %s"), $this->nameRaw, print_r($sms, true)));

            $to = $sms['to'];
            if (preg_match('/^[2-9]\d{2}[2-9]\d{6}$/', $to)) // ten digit NANP
            {
                $to = '1'.$to;
            }

            $from = $sms['from'];
            if (preg_match('/^[2-9]\d{2}[2-9]\d{6}$/', $from)) // ten digit NANP
            {
                $from = '1'.$from;
            }

            $text = $sms['message'] ?? '';
            $emid = $sms['id'];
            //$date = $sms['date'];
            $media = $sms['media'];

            try
            {
                $msgid = $connector->getMessage($to, $from, '', $text, null, null, $emid);
            }
            catch (\Exception $e)
            {
                throw new \Exception(sprintf(_('Unable to get message: %s'), $e->getMessage()));
            }

            if ($media)
            {
                $img = file_get_contents($media);
                $purl = parse_url($media);
                $name = $msgid . basename($purl['path']);
                try
                {
                    $connector->addMedia($msgid, $name, $img);
                }
                catch (\Exception $e)
                {
                    throw new \Exception(sprintf(_('Unable to store MMS media: %s'), $e->getMessage()));
                }
            }
            $connector->emitSmsInboundUserEvt($msgid, $to, $from, '', $text, null, 'Smsconnector', $emid);
            $return_code = 202;
        }
        return $return_code;
    }

    public function getWebHookUrl()
    {
        return sprintf("%s", parent::getWebHookUrl());
    }
}
billsimon commented 1 week ago

Please start a branch and post a pull request. We can work on it there. I won't merge until it's ready.

lgnch commented 1 week ago

Please start a branch and post a pull request. We can work on it there. I won't merge until it's ready.

Sorry, not a contributor so cannot create a branch. Happy for you to create one and then I can assist and provide testing.

lgnch commented 1 week ago

Correctly working code with instructions for INBOUND - Outbound is yet to be tested. Note, I have not yet tested contexts and will update when that occurs. But this should get people statrted.

<?php

        // parameters from DIDWW
        // Available variables for Path, Headers, and Request Body:
        // from https://doc.didww.com/sms/sms-trunks/technical-data/http-specification.html

        //
        // NOTE DIDWW changes parameters from inbound to outbound.
        //
        // Inbound Standard Headers
        // {SMS_UUID} - The ID of the received message.
        // {SMS_SRC_ADDR} - Source number of SMS sender. RFC 3986 encoded.
        // {SMS_DST_ADDR} - Destination number to which SMS is received. RFC 3986 encoded.
        // {SMS_TEXT} - The SMS Message body raw format.
        // {SMS_TEXT_BASE64_ENCODED} - The SMS Message body. Encrypted using Base 64 encoding.
        // {SMS_TIME} - Message receive date and time. RFC 1123 encoded.

        // Accepted Body types
        // RAW - the request body will be sent to customers system without changes.
        // JSON - the request body will be sent to customers system in JSON format.
        // HTML URL encoded - the request body will be sent to customers system in HTML URL Encoded format.
        // HTML Multipart - the request body will be sent to customers system in HTML Multipart format.

        // Outbound headers change from inbound headers as follows

        // [SMS_SRC_ADDR} changes to {source}
        // {SMS_DST_ADDR} chnages to {destination}
        // {SMS_TEXT} changes to {content}

        // Passing username and password is undertaken as  follows

        //      curl \
        //              -u username:password \
        //              -X POST \
        //              -H "Content-Type: application/vnd.api+json" \
        //              https://sms-out.didww.com/outbound_messages

        // Example curl string
        // curl -i -X POST https://sms-out.didww.com/outbound_messages -H "Content-Type: application/vnd.api+json" -H "Authorization: Basic" --data-raw '{"data": {"attributes": {"content": "Hello World!", "destination": "37041654321", "source": "37041123456"}, "type": "outbound_messages"}}'

        // Example Outbound Post
        // POST /outbound_messages HTTP/1.1
        //  Host: sms-out.didww.com
        // Content-Type: application/vnd.api+json
        // Authorization: Basic
                //{
                //    "data": {
                //    "type": "outbound_messages",
                //    "attributes": {
                //        "destination": "37041654321",
                //        "source": "37041123456",
                //        "content": "Hello World!"
                //        }
                //    }
                //}

        // Example callback request

        //      Notification payload is sent by using HTTP POST method:

        //      {
        //          "data": {
        //              "type": "outbound_message_callbacks",
        //              "id": "550e8400-e29b-41d4-a716-446655440000",
        //              "attributes": {
        //                  "time_start": "1997-07-16T19:20:30.45Z",
        //                  "end_time": "2001-07-16T19:20:30.45Z",
        //                  "destination": "37041654321",
        //                  "source": "37041123456",
        //                  "status": "Success",
        //                  "code_id": null,
        //                  "fragments_sent": 7,
        //                  "price": 0.001
        //              }
        //          }
        //      }

        // FREEPBX INSTALL INSTRUCTIONS
        // Follow install for SMS connector
        // Copy this code into a file in /var/www/html/admin/modules/smsconnector/providers
        //
        // nano provider-DIDWW.php
        //
        // chown asterisk:asieisk provider-DIDWW.php
        // chmod 644 provider-DIDWW.php
        // fwconsole reload
        //
        // configure FREEPBX DIDWW settings - MAKE SURE YOU SET YOU AMPURL in advanced as per SMSconnector instructions
        //
        // Make sure you login to the freepbx console and go to admin --> config edit
        // to avoid warnings about the missing smsconnector-messages-custom context
        // add the following line into extensions_custom.conf
        //
        //  [smsconnector-messages-custom]
        //  ; Custom context for SMS Connector
        //
        // save changes and apply config.
        //
        // create log file for writing.
        // login via ssh to your freepbx server - chang to root user su - root
        // touch /var/log/sms_test.log
        //
        // When adding your destination number make sure you do not use the +
        // inbound curl to use for testing curl -X POST https://yourliveurl/smsconn/provider.php?provider=didww -H "Content-Type: application/json" -d '{"SMS_TIME": "2024-11-14T12:34:56Z", "SMS_SRC_ADDR": "+ANUMBER", "SMS_DST_ADDR": "YOUREXTENSION-NO", "SMS_TEXT": "Hello, this is a test message"}'
        //
        // messages will appear in UCP - not yet in handsets unless you have setup teh contexts which is not yet included in this instruction.
        //
        // To monitor open terminal and run tail -f /var/log/sms_test.log and Freepbx full log for responses
        //

        namespace FreePBX\modules\Smsconnector\Provider;

        class DIDWW extends providerBase
        {
            public function __construct()
            {
                parent::__construct();
                $this->name       = _('DIDWW');
                $this->nameRaw    = 'didww';
                $this->APIUrlInfo = 'https://doc.didww.com/sms/sms-trunks/technical-data/http-specification.html';

                $this->configInfo = array(
                    'trunk_url' => array(
                        'type'        => 'string',
                        'label'       => _('HTTP Trunk URL'),
                        'help'        => _("Enter the DIDWW HTTP Trunk URL for sending SMS."),
                        'default'     => 'https://us.sms-out.didww.com/outbound_messages',
                        'required'    => true,
                        'placeholder' => _('Enter the DIDWW HTTP Trunk URL'),
                    ),
                    'api_key' => array(
                        'type'        => 'string',
                        'label'       => _('Username'),
                        'help'        => _("Enter the DIDWW Username"),
                        'default'     => '',
                        'required'    => true,
                        'placeholder' => _('Enter Username'),
                    ),
                    'api_secret' => array(
                        'type'        => 'string',
                        'label'       => _('Password'),
                        'help'        => _("Enter the Password"),
                        'default'     => '',
                        'required'    => true,
                        'class'       => 'confidential',
                        'placeholder' => _('Enter Password'),
                    ),
                );
            }

            // Outbound SMS Message Sending
            public function sendMessage($id, $to, $from, $message = null)
            {
                $payload = [
                    'data' => [
                        'type' => 'outbound_messages',
                        'attributes' => [
                            'destination' => $to,
                            'source' => $from,
                            'content' => $message
                        ]
                    ]
                ];
                $this->sendDIDWWRequest($payload, $id);
                return true;
            }

            // Outbound MMS Message Sending
            public function sendMedia($id, $to, $from, $message = null)
            {
                $payload = [
                    'data' => [
                        'type' => 'outbound_messages',
                        'attributes' => [
                            'destination' => $to,
                            'source' => $from,
                            'content' => $message,
                            'attachments' => $this->media_urls($id) // Assuming media_urls is a helper function to retrieve media URLs for MMS
                            ]
                            ]
                        ];
                        $this->sendDIDWWRequest($payload, $id);
                        return true;
                    }

                    // Handles sending of both SMS and MMS messages
                    private function sendDIDWWRequest($payload, $mid)
                    {
                        $config = $this->getConfig($this->nameRaw);
                        $url = $config['trunk_url'];
                        $headers = [
                            "Content-Type" => "application/vnd.api+json",
                            "Authorization" => "Basic " . base64_encode($config['api_key'] . ":" . $config['api_secret'])
                        ];
                        $jsonPayload = json_encode($payload);

                        $session = \FreePBX::Curl()->requests($url);
                        try {
                            $response = $session->post('', $headers, $jsonPayload);
                            $logMessage = sprintf(_("%s responds: HTTP %s, %s"), $this->nameRaw, $response->status_code, $response->body);
                            freepbx_log(FPBX_LOG_INFO, $logMessage);
                            file_put_contents("/var/log/sms_test.log", $logMessage . PHP_EOL, FILE_APPEND);

                            if (!$response->success) {
                                throw new \Exception(sprintf(_("HTTP %s, %s"), $response->status_code, $response->body));
                            }
                            $this->setDelivered($mid);
                        } catch (\Exception $e) {
                            $errorMessage = sprintf(_('Unable to send message: %s'), $e->getMessage());
                            freepbx_log(FPBX_LOG_ERROR, $errorMessage);
                            file_put_contents("/var/log/sms_test.log", $errorMessage . PHP_EOL, FILE_APPEND);
                            throw new \Exception($errorMessage);
                        }
                    }

                    // Inbound SMS Processing (handles DIDWW callbacks for received messages)
                    public function callPublic($connector)
                    {
                        $return_code = 405;
                        if ($_SERVER['REQUEST_METHOD'] === "POST") {
                            // Log raw POST data for debugging
                            $postdata = file_get_contents("php://input");
                            file_put_contents("/var/log/sms_test.log", "Received POST data: " . $postdata . PHP_EOL, FILE_APPEND);
                            $sms = json_decode($postdata, true); // Decode JSON as associative array

                            // Verify that all required DIDWW inbound fields are present
                            if (!isset($sms['SMS_SRC_ADDR'], $sms['SMS_DST_ADDR'], $sms['SMS_TEXT'])) {
                                $error_message = "Missing required fields: 'SMS_SRC_ADDR', 'SMS_DST_ADDR', or 'SMS_TEXT'";
                                file_put_contents("/var/log/sms_test.log", $error_message . PHP_EOL, FILE_APPEND);
                                $return_code = 400; // Bad Request if required fields are missing
                            } else {
                                // Map inbound variables from DIDWW to internal variables
                                $from = ltrim($sms['SMS_SRC_ADDR'], '+'); // Sender’s phone number
                                $to = ltrim($sms['SMS_DST_ADDR'], '+');   // DID number receiving the SMS
                                $text = urldecode($sms['SMS_TEXT']);      // Message content
                                $attachments = isset($sms['MMS_ATTACHMENTS']) ? $sms['MMS_ATTACHMENTS'] : [];

                                // Log the parsed values for verification
                                file_put_contents("/var/log/sms_test.log", "Parsed values - From: $from, To: $to, Text: $text" . PHP_EOL, FILE_APPEND);

                                // Generate the message ID and handle attachments if present
                                $msgid = $connector->getMessage($to, $from, '', $text);
                                foreach ($attachments as $attachment) {
                                    $filename = $attachment['FILENAME'];
                                    $content = base64_decode($attachment['CONTENT']);
                                    $connector->addMedia($msgid, $filename, $content);
                                }

                                // Emit SMS event for inbound message processing in FreePBX
                                file_put_contents("/var/log/sms_test.log", "Emitting SMS Inbound Event\n", FILE_APPEND);
                                $connector->emitSmsInboundUserEvt($msgid, $to, $from, '', $text, null, 'Smsconnector', null);
                                $return_code = 202; // Accepted
                            }
                        } else {
                            file_put_contents("/var/log/sms_test.log", "Invalid request method: " . $_SERVER['REQUEST_METHOD'] . PHP_EOL, FILE_APPEND);
                        }
                        return $return_code;
                    }
                    public function getWebHookUrl()
                    {
                        return sprintf("%s", parent::getWebHookUrl());
                    }
                }