Как правильно обрабатывать длинный текст SMS из Nexmo API?

Будьте добры, это мой первый вопрос, и мой английский не очень хорош.

У меня нет проблем со стандартными сообщениями от Nexmo API, и я хочу получать длинные SMS так же, как и стандартные (т.е. в одном блоке).

Пример данных, полученных от Nexmo для стандартного SMS:

    $_GET['msisdn'] ==> "33612345678" // "from"
    $_GET['to'] ==> "33687654321"
    $_GET['messageId'] ==> "02000000478EBE09"
    $_GET['text'] ==> "Hello world!"
    $_GET['type'] ==> "unicode"
    $_GET['keyword'] ==> "HELLO"
    $_GET['message-timestamp'] ==> "2014-11-25 14:06:58"

Длинный: (Nexmo отправил его по частям)

    $_GET['msisdn'] ==> "33612345678" // "from"
    $_GET['to'] ==> "33687654321"
    $_GET['messageId'] ==> "02000000478EBE09"
    $_GET['text'] ==> "the first part of a too long text..."
    $_GET['type'] ==> "unicode"
    $_GET['keyword'] ==> "THE"
    $_GET['message-timestamp'] ==> "2014-11-25 12:06:58"
    $_GET['concat'] ==> "true"
    $_GET['concat-ref'] ==> "108" // unique identifier for long SMS text
    $_GET['concat-total'] ==> "4" // or more, or less...
    $_GET['concat-part'] ==> "1" // index of the part, start at 1

Подробнее о документации Nexmo: здесь

Итак, я начал с библиотеки, найденной на github (Nexmo-PHP-lib), и сделал это: (довольно некрасиво, но это для тестов)

public function inboundText( $data=null ){
    if(!$data) $data = $_GET;

    if(!isset($data['text'], $data['msisdn'], $data['to'])) 
        return false;
    if(isset($data['concat']) && $data['concat'])
    {
        session_start();
        if ($data['concat-part'] > 1) // first part ?
        {
            if ($data['concat-total'] == $data['concat-part']) // last part ?
            {
                // last part ! stock the data in the text and unset now useless $_SESSION entry!
                $data['text'] = $_SESSION[(string)$data['concat-ref']] . $data['text'];
                unset($_SESSION[(string)$data['concat-ref']]);
            }
            else // not the first or the last, so concat !
            {
                // concat the new part in the entry named after the concat-ref
                $_SESSION[(string)$data['concat-ref']] .= $data['text'];
                return false;
            }
        }
        else // first part ! so creat a $_SESSION entry for that! (named after concat-ref)
        {
            $_SESSION[(string)$data['concat-ref']] = $data['text'];
            return false;
        }
    }
    // Get the relevant data
    $this->to = $data['to'];
    $this->from = $data['msisdn'];
    $this->text = $data['text'];
    $this->network = (isset($data['network-code'])) ? $data['network-code'] : '';
    $this->message_id = $data['messageId'];

    // Flag that we have an inbound message
    $this->inbound_message = true;

    return true;
}

Он отлично работает с локальным тестом, но не когда он размещен на моем сервере heroku, массив $_SESSION, похоже, сбрасывается в каждой части смс...

Итак, вы хоть представляете, как с этим правильно обращаться? (и без уродливой временной таблицы SQL). Как я могу восстановить предыдущую часть сообщения, пока не получу его полностью?


person Dreamonde    schedule 25.11.2014    source источник


Ответы (1)


Использование сеанса для хранения временных частей sms будет работать только в том случае, если между клиентом и сервером происходит обмен ключами с каждым HTTP-запросом, который идентифицирует сеанс.

В PHP, когда вы создаете сеанс и сохраняете значение внутри $_SESSION, на сервере создается файл для хранения этих данных (если вы не используете обработчик сеанса БД, и в этом случае данные сеанса хранятся в базе данных). На эти данные ссылаются с помощью идентификатора (например, PHPSESSID=66dda288eb1947843c2341b4e470fa28), который обычно предоставляется клиенту в виде файла cookie. Когда клиент возвращается при следующем HTTP-запросе, идентификатор возвращается на сервер в виде значения cookie, и сервер использует его для ссылки на те же данные сеанса.

Хотя ваш сервер может предоставлять файл cookie клиенту Nexmo, когда он подключается к URL-адресу вашей конечной точки, Nexmo, вероятно, не сохраняет файл cookie и не возвращает его при следующем запросе. (Это предположение с моей стороны, но я думаю, что оно безопасно — хотя я не могу объяснить, почему оно работает на вашей локальной машине, а не на Heroku. В любом случае, это легко проверить — просто проверьте, предоставляет ли клиент Nexmo любое значение $_COOKIE при последующих запросах).

Итог: если клиент Nexmo не использует файлы cookie для сохранения состояния между запросами, вы не можете использовать сеансы для сохранения временных частей смс.

В любом случае лучшим вариантом (поскольку временные сохраненные части сообщения сохранятся в случае перезагрузки сервера) будет сохранение каждой временной части в небольшую таблицу базы данных. (Не волнуйтесь, это красивая временная таблица SQL. ;) Вот пример с использованием MySQL:

