2017年10月10日 星期二

PHP 使用 Twilio 在網頁瀏覽器撥打電話

預計使用瀏覽器的 WebRTC 進行語音對話,所以瀏覽器須支援 WebRTC。
WebRTC 瀏覽器支援狀況:http://caniuse.com/#search=webrtc
注意:Chrome 47以上版本,若不是https加密連線網址,將不允許呼叫 getUserMedia() 使用麥克風、視訊。
  1. PHP Server 端用到的東西,可先參考另一邊文章 「PHP 使用 Twilio 發送簡訊範例」:
    A.下載 Twilio 提供的 PHP SDK
    B.取得 ACCOUNT SIDAUTH TOKEN (https://www.twilio.com/console)
  2. Clinet端瀏覽器則會用到 JavaScript SDK (twilio.min.js)
    我之前使用時,GA(Generally Available)版本為 1.3,所以以下都用1.3版當作範例。
    1.3版網址: https://www.twilio.com/docs/api/client/twilio-js-13
    官方說明為直接外部引用,也可選擇將 twilio.min.js 直接下載回來再使用。
    <script src="//media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js" type="text/javascript">
    官網說明:The twilio.js Library: Twilio in the Browser - Twilio
  3. 登入 Twilio 管理介面,到「Home / Phone Numbers / Tools / TwiML Apps
    新增一個 TwiML App 設定,然後記下 「TwiML App 的  SID
    「FRIENDLY NAME」:自己任意定義的名稱
    「Voice 的 REQUEST URL」:這個很重要,是要給 Twilio 訪問的網址,請填一個之後要開放給 Twilio 訪問的網址。
  4. 下載官方 PHP + JavaScript 範例測試
    GitHub:https://github.com/TwilioDevEd/client-quickstart-php
    下載:https://github.com/TwilioDevEd/client-quickstart-php/archive/master.zip
    解壓縮,並將步驟1 Twilio 提供的 PHP SDK,放到 vendor/ 資料夾下,因範例檔引用路徑是 include('./vendor/autoload.php')。
    最後上傳到可執行PHP的對外Server。
    官方 PHP + JavaScript 範例裡面有幾個檔:index.html、quickstart.js、config.example.php、token.php、randos.php、voice.php,說明如後。
  5. 「index.html」,引用 twilio.min.js (twilio 的 JavaScript SDK工具包)、quickstart.js (JavaScript 使用範例,可隨自己需求修改)
    <!DOCTYPE html>
    <html>
        <head>
            <title>Twilio Client Quickstart</title>
            <link rel="stylesheet" href="site.css">
        </head>
        <body>
            <div id="controls">
                <div id="info">
                    <p class="instructions">Twilio Client</p>
                    <div id="client-name"></div>
                </div>
                <div id="call-controls">
                    <p class="instructions">Make a Call:</p>
                    <input id="phone-number" type="text" placeholder="Enter a phone # or client name" />
                    <button id="button-call">Call</button>
                    <button id="button-hangup">Hangup</button>
                </div>
                <div id="log"></div>
            </div>
    
            <script type="text/javascript" src="//media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js"></script>
            <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
            <script src="quickstart.js"></script>
        </body>
    </html>
  6. 「quickstart.js」,twilio.min.js 使用範例。
    其中這一段會將參數資料傳送給 Twilio,之後 Twilio 訪問第3步驟設定的網址時,也會將這些參數資料一併回傳,所以自訂此處的參數資料,可做一些想達到的效果,例如接收要撥的電話號碼、或是進行自己定義的驗證規則。
    ...
    // get the phone number to connect the call to
    var params = {
        To: document.getElementById('phone-number').value
    };
    
    console.log('Calling ' + params.To + '...');
    Twilio.Device.connect(params);
    ...

    quickstart.js完整內容:
    $(function() {
        log('Requesting Capability Token...');
        $.getJSON('./token.php')
                .done(function(data) {
                    log('Got a token.');
                    console.log('Token: ' + data.token);
    
                    // Setup Twilio.Device
                    Twilio.Device.setup(data.token);
    
                    Twilio.Device.ready(function(device) {
                        log('Twilio.Device Ready!');
                        document.getElementById('call-controls').style.display = 'block';
                    });
    
                    Twilio.Device.error(function(error) {
                        log('Twilio.Device Error: ' + error.message);
                    });
    
                    Twilio.Device.connect(function(conn) {
                        log('Successfully established call!');
                        document.getElementById('button-call').style.display = 'none';
                        document.getElementById('button-hangup').style.display = 'inline';
                    });
    
                    Twilio.Device.disconnect(function(conn) {
                        log('Call ended.');
                        document.getElementById('button-call').style.display = 'inline';
                        document.getElementById('button-hangup').style.display = 'none';
                    });
    
                    Twilio.Device.incoming(function(conn) {
                        log('Incoming connection from ' + conn.parameters.From);
                        var archEnemyPhoneNumber = '+12099517118';
    
                        if (conn.parameters.From === archEnemyPhoneNumber) {
                            conn.reject();
                            log('It\'s your nemesis. Rejected call.');
                        } else {
                            // accept the incoming connection and start two-way audio
                            conn.accept();
                        }
                    });
    
                    setClientNameUI(data.identity);
                })
                .fail(function() {
                    log('Could not get a token from server!');
                });
    
        // Bind button to make call
        document.getElementById('button-call').onclick = function() {
            // get the phone number to connect the call to
            var params = {
                To: document.getElementById('phone-number').value
            };
    
            console.log('Calling ' + params.To + '...');
            Twilio.Device.connect(params);
        };
    
        // Bind button to hangup call
        document.getElementById('button-hangup').onclick = function() {
            log('Hanging up...');
            Twilio.Device.disconnectAll();
        };
    
    });
    
    // Activity log
    function log(message) {
        var logDiv = document.getElementById('log');
        logDiv.innerHTML += '<p>&gt;&nbsp;' + message + '</p>';
        logDiv.scrollTop = logDiv.scrollHeight;
    }
    
    // Set the client name in the UI
    function setClientNameUI(clientName) {
        var div = document.getElementById('client-name');
        div.innerHTML = 'Your client name: <strong>' + clientName +
                '</strong>';
    }
  7. 「config.example.php」,設定檔。
    config.example.php 改名 config.php
    修改 config.php
    $TWILIO_ACCOUNT_SID = 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // 前面取得的 ACCOUNT SID
    $TWILIO_AUTH_TOKEN = 'your_auth_token'; //前面取得的 AUTH TOKEN
    $TWILIO_TWIML_APP_SID = 'APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // 前面取得的 TwiML App SID
    $TWILIO_CALLER_ID = '+1XXXYYYZZZZ'; // 向 Twilio 買的有效的號碼(有語音功能)
    
  8. 「token.php」, JS 一開始會訪問「token.php」,「token.php」會產生給 JS 用的 token
    範例中只有 token是必須的,另一個 identity 可有可無(會出現在 Twilio 的撥號記錄)。
    <?php
    
    include('./vendor/autoload.php');
    include('./config.php');
    include('./randos.php');
    
    use Twilio\Jwt\ClientToken;
    
    // choose a random username for the connecting user
    $identity = randomUsername();
    
    $capability = new ClientToken($TWILIO_ACCOUNT_SID, $TWILIO_AUTH_TOKEN);
    $capability->allowClientOutgoing($TWILIO_TWIML_APP_SID);
    $capability->allowClientIncoming($identity);
    $token = $capability->generateToken();
    
    // return serialized token and the user's randomly generated ID
    header('Content-Type: application/json');
    echo json_encode(array(
        'identity' => $identity,
        'token' => $token,
    ));
    
  9. 「randos.php」,這個檔只是用來產生前面「token.php」的隨機identity,正式應該用不到。
    <?php
    
    // Generate a random username for the connecting client
    function randomUsername() {
        $ADJECTIVES = array(
            'Abrasive', 'Brash', 'Callous', 'Daft', 'Eccentric', 'Fiesty', 'Golden',
            'Holy', 'Ignominious', 'Joltin', 'Killer', 'Luscious', 'Mushy', 'Nasty',
            'OldSchool', 'Pompous', 'Quiet', 'Rowdy', 'Sneaky', 'Tawdry',
            'Unique', 'Vivacious', 'Wicked', 'Xenophobic', 'Yawning', 'Zesty',
        );
    
        $FIRST_NAMES = array(
            'Anna', 'Bobby', 'Cameron', 'Danny', 'Emmett', 'Frida', 'Gracie', 'Hannah',
            'Isaac', 'Jenova', 'Kendra', 'Lando', 'Mufasa', 'Nate', 'Owen', 'Penny',
            'Quincy', 'Roddy', 'Samantha', 'Tammy', 'Ulysses', 'Victoria', 'Wendy',
            'Xander', 'Yolanda', 'Zelda',
        );
    
        $LAST_NAMES = array(
            'Anchorage', 'Berlin', 'Cucamonga', 'Davenport', 'Essex', 'Fresno',
            'Gunsight', 'Hanover', 'Indianapolis', 'Jamestown', 'Kane', 'Liberty',
            'Minneapolis', 'Nevis', 'Oakland', 'Portland', 'Quantico', 'Raleigh',
            'SaintPaul', 'Tulsa', 'Utica', 'Vail', 'Warsaw', 'XiaoJin', 'Yale',
            'Zimmerman',
        );
    
        // Choose random components of username and return it
        $adj = $ADJECTIVES[array_rand($ADJECTIVES)];
        $fn = $FIRST_NAMES[array_rand($FIRST_NAMES)];
        $ln = $LAST_NAMES[array_rand($LAST_NAMES)];
    
        return $adj . $fn . $ln;
    }
    
  10. 「voice.php」,回應 Twilio,Twilio 會根據回應的內容做對應的動作。
    所以回應內容前,也可自己做一些驗證,例如 Twilio 傳送過來的 ACCOUNT SID、TwiML App SID 是否正確,或自己額外傳送的資料是否正常。
    voice.php 的網址須為「Voice 的 REQUEST URL」填的網址。
    <?php
    
    include('./vendor/autoload.php');
    include('./config.php');
    
    use Twilio\Twiml;
    
    $response = new Twiml;
    
    // get the phone number from the page request parameters, if given
    if (isset($_REQUEST['To']) && strlen($_REQUEST['To']) > 0) {
        $number = htmlspecialchars($_REQUEST['To']);
        $dial = $response->dial(array('callerId' => $TWILIO_CALLER_ID));
    
        // wrap the phone number or client name in the appropriate TwiML verb
        // by checking if the number given has only digits and format symbols
        if (preg_match("/^[\d\+\-\(\) ]+$/", $number)) {
            $dial->number($number); //播電話給$number這個號碼
        } else {
            $dial->client($number);
        }
    } else {
        $response->say("Thanks for calling!");//會用語音講"Thanks for calling"
    }
    
    header('Content-Type: text/xml');
    echo $response;
    
    //因要給Twilio 訪問,所以是公開的網址,因此也可以自行加些驗證判斷,不合法就回應404
    //header("HTTP/1.0 404 Not Found");
    
  11. Twilio 發送紀錄:https://www.twilio.com/console/voice/dashboard
    發送失敗debug:https://www.twilio.com/console/dev-tools/debugger


參考:

其他:
Create a Real-Time Video Chat Room with WebRTC & Twilio — SitePoint
https://docs.microsoft.com/zh-tw/azure/partner-twilio-php-make-phone-call
http://twimlets.com/message?Message=test
https://cloud.google.com/appengine/docs/flexible/nodejs/using-sms-and-voice-services-via-twilio
https://www.npmjs.com/package/twilio-js


2017年10月7日 星期六

PHP 使用 Twilio 發送簡訊範例

  1. 下載 Server 端 PHP 的 Twilio SDK(THE TWILIO PHP HELPER LIBRARY)
    官方說明網址:https://www.twilio.com/docs/libraries/php
    可用 Composer 或從 GitHub 下載。
    GitHub:
    https://github.com/twilio/twilio-php
    https://github.com/twilio/twilio-php/archive/master.zip
  2. 登入 Twilio(https://www.twilio.com/) 後,到 Console 頁面(https://www.twilio.com/console) 的 Dashboard,找到 ACCOUNT SIDAUTH TOKEN 兩個值,程式會用到。
  3. PHP 範例
    require __DIR__ . '/Twilio/autoload.php'; // require 下載的 Twilio PHP SDK
    
    $twilio_sid = "....."; //填入前面步驟在 Twilio 的 ACCOUNT SID
    $twilio_token = "....."; //填入前面步驟在 Twilio 的 AUTH TOKEN
    
    $Twilio = new \Twilio\Rest\Client($twilio_sid, $twilio_token);
    $rece_num = "+....."; // 接收者號碼
    $twilio_from = "+....."; // 發送者號碼,須為向 Twilio 買的有效的號碼(有SMS功能)
    $sms_con = "test 測試"; //簡訊內容
    try {
        $message = $Twilio->messages->create(
                $rece_num, array(
            'from' => $twilio_from,
            'body' => $sms_con,
                )
        );
    } catch (\Twilio\Exceptions\TwilioException $ex) {
        //發送失敗
        //"Twilio Exception getCode:" . $ex->getCode();
        //"Twilio Exception getMessage:" . $ex->getMessage();
    } catch (\Exception $ex) {
        // error
    }
    
    if (isset($message->sid)) {
        echo "Twilio message sid:" . $message->sid;
    }
    
  4. 程式使用的電話號碼格式為 E.164 格式


參考:
Sending Messages - Twilio
What is a Message SID? – Twilio Support
SMS Pricing for Text Messaging - Twilio (SMS費用)


2017年10月1日 星期日

硬碟壞軌備份硬碟資料

硬碟出現壞軌時,早期曾用 HDD Regenerator 之類的軟體修復壞軌,但沒多久就又出現壞軌,修幾次後,最終整顆硬碟都沒救。

所以現在只要一發現硬碟 S.M.A.R.T. 出現壞軌資訊時,都會盡量避免繼續使用舊硬碟,立刻買一顆新硬碟,將整個硬碟資料使用 sector by sector 方式複製到新硬碟。

Clone 硬碟的軟體是使用免費版的 EaseUS Todo Backup Free
網址:http://www.todo-backup.comhttps://tw.easeus.com/


新硬碟初始化:
使用 EaseUS Todo Backup Free 10.5 製作的 WinPE 開機光碟操作時,發現一個奇怪的現像,只裝新硬碟時可正常抓到,但若新、舊硬碟同時接上,卻只抓的到舊硬碟,看不到新硬碟。

解決方法是先將新硬碟裝進行初始化,才可同時抓到新、舊硬碟。
例如在 windows「磁碟管理」中初始化磁碟。

在新硬碟上右鍵選擇「初始化磁碟」(一般進去「磁碟管理」就會自動彈出第2張提示初始化的畫面)




建立開機光碟:
  1. 安裝好 EaseUS Todo Backup Free 後,開啟軟體。選擇「工具」->「建立開機碟」
  2. 選擇要用的開機工具與製作方式。
    例如:「建立 WinPE 開機碟」->「建立 ISO」->「執行」,再燒成開機光碟。


複製硬碟資料:
  1. 使用製作的開機光碟開機,可能需一點時間,開機過程畫面如下。


  2. 開機完成後,選擇「Clone」
  3. 選擇來源硬碟(舊硬碟),可選擇複製整個硬碟(disk)或選擇的分割區(partition),這邊是選整顆硬碟。
  4. 選擇目的地硬碟(新硬碟),並勾選「Advanced options」裡的「Sector by sector clone」
  5. 確定設定沒問題後,按「Proceed」開始備份複製。
  6. 複製過程畫面。複製完即可。



參考:
Sector by sector clone - EaseUS Todo Backup online help
How to Clone Drive/HDD/SSD with Bad Sectors - EaseUS

使用 Eraser 抹除硬碟資料,無法刪除未完成的任務

Eraser 可選擇不同演算法抹除硬碟的資料,避免刪除的資料被還原。

之前使用 Eraser(6.2.0.2979)  抹除硬碟資料時,因選的演算法較複雜,覺得要跑很久,想要先停止,再重新設定。

但在「Erase Schedule」中執行「Cancel Task」,卻無法停止,也無法刪除任務。
移除、重開機、重新安裝,都沒用,只要一打開 Eraser,就開始跑那無法刪除的任務。

解決方法:
到「%LOCALAPPDATA%\Eraser 6\」資料夾底下,刪除「Task List.ersy」這個檔案即可。

參考:
Task Won't Complete and Cannot be Cancelled | Eraser Forum


2017年8月14日 星期一

Linux Bash 刪除 history 指令操作歷史紀錄

在 Linux Bash 下過的指令紀錄,可用 history 指令查詢。
下過的指令,會先存放在 buffer,退出 bash 時,再寫入記錄檔。因此,之後登入時,也能看到之前下過的指令。

history操作紀錄相關的幾個環境變數:
  • 操作歷史紀錄,儲存的檔案位置。(操作歷史紀錄檔)(.bash_history)
    # echo $HISTFILE
    /root/.bash_history
    
  • 操作歷史紀錄檔,最多儲存幾筆。
    # echo $HISTFILESIZE
    1000
    
  • history 最多列出幾筆(在記憶體中存放的筆數)
    # echo $HISTSIZE
    1000
    


刪除全部的操作紀錄:
# history -c
# history -w
說明:「history -c」會刪除「下 history 指令時,列出的操作紀錄」, 但不會刪除「.bash_history」(HISTFILE)的檔案內容,為避免重新登入後,又讀取「.bash_history」(HISTFILE)的檔案內容, 所以須再用「history -w」寫入目前已清空的操作紀錄。


只刪除這次登入後的操作紀錄:
  • 方法一:清空 HISTFILE 變數內容,則登出時,不會將本次操作紀錄儲存到 HISTFILE 設定的檔案。
    # unset HISTFILE
    
  • 方法二:將 HISTSIZE 設為0,下「history」指令時,也不會列出指令(記憶體中沒存放指令紀錄?),則登出時,不會更新記錄檔內容(?)。
    (註:參考資料是寫會刪除全部紀錄,但我測試是只有不會儲存本次操作紀錄)
    # HISTSIZE=0
    
  • 方法三:強制刪除本次登入 Bash 的 PID,則本次操作紀錄,不會儲存到記錄檔。
    # kill -9 $$
    

說明:
  • 「$$」的變數內容是本次登入的 PID,一般和「$BASHPID」一樣,但有時不同(在 subshell 中會不同)。
    # echo $$
    4308
    # echo $BASHPID
    4308
    # (echo $BASHPID)
    4376
    # (echo $BASHPID $$)
    4377 4308
    
  • 「subshell」不等於「Bash 裡面再執行一次 Bash」。
    「subshell」,可以訪問父 Shell 的任何變數。
    「Bash 裡面再執行一次 Bash」,只能訪問父 Shell 的環境變數。
    TestAA:自訂的變數
    HISTSIZE:環境變數
    # TestAA=10
    # HISTSIZE=3
    # echo $TestAA
    10
    # echo $HISTSIZE
    3
    
    # (echo $TestAA)
    10
    # (echo $HISTSIZE)
    3
    
    # bash
    # echo $TestAA
    (無資料)
    # echo $HISTSIZE
    3
    


參考:
Linux系统下history命令的记录如何删除 - 我的技术文档整理 - 开源中国社区
Quit Bash Shell Without Saving Bash History (5 Methods) - If Not True Then False
SHLVL 和 BASH_SUBSHELL 兩個變量的區別 - 壹讀