CREATE TABLE `response_concat_tbl` (
    `concat-ref` SMALLINT NOT NULL DEFAULT '0',
    `concat-total` TINYINT UNSIGNED NOT NULL DEFAULT '0',
    `concat-part` TINYINT UNSIGNED NOT NULL DEFAULT '0',
    `text` VARCHAR(160) NOT NULL DEFAULT '',
    `added_datetime` DATETIME DEFAULT NULL,
    UNIQUE KEY `concat-ref` (`concat-ref`, `concat-part`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='A place to temporarily store multipart SMS messages before they are combined and saved as a whole response.';

Здесь я определил UNIQUE KEY, чтобы мы могли избежать дублирования частей сообщения с предложением REPLACE INTO (на случай, если клиент Nexmo попытается отправить одну и ту же часть дважды). added_datetime можно использовать для очистки потерянных сообщений в будущем (в случае, если последняя часть сообщения никогда не будет получена).

Теперь давайте вставим некоторые образцы данных:

REPLACE INTO response_concat_tbl (`concat-ref`,`concat-total`,`concat-part`,text,added_datetime) VALUES ('101','4','1','This is',NOW());
REPLACE INTO response_concat_tbl (`concat-ref`,`concat-total`,`concat-part`,text,added_datetime) VALUES ('101','4','2','a multipart',NOW());
REPLACE INTO response_concat_tbl (`concat-ref`,`concat-total`,`concat-part`,text,added_datetime) VALUES ('101','4','4','for you!',NOW());
REPLACE INTO response_concat_tbl (`concat-ref`,`concat-total`,`concat-part`,text,added_datetime) VALUES ('101','4','3','sms message',NOW());

Теперь мы можем использовать функцию MySQL GROUP_CONCAT для захвата всех частей в одном запросе.

SET SESSION group_concat_max_len = 1000000;
SELECT GROUP_CONCAT(text ORDER BY `concat-part` ASC SEPARATOR ' ') AS text
FROM response_concat_tbl
WHERE `concat-ref` = '101'
GROUP BY `concat-ref`;

Мы устанавливаем параметр group_concat_max_len, чтобы общая длина строки могла быть длиннее, чем стандартные 1024 символа (хотя это уже много сообщений). Вот результат:

+-------------------------------------------------------------+
| GROUP_CONCAT(text ORDER BY `concat-part` ASC SEPARATOR ' ') |
+-------------------------------------------------------------+
| This is a multipart sms message for you!                    |
+-------------------------------------------------------------+

Если вы не используете MySQL, вам может потребоваться немного больше работы (некоторая проверка дублирования, а затем цикл) без REPLACE INTO и GROUP_CONCAT.

Вот полный рабочий пример с использованием этой техники:

class SMS
{
    static public function processMultipart($sms)
    {
        $db =& DB::getInstance();

        if (isset($sms['concat']) && $sms['concat']) {
            // This sms is part of a multipart message, save it temporarily to the db.
            $sms = array_map('trim', $sms);
            $db->query("
                REPLACE INTO response_concat_tbl (
                    `concat-ref`,
                    `concat-total`,
                    `concat-part`,
                    `text`,
                    `added_datetime`
                ) VALUES (
                    '" . $db->escapeString($sms['concat-ref']) . "',
                    '" . $db->escapeString($sms['concat-total']) . "',
                    '" . $db->escapeString($sms['concat-part']) . "',
                    '" . $db->escapeString($sms['text']) . "',
                    NOW()
                )
            ");

            if ($sms['concat-total'] > $sms['concat-part']) {
                // Not all the parts have been received; return false to signal the fact we don't have a complete message yet.
                return false;
            }
            // Otherwise, it means the last part has just been received. Concatonate all the parts and return it.

            // Increase the max length returned by MySQL's GROUP_CONCAT function.
            $db->query("SET SESSION group_concat_max_len = 32000");

            // Group the sms responses by concat-ref and return them as a concatonated string.
            $qid = $db->query("
                SELECT GROUP_CONCAT(text ORDER BY `concat-part` ASC SEPARATOR ' ')
                FROM response_concat_tbl
                WHERE `concat-ref` = '" . $db->escapeString($sms['concat-ref']) . "'
                GROUP BY `concat-ref`
            ");
            list($sms['text']) = $db->fetch_row($qid);

            // Delete the temporary records.
            $db->query("
                DELETE FROM response_concat_tbl
                WHERE `concat-ref` = '" . $db->escapeString($sms['concat-ref']) . "'
            ");
        }

        // If this is not a multipart message, the original sms data is returned. If it is a multipart message, we're returning the fully-concatonated message.
        return $sms;
    }
}

А вот как использовать эту функцию:

// False is returned here if we need to wait for additional parts to arrive. Otherwise, $sms is populated with the final, usable data.
if (false === $sms = SMS::processMultipart($_GET)) {
    header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK', true, 200);
    die('Waiting for additional message parts');
}

// Do something with $sms['text'], e.g.,
SMSResponse::save($sms['text']);
person Quinn Comendant    schedule 26.11.2014