windhamdavid 10 years ago
parent
commit
881c771a33
88 changed files with 3545 additions and 0 deletions
  1. 311 0
      inc/lib/call/Services/Twilio.php
  2. 109 0
      inc/lib/call/Services/Twilio/AutoPagingIterator.php
  3. 346 0
      inc/lib/call/Services/Twilio/Capability.php
  4. 3 0
      inc/lib/call/Services/Twilio/HttpException.php
  5. 94 0
      inc/lib/call/Services/Twilio/HttpStream.php
  6. 84 0
      inc/lib/call/Services/Twilio/InstanceResource.php
  7. 203 0
      inc/lib/call/Services/Twilio/ListResource.php
  8. 35 0
      inc/lib/call/Services/Twilio/NumberType.php
  9. 68 0
      inc/lib/call/Services/Twilio/Page.php
  10. 41 0
      inc/lib/call/Services/Twilio/PartialApplicationHelper.php
  11. 36 0
      inc/lib/call/Services/Twilio/RequestValidator.php
  12. 134 0
      inc/lib/call/Services/Twilio/Resource.php
  13. 35 0
      inc/lib/call/Services/Twilio/Rest/Account.php
  14. 25 0
      inc/lib/call/Services/Twilio/Rest/Accounts.php
  15. 12 0
      inc/lib/call/Services/Twilio/Rest/Address.php
  16. 6 0
      inc/lib/call/Services/Twilio/Rest/Addresses.php
  17. 6 0
      inc/lib/call/Services/Twilio/Rest/Application.php
  18. 12 0
      inc/lib/call/Services/Twilio/Rest/Applications.php
  19. 6 0
      inc/lib/call/Services/Twilio/Rest/AuthorizedConnectApp.php
  20. 10 0
      inc/lib/call/Services/Twilio/Rest/AuthorizedConnectApps.php
  21. 7 0
      inc/lib/call/Services/Twilio/Rest/AvailablePhoneNumber.php
  22. 54 0
      inc/lib/call/Services/Twilio/Rest/AvailablePhoneNumbers.php
  23. 106 0
      inc/lib/call/Services/Twilio/Rest/Call.php
  24. 77 0
      inc/lib/call/Services/Twilio/Rest/Calls.php
  25. 12 0
      inc/lib/call/Services/Twilio/Rest/Conference.php
  26. 6 0
      inc/lib/call/Services/Twilio/Rest/Conferences.php
  27. 6 0
      inc/lib/call/Services/Twilio/Rest/ConnectApp.php
  28. 10 0
      inc/lib/call/Services/Twilio/Rest/ConnectApps.php
  29. 30 0
      inc/lib/call/Services/Twilio/Rest/Credential.php
  30. 42 0
      inc/lib/call/Services/Twilio/Rest/CredentialList.php
  31. 37 0
      inc/lib/call/Services/Twilio/Rest/CredentialListMapping.php
  32. 24 0
      inc/lib/call/Services/Twilio/Rest/CredentialListMappings.php
  33. 24 0
      inc/lib/call/Services/Twilio/Rest/CredentialLists.php
  34. 28 0
      inc/lib/call/Services/Twilio/Rest/Credentials.php
  35. 6 0
      inc/lib/call/Services/Twilio/Rest/DependentPhoneNumber.php
  36. 6 0
      inc/lib/call/Services/Twilio/Rest/DependentPhoneNumbers.php
  37. 70 0
      inc/lib/call/Services/Twilio/Rest/Domain.php
  38. 28 0
      inc/lib/call/Services/Twilio/Rest/Domains.php
  39. 34 0
      inc/lib/call/Services/Twilio/Rest/Feedback.php
  40. 33 0
      inc/lib/call/Services/Twilio/Rest/FeedbackSummary.php
  41. 91 0
      inc/lib/call/Services/Twilio/Rest/IncomingPhoneNumber.php
  42. 59 0
      inc/lib/call/Services/Twilio/Rest/IncomingPhoneNumbers.php
  43. 40 0
      inc/lib/call/Services/Twilio/Rest/IpAccessControlList.php
  44. 37 0
      inc/lib/call/Services/Twilio/Rest/IpAccessControlListMapping.php
  45. 25 0
      inc/lib/call/Services/Twilio/Rest/IpAccessControlListMappings.php
  46. 27 0
      inc/lib/call/Services/Twilio/Rest/IpAccessControlLists.php
  47. 34 0
      inc/lib/call/Services/Twilio/Rest/IpAddress.php
  48. 33 0
      inc/lib/call/Services/Twilio/Rest/IpAddresses.php
  49. 31 0
      inc/lib/call/Services/Twilio/Rest/Media.php
  50. 37 0
      inc/lib/call/Services/Twilio/Rest/MediaInstance.php
  51. 22 0
      inc/lib/call/Services/Twilio/Rest/Member.php
  52. 28 0
      inc/lib/call/Services/Twilio/Rest/Members.php
  53. 58 0
      inc/lib/call/Services/Twilio/Rest/Message.php
  54. 73 0
      inc/lib/call/Services/Twilio/Rest/Messages.php
  55. 6 0
      inc/lib/call/Services/Twilio/Rest/Notification.php
  56. 6 0
      inc/lib/call/Services/Twilio/Rest/Notifications.php
  57. 6 0
      inc/lib/call/Services/Twilio/Rest/OutgoingCallerId.php
  58. 12 0
      inc/lib/call/Services/Twilio/Rest/OutgoingCallerIds.php
  59. 10 0
      inc/lib/call/Services/Twilio/Rest/Participant.php
  60. 10 0
      inc/lib/call/Services/Twilio/Rest/Participants.php
  61. 10 0
      inc/lib/call/Services/Twilio/Rest/Queue.php
  62. 19 0
      inc/lib/call/Services/Twilio/Rest/Queues.php
  63. 9 0
      inc/lib/call/Services/Twilio/Rest/Recording.php
  64. 6 0
      inc/lib/call/Services/Twilio/Rest/Recordings.php
  65. 6 0
      inc/lib/call/Services/Twilio/Rest/Sandbox.php
  66. 6 0
      inc/lib/call/Services/Twilio/Rest/ShortCode.php
  67. 10 0
      inc/lib/call/Services/Twilio/Rest/ShortCodes.php
  68. 19 0
      inc/lib/call/Services/Twilio/Rest/Sip.php
  69. 6 0
      inc/lib/call/Services/Twilio/Rest/SmsMessage.php
  70. 18 0
      inc/lib/call/Services/Twilio/Rest/SmsMessages.php
  71. 5 0
      inc/lib/call/Services/Twilio/Rest/Token.php
  72. 24 0
      inc/lib/call/Services/Twilio/Rest/Tokens.php
  73. 6 0
      inc/lib/call/Services/Twilio/Rest/Transcription.php
  74. 6 0
      inc/lib/call/Services/Twilio/Rest/Transcriptions.php
  75. 6 0
      inc/lib/call/Services/Twilio/Rest/UsageRecord.php
  76. 33 0
      inc/lib/call/Services/Twilio/Rest/UsageRecords.php
  77. 5 0
      inc/lib/call/Services/Twilio/Rest/UsageTrigger.php
  78. 27 0
      inc/lib/call/Services/Twilio/Rest/UsageTriggers.php
  79. 44 0
      inc/lib/call/Services/Twilio/RestException.php
  80. 14 0
      inc/lib/call/Services/Twilio/SIPListResource.php
  81. 31 0
      inc/lib/call/Services/Twilio/TimeRangeResource.php
  82. 126 0
      inc/lib/call/Services/Twilio/TinyHttp.php
  83. 137 0
      inc/lib/call/Services/Twilio/Twiml.php
  84. 20 0
      inc/lib/call/Services/Twilio/UsageResource.php
  85. 33 0
      inc/lib/call/call.js
  86. 38 0
      inc/lib/call/call.php
  87. 15 0
      inc/lib/call/phone.php
  88. 5 0
      inc/lib/call/twiml.xml

+ 311 - 0
inc/lib/call/Services/Twilio.php

@@ -0,0 +1,311 @@
+<?php
+
+/*
+ * Author:   Neuman Vong neuman@twilio.com
+ * License:  http://creativecommons.org/licenses/MIT/ MIT
+ * Link:     https://twilio-php.readthedocs.org/en/latest/
+ */
+
+function Services_Twilio_autoload($className) {
+    if (substr($className, 0, 15) != 'Services_Twilio') {
+        return false;
+    }
+    $file = str_replace('_', '/', $className);
+    $file = str_replace('Services/', '', $file);
+    return include dirname(__FILE__) . "/$file.php";
+}
+
+spl_autoload_register('Services_Twilio_autoload');
+
+/**
+ * Create a client to talk to the Twilio API.
+ *
+ *
+ * :param string               $sid:      Your Account SID
+ * :param string               $token:    Your Auth Token from `your dashboard
+ *      <https://www.twilio.com/user/account>`_
+ * :param string               $version:  API version to use
+ * :param $_http:    A HTTP client for making requests.
+ * :type $_http: :php:class:`Services_Twilio_TinyHttp`
+ * :param int                  $retryAttempts:
+ *      Number of times to retry failed requests. Currently only idempotent
+ *      requests (GET's and DELETE's) are retried.
+ *
+ * Here's an example:
+ *
+ * .. code-block:: php
+ *
+ *      require('Services/Twilio.php');
+ *      $client = new Services_Twilio('AC123', '456bef', null, null, 3);
+ *      // Take some action with the client, etc.
+ */
+class Services_Twilio extends Services_Twilio_Resource
+{
+    const USER_AGENT = 'twilio-php/3.12.8';
+
+    protected $http;
+    protected $retryAttempts;
+    protected $last_response;
+    protected $version;
+    protected $versions = array('2008-08-01', '2010-04-01');
+
+    public function __construct(
+        $sid,
+        $token,
+        $version = null,
+        Services_Twilio_TinyHttp $_http = null,
+        $retryAttempts = 1
+    ) {
+        $this->version = in_array($version, $this->versions) ?
+                $version : end($this->versions);
+
+        if (null === $_http) {
+            if (!in_array('openssl', get_loaded_extensions())) {
+                throw new Services_Twilio_HttpException("The OpenSSL extension is required but not currently enabled. For more information, see http://php.net/manual/en/book.openssl.php");
+            }
+            if (in_array('curl', get_loaded_extensions())) {
+                  $_http = new Services_Twilio_TinyHttp(
+                      "https://api.twilio.com",
+                      array(
+                          "curlopts" => array(
+                              CURLOPT_USERAGENT => self::qualifiedUserAgent(phpversion()),
+                              CURLOPT_HTTPHEADER => array('Accept-Charset: utf-8'),
+                          ),
+                      )
+                  );
+            } else {
+                $_http = new Services_Twilio_HttpStream(
+                    "https://api.twilio.com",
+                    array(
+                        "http_options" => array(
+                            "http" => array(
+                                "user_agent" => self::qualifiedUserAgent(phpversion()),
+                                "header" => "Accept-Charset: utf-8\r\n",
+                            ),
+                            "ssl" => array(
+                                'verify_peer' => true,
+                                'verify_depth' => 5,
+                            ),
+                        ),
+                    )
+                );
+            }
+        }
+        $_http->authenticate($sid, $token);
+        $this->http = $_http;
+        $this->accounts = new Services_Twilio_Rest_Accounts($this, "/{$this->version}/Accounts");
+        $this->account = $this->accounts->get($sid);
+        $this->retryAttempts = $retryAttempts;
+    }
+
+    /**
+     * Fully qualified user agent with the current PHP Version.
+     *
+     * :return: the user agent
+     * :rtype: string
+     */
+    public static function qualifiedUserAgent($php_version) {
+        return self::USER_AGENT . " (php $php_version)";
+    }
+
+    /**
+     * Get the api version used by the rest client
+     *
+     * :return: the API version in use
+     * :returntype: string
+     */
+    public function getVersion() {
+        return $this->version;
+    }
+
+    /**
+     * Get the retry attempt limit used by the rest client
+     *
+     * :return: the number of retry attempts
+     * :rtype: int
+     */
+    public function getRetryAttempts() {
+        return $this->retryAttempts;
+    }
+
+    /**
+     * Construct a URI based on initial path, query params, and paging
+     * information
+     *
+     * We want to use the query params, unless we have a next_page_uri from the
+     * API.
+     *
+     * :param string $path: The request path (may contain query params if it's
+     *      a next_page_uri)
+     * :param array $params: Query parameters to use with the request
+     * :param boolean $full_uri: Whether the $path contains the full uri
+     *
+     * :return: the URI that should be requested by the library
+     * :returntype: string
+     */
+    public static function getRequestUri($path, $params, $full_uri = false) {
+        $json_path = $full_uri ? $path : "$path.json";
+        if (!$full_uri && !empty($params)) {
+            $query_path = $json_path . '?' . http_build_query($params, '', '&');
+        } else {
+            $query_path = $json_path;
+        }
+        return $query_path;
+    }
+
+    /**
+     * Helper method for implementing request retry logic
+     *
+     * :param array  $callable:      The function that makes an HTTP request
+     * :param string $uri:           The URI to request
+     * :param int    $retriesLeft:   Number of times to retry
+     *
+     * :return: The object representation of the resource
+     * :rtype: object
+     */
+    protected function _makeIdempotentRequest($callable, $uri, $retriesLeft) {
+        $response = call_user_func_array($callable, array($uri));
+        list($status, $headers, $body) = $response;
+        if ($status >= 500 && $retriesLeft > 0) {
+            return $this->_makeIdempotentRequest($callable, $uri, $retriesLeft - 1);
+        } else {
+            return $this->_processResponse($response);
+        }
+    }
+
+    /**
+     * GET the resource at the specified path.
+     *
+     * :param string $path:   Path to the resource
+     * :param array  $params: Query string parameters
+     * :param boolean  $full_uri: Whether the full URI has been passed as an
+     *      argument
+     *
+     * :return: The object representation of the resource
+     * :rtype: object
+     */
+    public function retrieveData($path, $params = array(),
+        $full_uri = false
+    ) {
+        $uri = self::getRequestUri($path, $params, $full_uri);
+        return $this->_makeIdempotentRequest(array($this->http, 'get'),
+            $uri, $this->retryAttempts);
+    }
+
+    /**
+     * DELETE the resource at the specified path.
+     *
+     * :param string $path:   Path to the resource
+     * :param array  $params: Query string parameters
+     *
+     * :return: The object representation of the resource
+     * :rtype: object
+     */
+    public function deleteData($path, $params = array())
+    {
+        $uri = self::getRequestUri($path, $params);
+        return $this->_makeIdempotentRequest(array($this->http, 'delete'),
+            $uri, $this->retryAttempts);
+    }
+
+    /**
+     * POST to the resource at the specified path.
+     *
+     * :param string $path:   Path to the resource
+     * :param array  $params: Query string parameters
+     *
+     * :return: The object representation of the resource
+     * :rtype: object
+     */
+    public function createData($path, $params = array())
+    {
+        $path = "$path.json";
+        $headers = array('Content-Type' => 'application/x-www-form-urlencoded');
+        $response = $this->http->post(
+            $path, $headers, self::buildQuery($params, '')
+        );
+        return $this->_processResponse($response);
+    }
+
+    /**
+     * Build a query string from query data
+     *
+     * :param array $queryData: An associative array of keys and values. The
+     *      values can be a simple type or a list, in which case the list is
+     *      converted to multiple query parameters with the same key.
+     * :param string $numericPrefix:
+     * :param string $queryStringStyle: Determine how to build the url
+     *      - strict: Build a standards compliant query string without braces (can be hacked by using braces in key)
+     *      - php: Build a PHP compatible query string with nested array syntax
+     * :return: The encoded query string
+     * :rtype: string
+     */
+    public static function buildQuery($queryData, $numericPrefix = '') {
+            $query = '';
+            // Loop through all of the $query_data
+            foreach ($queryData as $key => $value) {
+                // If the key is an int, add the numeric_prefix to the beginning
+                if (is_int($key)) {
+                    $key = $numericPrefix . $key;
+                }
+
+                // If the value is an array, we will end up recursing
+                if (is_array($value)) {
+                    // Loop through the values
+                    foreach ($value as $value2) {
+                        // Add an arg_separator if needed
+                        if ($query !== '') {
+                            $query .= '&';
+                        }
+                        // Recurse
+                        $query .= self::buildQuery(array($key => $value2), $numericPrefix);
+                    }
+                } else {
+                    // Add an arg_separator if needed
+                    if ($query !== '') {
+                        $query .= '&';
+                    }
+                    // Add the key and the urlencoded value (as a string)
+                    $query .= $key . '=' . urlencode((string)$value);
+                }
+            }
+        return $query;
+    }
+
+    /**
+     * Convert the JSON encoded resource into a PHP object.
+     *
+     * :param array $response: 3-tuple containing status, headers, and body
+     *
+     * :return: PHP object decoded from JSON
+     * :rtype: object
+     * :throws: A :php:class:`Services_Twilio_RestException` if the Response is
+     *      in the 300-500 range of status codes.
+     */
+    private function _processResponse($response)
+    {
+        list($status, $headers, $body) = $response;
+        if ($status === 204) {
+            return true;
+        }
+        $decoded = json_decode($body);
+        if ($decoded === null) {
+            throw new Services_Twilio_RestException(
+                $status,
+                'Could not decode response body as JSON. ' .
+                'This likely indicates a 500 server error'
+            );
+        }
+        if (200 <= $status && $status < 300) {
+            $this->last_response = $decoded;
+            return $decoded;
+        }
+        throw new Services_Twilio_RestException(
+            $status,
+            isset($decoded->message) ? $decoded->message : '',
+            isset($decoded->code) ? $decoded->code : null,
+            isset($decoded->more_info) ? $decoded->more_info : null
+        );
+    }
+}
+

+ 109 - 0
inc/lib/call/Services/Twilio/AutoPagingIterator.php

@@ -0,0 +1,109 @@
+<?php
+
+class Services_Twilio_AutoPagingIterator
+    implements Iterator
+{
+    protected $generator;
+    protected $args;
+    protected $items;
+
+    private $_args;
+
+    public function __construct($generator, $page, $size, $filters) {
+        $this->generator = $generator;
+        $this->page = $page;
+        $this->size = $size;
+        $this->filters = $filters;
+        $this->items = array();
+
+        // Save a backup for rewind()
+        $this->_args = array(
+            'page' => $page,
+            'size' => $size,
+            'filters' => $filters,
+        );
+    }
+
+    public function current()
+    {
+        return current($this->items);
+    }
+
+    public function key()
+    {
+        return key($this->items);
+    }
+
+    /*
+     * Return the next item in the list, making another HTTP call to the next
+     * page of resources if necessary.
+     */
+    public function next()
+    {
+        try {
+            $this->loadIfNecessary();
+            return next($this->items);
+        }
+        catch (Services_Twilio_RestException $e) {
+            // 20006 is an out of range paging error, everything else is valid
+            if ($e->getCode() != 20006) {
+                throw $e;
+            }
+        }
+    }
+
+    /*
+     * Restore everything to the way it was before we began paging. This gets
+     * called at the beginning of any foreach() loop
+     */
+    public function rewind()
+    {
+        foreach ($this->_args as $arg => $val) {
+            $this->$arg = $val;
+        }
+        $this->items = array();
+        $this->next_page_uri = null;
+    }
+
+    public function count()
+    {
+        throw new BadMethodCallException('Not allowed');
+    }
+
+    public function valid()
+    {
+        try {
+            $this->loadIfNecessary();
+            return key($this->items) !== null;
+        }
+        catch (Services_Twilio_RestException $e) {
+            // 20006 is an out of range paging error, everything else is valid
+            if ($e->getCode() != 20006) {
+                throw $e;
+            }
+        }
+        return false;
+    }
+
+    /*
+     * Fill $this->items with a new page from the API, if necessary.
+     */
+    protected function loadIfNecessary()
+    {
+        if (// Empty because it's the first time or last page was empty
+            empty($this->items)
+            // null key when the items list is iterated over completely
+            || key($this->items) === null
+        ) {
+            $page = call_user_func_array($this->generator, array(
+                $this->page,
+                $this->size,
+                $this->filters,
+                $this->next_page_uri,
+            ));
+            $this->next_page_uri = $page->next_page_uri;
+            $this->items = $page->getItems();
+            $this->page = $this->page + 1;
+        }
+    }
+}

+ 346 - 0
inc/lib/call/Services/Twilio/Capability.php

@@ -0,0 +1,346 @@
+<?php
+
+/**
+ * Twilio Capability Token generator
+ *
+ * @category Services
+ * @package  Services_Twilio
+ * @author Jeff Lindsay <jeff.lindsay@twilio.com>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ */
+class Services_Twilio_Capability
+{
+    public $accountSid;
+    public $authToken;
+    public $scopes;
+
+    /**
+     * Create a new TwilioCapability with zero permissions. Next steps are to
+     * grant access to resources by configuring this token through the
+     * functions allowXXXX.
+     *
+     * @param $accountSid the account sid to which this token is granted access
+     * @param $authToken the secret key used to sign the token. Note, this auth
+     *        token is not visible to the user of the token.
+     */
+    public function __construct($accountSid, $authToken)
+    {
+        $this->accountSid = $accountSid;
+        $this->authToken = $authToken;
+        $this->scopes = array();
+		$this->clientName = false;
+    }
+
+    /**
+     * If the user of this token should be allowed to accept incoming
+     * connections then configure the TwilioCapability through this method and
+     * specify the client name.
+     *
+     * @param $clientName
+     */
+    public function allowClientIncoming($clientName)
+    {
+
+        // clientName must be a non-zero length alphanumeric string
+        if (preg_match('/\W/', $clientName)) {
+            throw new InvalidArgumentException(
+                'Only alphanumeric characters allowed in client name.');
+        }
+
+        if (strlen($clientName) == 0) {
+            throw new InvalidArgumentException(
+                'Client name must not be a zero length string.');
+        }
+
+		$this->clientName = $clientName;
+        $this->allow('client', 'incoming',
+            array('clientName' => $clientName));
+    }
+
+    /**
+     * Allow the user of this token to make outgoing connections.
+     *
+     * @param $appSid the application to which this token grants access
+     * @param $appParams signed parameters that the user of this token cannot
+     *        overwrite.
+     */
+    public function allowClientOutgoing($appSid, array $appParams=array())
+    {
+        $this->allow('client', 'outgoing', array(
+            'appSid' => $appSid,
+            'appParams' => http_build_query($appParams, '', '&')));
+    }
+
+    /**
+     * Allow the user of this token to access their event stream.
+     *
+     * @param $filters key/value filters to apply to the event stream
+     */
+    public function allowEventStream(array $filters=array())
+    {
+        $this->allow('stream', 'subscribe', array(
+            'path' => '/2010-04-01/Events',
+            'params' => http_build_query($filters, '', '&'),
+        ));
+    }
+
+    /**
+     * Generates a new token based on the credentials and permissions that
+     * previously has been granted to this token.
+     *
+     * @param $ttl the expiration time of the token (in seconds). Default
+     *        value is 3600 (1hr)
+     * @return the newly generated token that is valid for $ttl seconds
+     */
+    public function generateToken($ttl = 3600)
+    {
+        $payload = array(
+            'scope' => array(),
+            'iss' => $this->accountSid,
+            'exp' => time() + $ttl,
+        );
+        $scopeStrings = array();
+
+        foreach ($this->scopes as $scope) {
+			if ($scope->privilege == "outgoing" && $this->clientName)
+				$scope->params["clientName"] = $this->clientName;
+            $scopeStrings[] = $scope->toString();
+        }
+
+        $payload['scope'] = implode(' ', $scopeStrings);
+        return JWT::encode($payload, $this->authToken, 'HS256');
+    }
+
+    protected function allow($service, $privilege, $params) {
+        $this->scopes[] = new ScopeURI($service, $privilege, $params);
+    }
+}
+
+/**
+ * Scope URI implementation
+ *
+ * Simple way to represent configurable privileges in an OAuth
+ * friendly way. For our case, they look like this:
+ *
+ * scope:<service>:<privilege>?<params>
+ *
+ * For example:
+ * scope:client:incoming?name=jonas
+ *
+ * @author Jeff Lindsay <jeff.lindsay@twilio.com>
+ */
+class ScopeURI
+{
+    public $service;
+    public $privilege;
+    public $params;
+
+    public function __construct($service, $privilege, $params = array())
+    {
+        $this->service = $service;
+        $this->privilege = $privilege;
+        $this->params = $params;
+    }
+
+    public function toString()
+    {
+        $uri = "scope:{$this->service}:{$this->privilege}";
+        if (count($this->params)) {
+            $uri .= "?".http_build_query($this->params, '', '&');
+        }
+        return $uri;
+    }
+
+    /**
+     * Parse a scope URI into a ScopeURI object
+     *
+     * @param string    $uri  The scope URI
+     * @return ScopeURI The parsed scope uri
+     */
+    public static function parse($uri)
+    {
+        if (strpos($uri, 'scope:') !== 0) {
+            throw new UnexpectedValueException(
+                'Not a scope URI according to scheme');
+        }
+
+        $parts = explode('?', $uri, 1);
+        $params = null;
+
+        if (count($parts) > 1) {
+            parse_str($parts[1], $params);
+        }
+
+        $parts = explode(':', $parts[0], 2);
+
+        if (count($parts) != 3) {
+            throw new UnexpectedValueException(
+                'Not enough parts for scope URI');
+        }
+
+        list($scheme, $service, $privilege) = $parts;
+        return new ScopeURI($service, $privilege, $params);
+    }
+
+}
+
+/**
+ * JSON Web Token implementation
+ *
+ * Minimum implementation used by Realtime auth, based on this spec:
+ * http://self-issued.info/docs/draft-jones-json-web-token-01.html.
+ *
+ * @author Neuman Vong <neuman@twilio.com>
+ */
+class JWT
+{
+    /**
+     * @param string      $jwt    The JWT
+     * @param string|null $key    The secret key
+     * @param bool        $verify Don't skip verification process
+     *
+     * @return object The JWT's payload as a PHP object
+     */
+    public static function decode($jwt, $key = null, $verify = true)
+    {
+        $tks = explode('.', $jwt);
+        if (count($tks) != 3) {
+            throw new UnexpectedValueException('Wrong number of segments');
+        }
+        list($headb64, $payloadb64, $cryptob64) = $tks;
+        if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))
+        ) {
+            throw new UnexpectedValueException('Invalid segment encoding');
+        }
+        if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payloadb64))
+        ) {
+            throw new UnexpectedValueException('Invalid segment encoding');
+        }
+        $sig = JWT::urlsafeB64Decode($cryptob64);
+        if ($verify) {
+            if (empty($header->alg)) {
+                throw new DomainException('Empty algorithm');
+            }
+            if ($sig != JWT::sign("$headb64.$payloadb64", $key, $header->alg)) {
+                throw new UnexpectedValueException('Signature verification failed');
+            }
+        }
+        return $payload;
+    }
+
+    /**
+      * @param object|array $payload PHP object or array
+      * @param string       $key     The secret key
+      * @param string       $algo    The signing algorithm
+      *
+      * @return string A JWT
+      */
+    public static function encode($payload, $key, $algo = 'HS256')
+    {
+        $header = array('typ' => 'JWT', 'alg' => $algo);
+
+        $segments = array();
+        $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
+        $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
+        $signing_input = implode('.', $segments);
+
+        $signature = JWT::sign($signing_input, $key, $algo);
+        $segments[] = JWT::urlsafeB64Encode($signature);
+
+        return implode('.', $segments);
+    }
+
+    /**
+     * @param string $msg    The message to sign
+     * @param string $key    The secret key
+     * @param string $method The signing algorithm
+     *
+     * @return string An encrypted message
+     */
+    public static function sign($msg, $key, $method = 'HS256')
+    {
+        $methods = array(
+            'HS256' => 'sha256',
+            'HS384' => 'sha384',
+            'HS512' => 'sha512',
+        );
+        if (empty($methods[$method])) {
+            throw new DomainException('Algorithm not supported');
+        }
+        return hash_hmac($methods[$method], $msg, $key, true);
+    }
+
+    /**
+     * @param string $input JSON string
+     *
+     * @return object Object representation of JSON string
+     */
+    public static function jsonDecode($input)
+    {
+        $obj = json_decode($input);
+        if (function_exists('json_last_error') && $errno = json_last_error()) {
+            JWT::handleJsonError($errno);
+        }
+        else if ($obj === null && $input !== 'null') {
+            throw new DomainException('Null result with non-null input');
+        }
+        return $obj;
+    }
+
+    /**
+     * @param object|array $input A PHP object or array
+     *
+     * @return string JSON representation of the PHP object or array
+     */
+    public static function jsonEncode($input)
+    {
+        $json = json_encode($input);
+        if (function_exists('json_last_error') && $errno = json_last_error()) {
+            JWT::handleJsonError($errno);
+        }
+        else if ($json === 'null' && $input !== null) {
+            throw new DomainException('Null result with non-null input');
+        }
+        return $json;
+    }
+
+    /**
+     * @param string $input A base64 encoded string
+     *
+     * @return string A decoded string
+     */
+    public static function urlsafeB64Decode($input)
+    {
+        $padlen = 4 - strlen($input) % 4;
+        $input .= str_repeat('=', $padlen);
+        return base64_decode(strtr($input, '-_', '+/'));
+    }
+
+    /**
+     * @param string $input Anything really
+     *
+     * @return string The base64 encode of what you passed in
+     */
+    public static function urlsafeB64Encode($input)
+    {
+        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
+    }
+
+    /**
+     * @param int $errno An error number from json_last_error()
+     *
+     * @return void
+     */
+    private static function handleJsonError($errno)
+    {
+        $messages = array(
+            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
+            JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
+            JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
+        );
+        throw new DomainException(isset($messages[$errno])
+            ? $messages[$errno]
+            : 'Unknown JSON error: ' . $errno
+        );
+    }
+}

+ 3 - 0
inc/lib/call/Services/Twilio/HttpException.php

@@ -0,0 +1,3 @@
+<?php
+
+class Services_Twilio_HttpException extends ErrorException {}

+ 94 - 0
inc/lib/call/Services/Twilio/HttpStream.php

@@ -0,0 +1,94 @@
+<?php
+/**
+ * HTTP Stream version of the TinyHttp Client used to connect to Twilio
+ * services.
+ */
+
+class Services_Twilio_HttpStreamException extends ErrorException {}
+
+class Services_Twilio_HttpStream {
+
+    private $auth_header = null;
+    private $uri = null;
+    private $debug = false;
+    private static $default_options = array(
+        "http" => array(
+            "headers" => "",
+            "timeout" => 60,
+            "follow_location" => true,
+            "ignore_errors" => true,
+        ),
+        "ssl" => array(),
+    );
+    private $options = array();
+
+    public function __construct($uri = '', $kwargs = array()) {
+        $this->uri = $uri;
+        if (isset($kwargs['debug'])) {
+            $this->debug = true;
+        }
+        if (isset($kwargs['http_options'])) {
+            $this->options = $kwargs['http_options'] + self::$default_options;
+        } else {
+            $this->options = self::$default_options;
+        }
+    }
+
+    public function __call($name, $args) {
+        list($res, $req_headers, $req_body) = $args + array(0, array(), '');
+
+        $request_options = $this->options;
+        $url = $this->uri . $res;
+
+        if (isset($req_body) && strlen($req_body) > 0) {
+            $request_options['http']['content'] = $req_body;
+        }
+
+        foreach($req_headers as $key => $value) {
+            $request_options['http']['header'] .= sprintf("%s: %s\r\n", $key, $value);
+        }
+
+        if (isset($this->auth_header)) {
+            $request_options['http']['header'] .= $this->auth_header;
+        }
+
+        $request_options['http']['method'] = strtoupper($name);
+        $request_options['http']['ignore_errors'] = true;
+
+        if ($this->debug) {
+            error_log(var_export($request_options, true));
+        }
+        $ctx = stream_context_create($request_options);
+        $result = file_get_contents($url, false, $ctx);
+
+        if (false === $result) {
+            throw new Services_Twilio_HttpStreamException(
+                "Unable to connect to service");
+        }
+
+        $status_header = array_shift($http_response_header);
+        if (1 !== preg_match('#HTTP/\d+\.\d+ (\d+)#', $status_header, $matches)) {
+            throw new Services_Twilio_HttpStreamException(
+                "Unable to detect the status code in the HTTP result.");
+        }
+
+        $status_code = intval($matches[1]);
+        $response_headers = array();
+
+        foreach($http_response_header as $header) {
+            list($key, $val) = explode(":", $header);
+            $response_headers[trim($key)] = trim($val);
+        }
+
+        return array($status_code, $response_headers, $result);
+    }
+
+    public function authenticate($user, $pass) {
+        if (isset($user) && isset($pass)) {
+            $this->auth_header = sprintf("Authorization: Basic %s",
+                base64_encode(sprintf("%s:%s", $user, $pass)));
+        } else {
+            $this->auth_header = null;
+        }
+    }
+}

+ 84 - 0
inc/lib/call/Services/Twilio/InstanceResource.php

@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @category Services
+ * @package  Services_Twilio
+ * @author   Neuman Vong <neuman@twilio.com>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://pear.php.net/package/Services_Twilio
+ */
+
+/**
+ * Abstraction of an instance resource from the Twilio API.
+ */
+abstract class Services_Twilio_InstanceResource extends Services_Twilio_Resource {
+
+    /**
+     * Make a request to the API to update an instance resource
+     *
+     * :param mixed $params: An array of updates, or a property name
+     * :param mixed $value:  A value with which to update the resource
+     *
+     * :rtype: null
+     * :throws: a :php:class:`RestException <Services_Twilio_RestException>` if
+     *      the update fails.
+     */
+    public function update($params, $value = null)
+    {
+        if (!is_array($params)) {
+            $params = array($params => $value);
+        }
+        $decamelizedParams = $this->client->createData($this->uri, $params);
+        $this->updateAttributes($decamelizedParams);
+    }
+
+    /*
+     * Add all properties from an associative array (the JSON response body) as
+     * properties on this instance resource, except the URI
+     *
+     * :param stdClass $params: An object containing all of the parameters of
+     *      this instance
+     * :return: Nothing, this is purely side effecting
+     * :rtype: null
+     */
+    public function updateAttributes($params) {
+        unset($params->uri);
+        foreach ($params as $name => $value) {
+            $this->$name = $value;
+        }
+    }
+
+    /**
+     * Get the value of a property on this resource.
+     *
+     * Instead of defining all of the properties of an object directly, we rely
+     * on the API to tell us which properties an object has. This method will
+     * query the API to retrieve a property for an object, if it is not already
+     * set on the object.
+     *
+     * If the call is to a subresource, eg ``$client->account->messages``, no
+     * request is made.
+     *
+     * To help with lazy HTTP requests, we don't actually retrieve an object
+     * from the API unless you really need it. Hence, this function may make API
+     * requests even if the property you're requesting isn't available on the
+     * resource.
+     *
+     * :param string $key: The property name
+     *
+     * :return mixed: Could be anything.
+     * :throws: a :php:class:`RestException <Services_Twilio_RestException>` if
+     *      the update fails.
+     */
+    public function __get($key)
+    {
+        if ($subresource = $this->getSubresources($key)) {
+            return $subresource;
+        }
+        if (!isset($this->$key)) {
+            $params = $this->client->retrieveData($this->uri);
+            $this->updateAttributes($params);
+        }
+        return $this->$key;
+    }
+}

+ 203 - 0
inc/lib/call/Services/Twilio/ListResource.php

@@ -0,0 +1,203 @@
+<?php
+
+/**
+ * @author   Neuman Vong neuman@twilio.com
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://pear.php.net/package/Services_Twilio
+ */
+
+/**
+ * Abstraction of a list resource from the Twilio API.
+ *
+ * The list resource implements the `IteratorAggregate
+ * <http://php.net/manual/en/class.iteratoraggregate.php>`_ and the `Countable
+ * <http://php.net/manual/en/class.countable.php>`_ interfaces.
+ *
+ */
+abstract class Services_Twilio_ListResource extends Services_Twilio_Resource
+    implements IteratorAggregate, Countable
+{
+
+    public function __construct($client, $uri) {
+        $name = $this->getResourceName(true);
+        /*
+         * By default trim the 's' from the end of the list name to get the
+         * instance name (ex Accounts -> Account). This behavior can be
+         * overridden by child classes if the rule doesn't work.
+         */
+        if (!isset($this->instance_name)) {
+            $this->instance_name = "Services_Twilio_Rest_" . rtrim($name, 's');
+        }
+
+        parent::__construct($client, $uri);
+    }
+
+    /**
+     * Gets a resource from this list.
+     *
+     * :param string $sid: The resource SID
+     * :return: The resource
+     * :rtype: :php:class:`InstanceResource <Services_Twilio_InstanceResource>`
+     */
+    public function get($sid) {
+        $instance = new $this->instance_name(
+            $this->client, $this->uri . "/$sid"
+        );
+        // XXX check if this is actually a sid in all cases.
+        $instance->sid = $sid;
+        return $instance;
+    }
+
+    /**
+     * Construct an :php:class:`InstanceResource
+     * <Services_Twilio_InstanceResource>` with the specified params.
+     *
+     * :param array $params: usually a JSON HTTP response from the API
+     * :return: An instance with properties
+     *      initialized to the values in the params array.
+     * :rtype: :php:class:`InstanceResource <Services_Twilio_InstanceResource>`
+     */
+    public function getObjectFromJson($params, $idParam = "sid")
+    {
+        if (isset($params->{$idParam})) {
+            $uri = $this->uri . "/" . $params->{$idParam};
+        } else {
+            $uri = $this->uri;
+        }
+        return new $this->instance_name($this->client, $uri, $params);
+    }
+
+    /**
+     * Deletes a resource from this list.
+     *
+     * :param string $sid: The resource SID
+     * :rtype: null
+     */
+    public function delete($sid, $params = array())
+    {
+        $this->client->deleteData($this->uri . '/' . $sid, $params);
+    }
+
+    /**
+     * Create a resource on the list and then return its representation as an
+     * InstanceResource.
+     *
+     * :param array $params: The parameters with which to create the resource
+     *
+     * :return: The created resource
+     * :rtype: :php:class:`InstanceResource <Services_Twilio_InstanceResource>`
+     */
+    protected function _create($params)
+    {
+        $params = $this->client->createData($this->uri, $params);
+        /* Some methods like verified caller ID don't return sids. */
+        if (isset($params->sid)) {
+            $resource_uri = $this->uri . '/' . $params->sid;
+        } else {
+            $resource_uri = $this->uri;
+        }
+        return new $this->instance_name($this->client, $resource_uri, $params);
+    }
+
+    /**
+     * Returns a page of :php:class:`InstanceResources
+     * <Services_Twilio_InstanceResource>` from this list.
+     *
+     * :param int    $page: The start page
+     * :param int    $size: Number of items per page
+     * :param array  $filters: Optional filters
+     * :param string $deep_paging_uri: if provided, the $page and $size
+     *      parameters will be ignored and this URI will be requested directly.
+     *
+     * :return: A page of resources
+     * :rtype: :php:class:`Services_Twilio_Page`
+     */
+    public function getPage(
+        $page = 0, $size = 50, $filters = array(), $deep_paging_uri = null
+    ) {
+        $list_name = $this->getResourceName();
+        if ($deep_paging_uri !== null) {
+            $page = $this->client->retrieveData($deep_paging_uri, array(), true);
+        } else {
+            $page = $this->client->retrieveData($this->uri, array(
+                'Page' => $page,
+                'PageSize' => $size,
+            ) + $filters);
+        }
+
+        /* create a new PHP object for each json obj in the api response. */
+        $page->$list_name = array_map(
+            array($this, 'getObjectFromJson'),
+            $page->$list_name
+        );
+        if (isset($page->next_page_uri)) {
+            $next_page_uri = $page->next_page_uri;
+        } else {
+            $next_page_uri = null;
+        }
+        return new Services_Twilio_Page($page, $list_name, $next_page_uri);
+    }
+
+    /**
+     * Get the total number of instances for this list.
+     *
+     * This will make one HTTP request to retrieve the total, every time this
+     * method is called.
+     *
+     * If the total is not set, or an Exception was thrown, returns 0
+     *
+     * :return: The total number of instance members
+     * :rtype: integer
+     */
+    public function count() {
+        try {
+            $page = $this->getPage(0, 1);
+            return $page ? (int)$page->total : 0;
+        } catch (Exception $e) {
+            return 0;
+        }
+    }
+
+
+    /**
+     * Returns an iterable list of
+     * :php:class:`instance resources <Services_Twilio_InstanceResource>`.
+     *
+     * :param int   $page: The start page
+     * :param int   $size: Number of items per page
+     * :param array $filters: Optional filters.
+     *      The filter array can accept full datetimes when StartTime or DateCreated
+     *      are used. Inequalities should be within the key portion of the array and
+     *      multiple filter parameters can be combined for more specific searches.
+     *
+     *      .. code-block:: php
+     *
+     *          array('DateCreated>' => '2011-07-05 08:00:00', 'DateCreated<' => '2011-08-01')
+     *
+     *      .. code-block:: php
+     *
+     *          array('StartTime<' => '2011-07-05 08:00:00')
+     *
+     * :return: An iterator
+     * :rtype: :php:class:`Services_Twilio_AutoPagingIterator`
+     */
+    public function getIterator(
+        $page = 0, $size = 50, $filters = array()
+    ) {
+        return new Services_Twilio_AutoPagingIterator(
+            array($this, 'getPageGenerator'), $page, $size, $filters
+        );
+    }
+
+    /**
+     * Retrieve a new page of API results, and update iterator parameters. This
+     * function is called by the paging iterator to retrieve a new page and
+     * shouldn't be called directly.
+     */
+    public function getPageGenerator(
+        $page, $size, $filters = array(), $deep_paging_uri = null
+    ) {
+        return $this->getPage($page, $size, $filters, $deep_paging_uri);
+    }
+}
+

+ 35 - 0
inc/lib/call/Services/Twilio/NumberType.php

@@ -0,0 +1,35 @@
+<?php
+
+class Services_Twilio_NumberType extends Services_Twilio_ListResource
+{
+    public function getResourceName($camelized = false) {
+        $this->instance_name = 'Services_Twilio_Rest_IncomingPhoneNumber';
+        return $camelized ? 'IncomingPhoneNumbers' : 'incoming_phone_numbers';
+    }
+
+    /**
+     * Purchase a new phone number.
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $marlosBurner = '+14105551234';
+     *      $client->account->incoming_phone_numbers->local->purchase($marlosBurner);
+     *
+     * :param string $phone_number: The phone number to purchase
+     * :param array $params: An optional array of parameters to pass along with
+     *      the request (to configure the phone number)
+     */
+    public function purchase($phone_number, array $params = array()) {
+        $postParams = array(
+            'PhoneNumber' => $phone_number
+        );
+        return $this->create($postParams + $params);
+    }
+
+    public function create(array $params = array()) {
+        return parent::_create($params);
+    }
+
+}

+ 68 - 0
inc/lib/call/Services/Twilio/Page.php

@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * A representation of a page of resources.
+ *
+ * @category Services
+ * @package  Services_Twilio
+ * @author   Neuman Vong <neuman@twilio.com>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://pear.php.net/package/Services_Twilio
+ */ 
+class Services_Twilio_Page
+    implements IteratorAggregate
+{
+
+    /**
+     * The item list.
+     *
+     * @var array $items
+     */
+    protected $items;
+
+    /**
+     * Constructs a page.
+     *
+     * @param object $page The page object
+     * @param string $name The key of the item list
+     */
+    public function __construct($page, $name, $next_page_uri = null)
+    {
+        $this->page = $page;
+        $this->items = $page->{$name};
+        $this->next_page_uri = $next_page_uri;
+    }
+
+    /**
+     * The item list of the page.
+     *
+     * @return array A list of instance resources
+     */
+    public function getItems()
+    {
+        return $this->items;
+    }
+
+    /**
+     * Magic method to allow retrieving the properties of the wrapped page.
+     *
+     * @param string $prop The property name
+     *
+     * @return mixed Could be anything
+     */
+    public function __get($prop)
+    {
+        return $this->page->$prop;
+    }
+
+    /**
+     * Implementation of IteratorAggregate::getIterator().
+     *
+     * @return Traversable
+     */
+    public function getIterator()
+    {
+        return $this->getItems();
+    }
+}
+

+ 41 - 0
inc/lib/call/Services/Twilio/PartialApplicationHelper.php

@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Helper class to wrap an object with a modified interface created by
+ * a partial application of its existing methods.
+ *
+ * @category Services
+ * @package  Services_Twilio
+ * @author   Neuman Vong <neuman@twilio.com>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://pear.php.net/package/Services_Twilio
+ */ 
+class Services_Twilio_PartialApplicationHelper
+{
+    private $callbacks;
+
+    public function __construct()
+    {
+        $this->callbacks = array();
+    }
+
+    public function set($method, $callback, array $args)
+    {
+        if (!is_callable($callback)) {
+            return FALSE;
+        }
+        $this->callbacks[$method] = array($callback, $args);
+    }
+
+    public function __call($method, $args)
+    {
+        if (!isset($this->callbacks[$method])) {
+            throw new Exception("Method not found: $method");
+        }
+        list($callback, $cb_args) = $this->callbacks[$method];
+        return call_user_func_array(
+            $callback,
+            array_merge($cb_args, $args)
+        );
+    }
+}

+ 36 - 0
inc/lib/call/Services/Twilio/RequestValidator.php

@@ -0,0 +1,36 @@
+<?php
+
+class Services_Twilio_RequestValidator
+{
+
+    protected $AuthToken;
+
+    function __construct($token)
+    {
+        $this->AuthToken = $token;
+    }
+    
+    public function computeSignature($url, $data = array())
+    {
+        // sort the array by keys
+        ksort($data);
+
+        // append them to the data string in order
+        // with no delimiters
+        foreach($data as $key => $value)
+            $url .= "$key$value";
+            
+        // This function calculates the HMAC hash of the data with the key
+        // passed in
+        // Note: hash_hmac requires PHP 5 >= 5.1.2 or PECL hash:1.1-1.5
+        // Or http://pear.php.net/package/Crypt_HMAC/
+        return base64_encode(hash_hmac("sha1", $url, $this->AuthToken, true));
+    }
+
+    public function validate($expectedSignature, $url, $data = array())
+    {
+        return $this->computeSignature($url, $data)
+            == $expectedSignature;
+    }
+
+}

+ 134 - 0
inc/lib/call/Services/Twilio/Resource.php

@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * Abstraction of a Twilio resource.
+ *
+ * @category Services
+ * @package  Services_Twilio
+ * @author   Neuman Vong <neuman@twilio.com>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://pear.php.net/package/Services_Twilio
+ */
+abstract class Services_Twilio_Resource {
+    protected $subresources;
+
+    public function __construct($client, $uri, $params = array())
+    {
+        $this->subresources = array();
+        $this->client = $client;
+
+        foreach ($params as $name => $param) {
+            $this->$name = $param;
+        }
+
+        $this->uri = $uri;
+        $this->init($client, $uri);
+    }
+
+    protected function init($client, $uri)
+    {
+        // Left empty for derived classes to implement
+    }
+
+    public function getSubresources($name = null) {
+        if (isset($name)) {
+            return isset($this->subresources[$name])
+                ? $this->subresources[$name]
+                : null;
+        }
+        return $this->subresources;
+    }
+
+    protected function setupSubresources()
+    {
+        foreach (func_get_args() as $name) {
+            $constantized = ucfirst(self::camelize($name));
+            $type = "Services_Twilio_Rest_" . $constantized;
+            $this->subresources[$name] = new $type(
+                $this->client, $this->uri . "/$constantized"
+            );
+        }
+    }
+
+    /*
+     * Get the resource name from the classname
+     *
+     * Ex: Services_Twilio_Rest_Accounts -> Accounts
+     *
+     * @param boolean $camelized Whether to return camel case or not
+     */
+    public function getResourceName($camelized = false)
+    {
+        $name = get_class($this);
+        $parts = explode('_', $name);
+        $basename = end($parts);
+        if ($camelized) {
+            return $basename;
+        } else {
+            return self::decamelize($basename);
+        }
+    }
+
+    public static function decamelize($word)
+    {
+        $callback = create_function('$matches',
+            'return strtolower(strlen("$matches[1]") ? "$matches[1]_$matches[2]" : "$matches[2]");');
+
+        return preg_replace_callback(
+            '/(^|[a-z])([A-Z])/',
+            $callback,
+            $word
+        );
+    }
+
+    /**
+     * Return camelized version of a word
+     * Examples: sms_messages => SMSMessages, calls => Calls,
+     * incoming_phone_numbers => IncomingPhoneNumbers
+     *
+     * @param string $word The word to camelize
+     * @return string
+     */
+    public static function camelize($word) {
+        $callback = create_function('$matches', 'return strtoupper("$matches[2]");');
+
+        return preg_replace_callback('/(^|_)([a-z])/',
+            $callback,
+            $word);
+    }
+
+    /**
+     * Get the value of a property on this resource.
+     *
+     * @param string $key The property name
+     * @return mixed Could be anything.
+     */
+    public function __get($key) {
+        if ($subresource = $this->getSubresources($key)) {
+            return $subresource;
+        }
+        return $this->$key;
+    }
+
+    /**
+     * Print a JSON representation of this object. Strips the HTTP client
+     * before returning.
+     *
+     * Note, this should mainly be used for debugging, and is not guaranteed
+     * to correspond 1:1 with the JSON API output.
+     *
+     * Note that echoing an object before an HTTP request has been made to
+     * "fill in" its properties may return an empty object
+     */
+    public function __toString() {
+        $out = array();
+        foreach ($this as $key => $value) {
+            if ($key !== 'client' && $key !== 'subresources') {
+                $out[$key] = $value;
+            }
+        }
+        return json_encode($out, true);
+    }
+
+}
+

+ 35 - 0
inc/lib/call/Services/Twilio/Rest/Account.php

@@ -0,0 +1,35 @@
+<?php
+
+class Services_Twilio_Rest_Account extends Services_Twilio_InstanceResource {
+
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'applications',
+            'available_phone_numbers',
+            'outgoing_caller_ids',
+            'calls',
+            'conferences',
+            'incoming_phone_numbers',
+            'media',
+            'messages',
+            'notifications',
+            'outgoing_callerids',
+            'recordings',
+            'sms_messages',
+            'short_codes',
+            'tokens',
+            'transcriptions',
+            'connect_apps',
+            'authorized_connect_apps',
+            'usage_records',
+            'usage_triggers',
+            'queues',
+            'sip',
+            'addresses'
+        );
+
+        $this->sandbox = new Services_Twilio_Rest_Sandbox(
+            $client, $uri . '/Sandbox'
+        );
+    }
+}

+ 25 - 0
inc/lib/call/Services/Twilio/Rest/Accounts.php

@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * For more information, see the `Account List Resource
+ * <http://www.twilio.com/docs/api/rest/account#list>`_ documentation.
+ */
+class Services_Twilio_Rest_Accounts extends Services_Twilio_ListResource {
+
+    /**
+     * Create a new subaccount.
+     *
+     * :param array $params: An array of parameters describing the new
+     *      subaccount. The ``$params`` array can contain the following keys:
+     *
+     *      *FriendlyName*
+     *          A description of this account, up to 64 characters long
+     *
+     * :returns: The new subaccount
+     * :rtype: :php:class:`Services_Twilio_Rest_Account`
+     *
+     */
+    public function create($params = array()) {
+        return parent::_create($params);
+    }
+}

+ 12 - 0
inc/lib/call/Services/Twilio/Rest/Address.php

@@ -0,0 +1,12 @@
+<?php
+
+class Services_Twilio_Rest_Address
+    extends Services_Twilio_InstanceResource
+{
+    protected function init($client, $uri)
+    {
+        $this->setupSubresources(
+            'dependent_phone_numbers'
+        );
+    }
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Addresses.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Addresses
+    extends Services_Twilio_ListResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Application.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Application
+    extends Services_Twilio_InstanceResource
+{
+}

+ 12 - 0
inc/lib/call/Services/Twilio/Rest/Applications.php

@@ -0,0 +1,12 @@
+<?php
+
+class Services_Twilio_Rest_Applications
+    extends Services_Twilio_ListResource
+{
+    public function create($name, array $params = array())
+    {
+        return parent::_create(array(
+            'FriendlyName' => $name
+        ) + $params);
+    }
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/AuthorizedConnectApp.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_AuthorizedConnectApp
+    extends Services_Twilio_InstanceResource
+{
+}

+ 10 - 0
inc/lib/call/Services/Twilio/Rest/AuthorizedConnectApps.php

@@ -0,0 +1,10 @@
+<?php
+
+class Services_Twilio_Rest_AuthorizedConnectApps
+    extends Services_Twilio_ListResource
+{
+   public function create($name, array $params = array())
+    {
+        throw new BadMethodCallException('Not allowed');
+    }
+}

+ 7 - 0
inc/lib/call/Services/Twilio/Rest/AvailablePhoneNumber.php

@@ -0,0 +1,7 @@
+<?php
+
+class Services_Twilio_Rest_AvailablePhoneNumber
+    extends Services_Twilio_InstanceResource
+{
+}
+

+ 54 - 0
inc/lib/call/Services/Twilio/Rest/AvailablePhoneNumbers.php

@@ -0,0 +1,54 @@
+<?php
+
+class Services_Twilio_Rest_AvailablePhoneNumbers
+    extends Services_Twilio_ListResource
+{
+    public function getLocal($country) {
+        $curried = new Services_Twilio_PartialApplicationHelper();
+        $curried->set(
+            'getList',
+            array($this, 'getList'),
+            array($country, 'Local')
+        );
+        return $curried;
+    }
+    public function getTollFree($country) {
+        $curried = new Services_Twilio_PartialApplicationHelper();
+        $curried->set(
+            'getList',
+            array($this, 'getList'),
+            array($country, 'TollFree')
+        );
+        return $curried;
+    }
+
+    public function getMobile($country)
+    {
+        $curried = new Services_Twilio_PartialApplicationHelper();
+        $curried->set(
+            'getList',
+            array($this, 'getList'),
+            array($country, 'Mobile')
+        );
+        return $curried;
+    }
+
+    /**
+     * Get a list of available phone numbers.
+     *
+     * @param string $country The 2-digit country code you'd like to search for
+     *    numbers e.g. ('US', 'CA', 'GB')
+     * @param string $type The type of number ('Local', 'TollFree', or 'Mobile')
+     * @return object The object representation of the resource
+     */
+    public function getList($country, $type, array $params = array())
+    {
+        return $this->client->retrieveData($this->uri . "/$country/$type", $params);
+    }
+
+    public function getResourceName($camelized = false) {
+        // You can't page through the list of available phone numbers.
+        $this->instance_name = 'Services_Twilio_Rest_AvailablePhoneNumber';
+        return $camelized ? 'Countries' : 'countries';
+    }
+}

+ 106 - 0
inc/lib/call/Services/Twilio/Rest/Call.php

@@ -0,0 +1,106 @@
+<?php
+
+/**
+ *   For more information, see the `Call Instance Resource <http://www.twilio.com/docs/api/rest/call#instance>`_ documentation.
+ *
+ *   .. php:attr:: sid
+ *
+ *      A 34 character string that uniquely identifies this resource.
+ *
+ *   .. php:attr:: parent_call_sid
+ *
+ *      A 34 character string that uniquely identifies the call that created this leg.
+ *
+ *   .. php:attr:: date_created
+ *
+ *      The date that this resource was created, given as GMT in RFC 2822 format.
+ *
+ *   .. php:attr:: date_updated
+ *
+ *      The date that this resource was last updated, given as GMT in RFC 2822 format.
+ *
+ *   .. php:attr:: account_sid
+ *
+ *      The unique id of the Account responsible for creating this call.
+ *
+ *   .. php:attr:: to
+ *
+ *      The phone number that received this call. e.g., +16175551212 (E.164 format)
+ *
+ *   .. php:attr:: from
+ *
+ *      The phone number that made this call. e.g., +16175551212 (E.164 format)
+ *
+ *   .. php:attr:: phone_number_sid
+ *
+ *      If the call was inbound, this is the Sid of the IncomingPhoneNumber that
+ *      received the call. If the call was outbound, it is the Sid of the
+ *      OutgoingCallerId from which the call was placed.
+ *
+ *   .. php:attr:: status
+ *
+ *      A string representing the status of the call. May be `QUEUED`, `RINGING`,
+ *      `IN-PROGRESS`, `COMPLETED`, `FAILED`, `BUSY` or `NO_ANSWER`.
+ *
+ *   .. php:attr:: stat_time
+ *
+ *      The start time of the call, given as GMT in RFC 2822 format. Empty if the call has not yet been dialed.
+ *
+ *   .. php:attr:: end_time
+ *
+ *      The end time of the call, given as GMT in RFC 2822 format. Empty if the call did not complete successfully.
+ *
+ *   .. php:attr:: duration
+ *
+ *      The length of the call in seconds. This value is empty for busy, failed, unanswered or ongoing calls.
+ *
+ *   .. php:attr:: price
+ *
+ *      The charge for this call in USD. Populated after the call is completed. May not be immediately available.
+ *
+ *   .. php:attr:: direction
+ *
+ *         A string describing the direction of the call. inbound for inbound
+ *         calls, outbound-api for calls initiated via the REST API or
+ *         outbound-dial for calls initiated by a <Dial> verb.
+ *
+ *   .. php:attr:: answered_by
+ *
+ *      If this call was initiated with answering machine detection, either human or machine. Empty otherwise.
+ *
+ *   .. php:attr:: forwarded_from
+ *
+ *        If this call was an incoming call forwarded from another number, the
+ *        forwarding phone number (depends on carrier supporting forwarding).
+ *        Empty otherwise.
+ *
+ *   .. php:attr:: caller_name
+ *
+ *      If this call was an incoming call from a phone number with Caller ID Lookup enabled, the caller's name. Empty otherwise.
+ */
+class Services_Twilio_Rest_Call extends Services_Twilio_InstanceResource {
+
+    /**
+     * Hang up the call
+     */
+    public function hangup() {
+        $this->update('Status', 'completed');
+    }
+
+    /**
+     * Redirect the call to a new URL
+     *
+     * :param string $url: the new URL to retrieve call flow from.
+     */
+    public function route($url) {
+        $this->update('Url', $url);
+    }
+
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'notifications',
+            'recordings',
+            'feedback'
+        );
+    }
+}

+ 77 - 0
inc/lib/call/Services/Twilio/Rest/Calls.php

@@ -0,0 +1,77 @@
+<?php
+
+class Services_Twilio_Rest_Calls
+    extends Services_Twilio_ListResource
+{
+
+    function init($client, $uri)
+    {
+        $this->setupSubresources(
+            'feedback_summary'
+        );
+    }
+
+    public static function isApplicationSid($value)
+    {
+        return strlen($value) == 34
+            && !(strpos($value, "AP") === false);
+    }
+
+    public function create($from, $to, $url, array $params = array())
+    {
+
+        $params["To"] = $to;
+        $params["From"] = $from;
+
+        if (self::isApplicationSid($url)) {
+            $params["ApplicationSid"] = $url;
+        } else {
+            $params["Url"] = $url;
+        }
+
+        return parent::_create($params);
+    }
+
+    /**
+     * Create a feedback for a call.
+     *
+     * @param $callSid
+     * @param $qualityScore
+     * @param array $issue
+     * @return Services_Twilio_Rest_Feedback
+     */
+    public function createFeedback($callSid, $qualityScore, array $issue = array())
+    {
+        $params["QualityScore"] = $qualityScore;
+        $params["Issue"] = $issue;
+
+        $feedbackUri = $this->uri . '/' . $callSid . '/Feedback';
+
+        $response = $this->client->createData($feedbackUri, $params);
+        return new Services_Twilio_Rest_Feedback($this->client, $feedbackUri, $response);
+    }
+
+    /**
+     * Delete a feedback for a call.
+     *
+     * @param $callSid
+     */
+    public function deleteFeedback($callSid)
+    {
+        $feedbackUri = $this->uri . '/' . $callSid . '/Feedback';
+        $this->client->deleteData($feedbackUri);
+    }
+
+    /**
+     * Get a feedback for a call.
+     *
+     * @param $callSid
+     * @return Services_Twilio_Rest_Feedback
+     */
+    public function getFeedback($callSid)
+    {
+        $feedbackUri = $this->uri . '/' . $callSid . '/Feedback';
+        $response = $this->client->retrieveData($feedbackUri);
+        return new Services_Twilio_Rest_Feedback($this->client, $feedbackUri, $response);
+    }
+}

+ 12 - 0
inc/lib/call/Services/Twilio/Rest/Conference.php

@@ -0,0 +1,12 @@
+<?php
+
+class Services_Twilio_Rest_Conference
+    extends Services_Twilio_InstanceResource
+{
+    protected function init($client, $uri)
+    {
+        $this->setupSubresources(
+            'participants'
+        );
+    }
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Conferences.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Conferences
+    extends Services_Twilio_ListResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/ConnectApp.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_ConnectApp
+    extends Services_Twilio_InstanceResource
+{
+}

+ 10 - 0
inc/lib/call/Services/Twilio/Rest/ConnectApps.php

@@ -0,0 +1,10 @@
+<?php
+
+class Services_Twilio_Rest_ConnectApps
+    extends Services_Twilio_ListResource
+{
+    public function create($name, array $params = array())
+    {
+        throw new BadMethodCallException('Not allowed');
+    }
+}

+ 30 - 0
inc/lib/call/Services/Twilio/Rest/Credential.php

@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * A single Credential
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date the Credential was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date the Credential was updated
+ *
+ * .. php:attr:: sid
+ *
+ *    A 34 character string that identifies this object
+ *
+ * .. php:attr:: account_sid
+ *
+ *    The account that created this credential
+ *
+ * .. php:attr:: username
+ *
+ *    The username of this Credential object
+ *
+ * .. php:attr:: uri
+ *
+ *    The uri of this Credential object
+ */
+class Services_Twilio_Rest_Credential extends Services_Twilio_InstanceResource { }

+ 42 - 0
inc/lib/call/Services/Twilio/Rest/CredentialList.php

@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * A single CredentialList
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date the credential list was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date the credential list was updated
+ *
+ * .. php:attr:: sid
+ *
+ *    A 34 character string that identifies this object
+ *
+ * .. php:attr:: account_sid
+ *
+ *    The account that created the credential list
+ *
+ * .. php:attr:: friendly_name
+ *
+ *    The friendly name of the credential list
+ *
+ * .. php:attr:: uri
+ *
+ *    The uri of the credential list
+ *
+ * .. php:attr:: subresource_uris
+ *
+ *    The subresources associated with this credential list (Credentials)
+ */
+
+class Services_Twilio_Rest_CredentialList extends Services_Twilio_InstanceResource {
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'credentials'
+        );
+    }
+}
+

+ 37 - 0
inc/lib/call/Services/Twilio/Rest/CredentialListMapping.php

@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * A single CredentialListMapping
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date this mapping was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date this mapping was updated
+ *
+ * .. php:attr:: sid
+ *
+ *    The sid of this mapping
+ *
+ * .. php:attr:: friendly_name
+ *
+ *    The friendly name of this mapping
+ *
+ * .. php:attr:: uri
+ *
+ *    The uri of this mapping
+ *
+ * .. php:attr:: subresource_uris
+ *
+ *    The subresources associated with this mapping (Credentials)
+ */
+
+class Services_Twilio_Rest_CredentialListMapping extends Services_Twilio_InstanceResource {
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'credentials'
+        );
+    }
+}

+ 24 - 0
inc/lib/call/Services/Twilio/Rest/CredentialListMappings.php

@@ -0,0 +1,24 @@
+<?php
+
+class Services_Twilio_Rest_CredentialListMappings extends Services_Twilio_SIPListResource {
+
+    /**
+     * Creates a new CredentialListMapping instance
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $client->account->sip->domains->get('SDXXX')->credential_list_mappings->create("CLXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
+     *
+     * :param string $credential_list_sid: the sid of the CredentialList you're adding to this domain.
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API.
+     */
+    public function create($credential_list_sid, $params = array()) {
+        return parent::_create(array(
+            'CredentialListSid' => $credential_list_sid,
+        ) + $params);
+    }
+}
+

+ 24 - 0
inc/lib/call/Services/Twilio/Rest/CredentialLists.php

@@ -0,0 +1,24 @@
+<?php
+
+class Services_Twilio_Rest_CredentialLists extends Services_Twilio_SIPListResource {
+
+    /**
+     * Creates a new CredentialList instance
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $client->account->sip->credential_lists->create("MyFriendlyName");
+     *
+     * :param string $friendly_name: the friendly name of this credential list
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API.
+     */
+    public function create($friendly_name, $params = array()) {
+        return parent::_create(array(
+            'FriendlyName' => $friendly_name,
+        ) + $params);
+    }
+
+}

+ 28 - 0
inc/lib/call/Services/Twilio/Rest/Credentials.php

@@ -0,0 +1,28 @@
+<?php
+
+class Services_Twilio_Rest_Credentials extends Services_Twilio_SIPListResource {
+
+    /**
+     * Creates a new Credential instance
+     *
+     * Example usage:
+     *
+     *  .. code-block:: php
+     *
+     *      $client->account->sip->credential_lists->get('CLXXX')->credentials->create(
+     *          "AwesomeUsername", "SuperSecretPassword",
+     *      );
+     *
+     * :param string $username: the username for the new Credential object
+     * :param string $password: the password for the new Credential object
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API.
+     */
+    public function create($username, $password, $params = array()) {
+        return parent::_create(array(
+            'Username' => $username,
+            'Password' => $password,
+        ) + $params);
+    }
+
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/DependentPhoneNumber.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_DependentPhoneNumber
+    extends Services_Twilio_InstanceResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/DependentPhoneNumbers.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_DependentPhoneNumbers
+    extends Services_Twilio_ListResource
+{
+}

+ 70 - 0
inc/lib/call/Services/Twilio/Rest/Domain.php

@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * A single Domain
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date the domain was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date the domain was updated
+ *
+ * .. php:attr:: sid
+ *
+ *    A 34 character string that identifies this object
+ *
+ * .. php:attr:: account_sid
+ *
+ *    The account that created the domain
+ *
+ * .. php:attr:: friendly_name
+ *
+ *    The friendly name of the domain
+ *
+ * .. php:attr:: domain_name
+ *
+ *    The *.sip.twilio domain for the domain
+ *
+ * .. php:attr:: auth_type
+ *
+ *    The auth type used for the domain
+ *
+ * .. php:attr:: voice_url
+ *
+ *    The voice url for the domain
+ *
+ * .. php:attr:: voice_fallback_url
+ *
+ *    The voice fallback url for the domain
+ *
+ * .. php:attr:: voice_fallback_method
+ *
+ *    The voice fallback method for the domain
+ *
+ * .. php:attr:: voice_status_callback_url
+ *
+ *    The voice status callback url for the domain
+ *
+ * .. php:attr:: voice_status_callback_method
+ *
+ *    The voice status_callback_method for the domain
+ *
+ * .. php:attr:: uri
+ *
+ *    The uri of the domain
+ *
+ * .. php:attr:: subresource_uris
+ *
+ *    The subresources associated with this domain (IpAccessControlListMappings, CredentialListMappings)
+ *
+ */
+class Services_Twilio_Rest_Domain extends Services_Twilio_InstanceResource {
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'ip_access_control_list_mappings',
+            'credential_list_mappings'
+        );
+    }
+}

+ 28 - 0
inc/lib/call/Services/Twilio/Rest/Domains.php

@@ -0,0 +1,28 @@
+<?php
+
+class Services_Twilio_Rest_Domains extends Services_Twilio_SIPListResource {
+
+    /**
+     * Creates a new Domain instance
+     *
+     * Example usage:
+     *
+     *  .. code-block:: php
+     *
+     *      $client->account->sip->domains->create(
+     *          "MyFriendlyName", "MyDomainName"
+     *      );
+     *
+     * :param string $friendly_name: the friendly name of this domain
+     * :param string $domain_name: the domain name for this domain
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API.
+     */
+    public function create($friendly_name, $domain_name, $params = array()) {
+        return parent::_create(array(
+            'FriendlyName' => $friendly_name,
+            'DomainName' => $domain_name,
+        ) + $params);
+    }
+}
+

+ 34 - 0
inc/lib/call/Services/Twilio/Rest/Feedback.php

@@ -0,0 +1,34 @@
+<?php
+
+class Services_Twilio_Rest_Feedback extends Services_Twilio_InstanceResource {
+
+    public function __construct($client, $uri, $params = array()) {
+        $this->instance_name = "Services_Twilio_Rest_Feedback";
+        return parent::__construct($client, $uri, $params);
+    }
+
+    /**
+     * Create feedback for the parent call
+     */
+    public function create(array $params = array()) {
+        $params = $this->client->createData($this->uri, $params);
+        return new $this->instance_name($this->client, $this->uri, $params);
+    }
+
+    /**
+     * Delete feedback for the parent call
+     */
+    public function delete() {
+        $this->client->deleteData($this->uri);
+    }
+
+    /**
+     * Fetch the feedback for the parent call
+     */
+    public function get() {
+        return new $this->instance_name(
+            $this->client, $this->uri
+        );
+    }
+
+}

+ 33 - 0
inc/lib/call/Services/Twilio/Rest/FeedbackSummary.php

@@ -0,0 +1,33 @@
+<?php
+
+class Services_Twilio_Rest_FeedbackSummary extends Services_Twilio_InstanceResource {
+
+    public function __construct($client, $uri, $params = array()) {
+        $this->instance_name = "Services_Twilio_Rest_FeedbackSummary";
+        return parent::__construct($client, $uri, $params);
+    }
+
+    /**
+     * Create feedback summary for calls
+     */
+    public function create(array $params = array()) {
+        $params = $this->client->createData($this->uri, $params);
+        return new $this->instance_name($this->client, $this->uri, $params);
+    }
+
+    /**
+     * Delete a feedback summary
+     */
+    public function delete($sid) {
+        $this->client->deleteData($this->uri . '/' . $sid);
+    }
+
+    /**
+     * Get a feedback summary
+     */
+    public function get($sid) {
+        return new $this->instance_name(
+            $this->client, $this->uri . '/' . $sid
+        );
+    }
+}

+ 91 - 0
inc/lib/call/Services/Twilio/Rest/IncomingPhoneNumber.php

@@ -0,0 +1,91 @@
+<?php
+
+/**
+ *   An object representing a single phone number. For more
+ *   information, see the `IncomingPhoneNumber Instance Resource
+ *   <http://www.twilio.com/docs/api/rest/incoming-phone-numbers#instance>`_
+ *   documentation.
+ *
+ *   .. php:attr:: sid
+ *
+ *      A 34 character string that uniquely idetifies this resource.
+ *
+ *   .. php:attr:: date_created
+ *
+ *      The date that this resource was created, given as GMT RFC 2822 format.
+ *
+ *   .. php:attr:: date_updated
+ *
+ *      The date that this resource was last updated, given as GMT RFC 2822 format.
+ *
+ *   .. php:attr:: friendly_name
+ *
+ *      A human readable descriptive text for this resource, up to 64
+ *      characters long. By default, the FriendlyName is a nicely formatted
+ *      version of the phone number.
+ *
+ *   .. php:attr:: account_sid
+ *
+ *      The unique id of the Account responsible for this phone number.
+ *
+ *   .. php:attr:: phone_number
+ *
+ *      The incoming phone number. e.g., +16175551212 (E.164 format)
+ *
+ *   .. php:attr:: api_version
+ *
+ *      Calls to this phone number will start a new TwiML session with this
+ *      API version.
+ *
+ *   .. php:attr:: voice_caller_id_lookup
+ *
+ *      Look up the caller's caller-ID name from the CNAM database (additional charges apply). Either true or false.
+ *
+ *   .. php:attr:: voice_url
+ *
+ *      The URL Twilio will request when this phone number receives a call.
+ *
+ *   .. php:attr:: voice_method
+ *
+ *      The HTTP method Twilio will use when requesting the above Url. Either GET or POST.
+ *
+ *   .. php:attr:: voice_fallback_url
+ *
+ *      The URL that Twilio will request if an error occurs retrieving or executing the TwiML requested by Url.
+ *
+ *   .. php:attr:: voice_fallback_method
+ *
+ *      The HTTP method Twilio will use when requesting the VoiceFallbackUrl. Either GET or POST.
+ *
+ *   .. php:attr:: status_callback
+ *
+ *      The URL that Twilio will request to pass status parameters (such as call ended) to your application.
+ *
+ *   .. php:attr:: status_callback_method
+ *
+ *      The HTTP method Twilio will use to make requests to the StatusCallback URL. Either GET or POST.
+ *
+ *   .. php:attr:: sms_url
+ *
+ *      The URL Twilio will request when receiving an incoming SMS message to this number.
+ *
+ *   .. php:attr:: sms_method
+ *
+ *      The HTTP method Twilio will use when making requests to the SmsUrl. Either GET or POST.
+ *
+ *   .. php:attr:: sms_fallback_url
+ *
+ *      The URL that Twilio will request if an error occurs retrieving or executing the TwiML from SmsUrl.
+ *
+ *   .. php:attr:: sms_fallback_method
+ *
+ *      The HTTP method Twilio will use when requesting the above URL. Either GET or POST.
+ *
+ *   .. php:attr:: uri
+ *
+ *    The URI for this resource, relative to https://api.twilio.com.
+ */
+class Services_Twilio_Rest_IncomingPhoneNumber
+    extends Services_Twilio_InstanceResource
+{
+}

+ 59 - 0
inc/lib/call/Services/Twilio/Rest/IncomingPhoneNumbers.php

@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * For more information, see the
+ * `IncomingPhoneNumbers API Resource
+ * <http://www.twilio.com/docs/api/rest/incoming-phone-numbers#local>`_
+ * documentation at twilio.com.
+ */
+class Services_Twilio_Rest_IncomingPhoneNumbers extends Services_Twilio_ListResource {
+    function init($client, $uri) {
+        $this->setupSubresources(
+            'local',
+            'toll_free',
+            'mobile'
+        );
+    }
+
+    function create(array $params = array()) {
+        return parent::_create($params);
+    }
+
+    function getList($type, array $params = array())
+    {
+        return $this->client->retrieveData($this->uri . "/$type", $params);
+    }
+
+    /**
+     * Return a phone number instance from its E.164 representation. If more
+     * than one number matches the search string, returns the first one.
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $number = $client->account->incoming_phone_numbers->getNumber('+14105551234');
+     *      echo $number->sid;
+     *
+     * :param string $number: The number in E.164 format, eg "+684105551234"
+     * :return:  A :php:class:`Services_Twilio_Rest_IncomingPhoneNumber` object, or null
+     * :raises: a A :php:class:`Services_Twilio_RestException` if the number is
+     *      invalid, not provided in E.164 format or for any other API exception.
+     */
+    public function getNumber($number) {
+        $page = $this->getPage(0, 1, array(
+            'PhoneNumber' => $number
+        ));
+        $items = $page->getItems();
+        if (is_null($items) || empty($items)) {
+            return null;
+        }
+        return $items[0];
+    }
+}
+
+class Services_Twilio_Rest_Local extends Services_Twilio_NumberType { }
+
+class Services_Twilio_Rest_Mobile extends Services_Twilio_NumberType { }
+
+class Services_Twilio_Rest_TollFree extends Services_Twilio_NumberType { }

+ 40 - 0
inc/lib/call/Services/Twilio/Rest/IpAccessControlList.php

@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * A single IpAccessControlList
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date the ip access control list was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date the ip access control list was updated
+ *
+ * .. php:attr:: sid
+ *
+ *    A 34 character string that identifies this object
+ *
+ * .. php:attr:: account_sid
+ *
+ *    The account that created the ip access control list
+ *
+ * .. php:attr:: friendly_name
+ *
+ *    The friendly name of the ip access control list
+ *
+ * .. php:attr:: uri
+ *
+ *    The uri of the ip access control list
+ *
+ * .. php:attr:: subresource_uris
+ *
+ *    The subresources associated with this ip access control list (IpAddresses)
+ */
+class Services_Twilio_Rest_IpAccessControlList extends Services_Twilio_InstanceResource {
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'ip_addresses'
+        );
+    }
+}

+ 37 - 0
inc/lib/call/Services/Twilio/Rest/IpAccessControlListMapping.php

@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * A single IpAccessControlListMapping
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date this mapping was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date this mapping was updated
+ *
+ * .. php:attr:: sid
+ *
+ *    The sid of this mapping
+ *
+ * .. php:attr:: friendly_name
+ *
+ *    The friendly name of this mapping
+ *
+ * .. php:attr:: uri
+ *
+ *    The uri of this mapping
+ *
+ * .. php:attr:: subresource_uris
+ *
+ *    The subresources associated with this mapping (IpAddresses)
+ */
+class Services_Twilio_Rest_IpAccessControlListMapping extends Services_Twilio_InstanceResource {
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'ip_addresses'
+        );
+    }
+}
+

+ 25 - 0
inc/lib/call/Services/Twilio/Rest/IpAccessControlListMappings.php

@@ -0,0 +1,25 @@
+<?php
+
+class Services_Twilio_Rest_IpAccessControlListMappings extends Services_Twilio_SIPListResource {
+
+    /**
+     * Creates a new IpAccessControlListMapping instance
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $client->account->sip->domains->get('SDXXX')->ip_access_control_list_mappings->create("ALXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
+     *
+     * :param string $ip_access_control_list_sid: the sid of the IpAccessControList
+     *      you're adding to this domain.
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API.
+     */
+    public function create($ip_access_control_list_sid, $params = array()) {
+        return parent::_create(array(
+            'IpAccessControlListSid' => $ip_access_control_list_sid,
+        ) + $params);
+    }
+}
+

+ 27 - 0
inc/lib/call/Services/Twilio/Rest/IpAccessControlLists.php

@@ -0,0 +1,27 @@
+<?php
+
+class Services_Twilio_Rest_IpAccessControlLists extends Services_Twilio_SIPListResource {
+
+    /**
+     * Creates a new IpAccessControlLists instance
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $client->account->sip->ip_access_control_lists->create("MyFriendlyName");
+     *
+     * :param string $friendly_name: the friendly name of this ip access control list
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API.
+     * :return: the created list
+     * :rtype: :class:`Services_Twilio_Rest_IpAccessControlList`
+     *
+     */
+    public function create($friendly_name, $params = array()) {
+        return parent::_create(array(
+            'FriendlyName' => $friendly_name,
+        ) + $params);
+    }
+
+}

+ 34 - 0
inc/lib/call/Services/Twilio/Rest/IpAddress.php

@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * A single IpAddress
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date the IpAddress was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date the IpAddress was updated
+ *
+ * .. php:attr:: sid
+ *
+ *    A 34 character string that identifies this object
+ *
+ * .. php:attr:: account_sid
+ *
+ *    The account that created this credential
+ *
+ * .. php:attr:: friendly_name
+ *
+ *    The friendly name of the IpAddress
+ *
+ * .. php:attr:: ip_address
+ *
+ *    The ip address of this IpAddress object
+ *
+ * .. php:attr:: uri
+ *
+ *    The uri of this IpAddress object
+ */
+class Services_Twilio_Rest_IpAddress extends Services_Twilio_InstanceResource { }

+ 33 - 0
inc/lib/call/Services/Twilio/Rest/IpAddresses.php

@@ -0,0 +1,33 @@
+<?php
+
+class Services_Twilio_Rest_IpAddresses extends Services_Twilio_SIPListResource {
+
+    public function __construct($client, $uri) {
+        $this->instance_name = "Services_Twilio_Rest_IpAddress";
+        parent::__construct($client, $uri);
+    }
+
+    /**
+     * Creates a new IpAddress instance
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $client->account->sip->ip_access_control_lists->get('ALXXX')->ip_addresses->create(
+     *          "FriendlyName", "127.0.0.1"
+     *      );
+     *
+     * :param string $friendly_name: the friendly name for the new IpAddress object
+     * :param string $ip_address: the ip address for the new IpAddress object
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API.
+     */
+    public function create($friendly_name, $ip_address, $params = array()) {
+        return parent::_create(array(
+            'FriendlyName' => $friendly_name,
+            'IpAddress' => $ip_address,
+        ) + $params);
+    }
+}
+

+ 31 - 0
inc/lib/call/Services/Twilio/Rest/Media.php

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * A list of :class:`Media <Services_Twilio_Rest_MediaInstance>` objects.
+ * For the definitive reference, see the `Twilio Media List Documentation
+ * <https://www.twilio.com/docs/api/rest/media>`_.
+ */
+class Services_Twilio_Rest_Media extends Services_Twilio_ListResource {
+
+
+    // This is overridden because the list key in the Twilio response
+    // is "media_list", not "media".
+    public function getResourceName($camelized = false)
+    {
+        if ($camelized) {
+            return "MediaList";
+        } else {
+            return "media_list";
+        }
+    }
+
+    // We manually set the instance name here so that the parent
+    // constructor doesn't attempt to figure out it. It would do it
+    // incorrectly because we override getResourceName above.
+    public function __construct($client, $uri) {
+        $this->instance_name = "Services_Twilio_Rest_MediaInstance";
+        parent::__construct($client, $uri);
+    }
+
+}
+

+ 37 - 0
inc/lib/call/Services/Twilio/Rest/MediaInstance.php

@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * A single Media object. For the definitive reference, see the `Twilio Media
+ * Documentation <https://www.twilio.com/docs/api/rest/media>`_.
+ *
+ * .. php:attr:: sid
+ *
+ *    A 34 character string that identifies this object
+ *
+ * .. php:attr:: account_sid
+ *
+ *    A 34 character string representing the account that sent the message
+ *
+ * .. php:attr:: parent_sid
+ *
+ *    The sid of the message that created this media.
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date the message was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date the message was updated
+ *
+ * .. php:attr:: content_type
+ *
+ *    The content-type of the media.
+ */
+class Services_Twilio_Rest_MediaInstance extends Services_Twilio_InstanceResource {
+    public function __construct($client, $uri) {
+        $uri = str_replace('MediaInstance', 'Media', $uri);
+        parent::__construct($client, $uri);
+    }
+}
+

+ 22 - 0
inc/lib/call/Services/Twilio/Rest/Member.php

@@ -0,0 +1,22 @@
+<?php
+
+class Services_Twilio_Rest_Member
+    extends Services_Twilio_InstanceResource
+{
+
+    /**
+     * Dequeue this member
+     *
+     * @param string $url The Twiml URL to play for this member, after 
+     *      dequeueing them
+     * @param string $method The HTTP method to use when fetching the Twiml 
+     *      URL. Defaults to POST.
+     * @return Services_Twilio_Rest_Member The dequeued member
+     */
+    public function dequeue($url, $method = 'POST') {
+        return self::update(array(
+            'Url' => $url,
+            'Method' => $method,
+        ));
+    }
+}

+ 28 - 0
inc/lib/call/Services/Twilio/Rest/Members.php

@@ -0,0 +1,28 @@
+<?php
+
+class Services_Twilio_Rest_Members
+    extends Services_Twilio_ListResource
+{
+    /**
+     * Return the member at the front of the queue. Note that any operations 
+     * performed on the Member returned from this function will use the /Front 
+     * Uri, not the Member's CallSid.
+     *
+     * @return Services_Twilio_Rest_Member The member at the front of the queue
+     */
+    public function front() {
+        return new $this->instance_name($this->client, $this->uri . '/Front');
+    }
+
+    /* Participants are identified by CallSid, not like ME123 */
+    public function getObjectFromJson($params, $idParam = 'sid') {
+        return parent::getObjectFromJson($params, 'call_sid');
+    }
+
+    public function getResourceName($camelized = false)
+    {
+        // The JSON property name is atypical.
+        return $camelized ? 'Members' : 'queue_members';
+    }
+}
+

+ 58 - 0
inc/lib/call/Services/Twilio/Rest/Message.php

@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * A single Message
+ *
+ * .. php:attr:: date_created
+ *
+ *    The date the message was created
+ *
+ * .. php:attr:: date_updated
+ *
+ *    The date the message was updated
+ *
+ * .. php:attr:: sid
+ *
+ *    A 34 character string that identifies this object
+ *
+ * .. php:attr:: account_sid
+ *
+ *    The account that sent the message
+ *
+ * .. php:attr:: body
+ *
+ *    The body of the message
+ *
+ * .. php:attr:: num_segments
+ *
+ *    The number of sms messages used to deliver the body
+ *
+ * .. php:attr:: num_media
+ *
+ *    The number of media that are associated with the image
+ *
+ * .. php:attr:: subresource_uris
+ *
+ *    The subresources associated with this message (just Media at the moment)
+ *
+ * .. php:attr:: from
+ *
+ *    The number this message was sent from
+ *
+ * .. php:attr:: to
+ *
+ *    The phone number this message was sent to
+ */
+class Services_Twilio_Rest_Message extends Services_Twilio_InstanceResource {
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'media'
+        );
+    }
+
+    public function redact() {
+        $postParams = array('Body' => '');
+        self::update($postParams);
+    }
+}
+

+ 73 - 0
inc/lib/call/Services/Twilio/Rest/Messages.php

@@ -0,0 +1,73 @@
+<?php
+
+class Services_Twilio_Rest_Messages extends Services_Twilio_ListResource {
+
+    /**
+     * Create a new Message instance
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $client->account->messages->create(array(
+     *          "Body" => "foo",
+     *          "From" => "+14105551234",
+     *          "To" => "+14105556789",
+     *      ));
+     *
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API. You may find it easier to use the
+     *      sendMessage helper instead of this function.
+     *
+     */
+    public function create($params = array()) {
+        return parent::_create($params);
+    }
+
+    /**
+     * Send a message
+     *
+     * .. code-block:: php
+     *
+     *      $client = new Services_Twilio('AC123', '123');
+     *      $message = $client->account->messages->sendMessage(
+     *          '+14105551234', // From a Twilio number in your account
+     *          '+14105556789', // Text any number
+     *          'Come at the king, you best not miss.'   // Message body (if any)
+     *          array('https://demo.twilio.com/owl.png'),   // An array of MediaUrls
+     *      );
+     *
+     * :param string $from: the from number for the message, this must be a
+     *      number you purchased from Twilio
+     * :param string $to: the message recipient's phone number
+     * :param $mediaUrls: the URLs of images to send in this MMS
+     * :type $mediaUrls: null (don't include media), a single URL, or an array
+     *      of URLs to send as media with this message
+     * :param string $body: the text to include along with this MMS
+     * :param array $params: Any additional params (callback, etc) you'd like to
+     *      send with this request, these are serialized and sent as POST
+     *      parameters
+     *
+     * :return: The created :class:`Services_Twilio_Rest_Message`
+     * :raises: :class:`Services_Twilio_RestException`
+     *      An exception if the parameters are invalid (for example, the from
+     *      number is not a Twilio number registered to your account, or is
+     *      unable to send MMS)
+     */
+    public function sendMessage($from, $to, $body = null, $mediaUrls = null,
+        $params = array()
+    ) {
+        $postParams = array(
+            'From' => $from,
+            'To' => $to,
+        );
+        // When the request is made, this will get serialized into MediaUrl=a&MediaUrl=b
+        if (!is_null($mediaUrls)) {
+            $postParams['MediaUrl'] = $mediaUrls;
+        }
+        if (!is_null($body)) {
+            $postParams['Body'] = $body;
+        }
+        return self::create($postParams + $params);
+    }
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Notification.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Notification
+    extends Services_Twilio_InstanceResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Notifications.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Notifications
+    extends Services_Twilio_ListResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/OutgoingCallerId.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_OutgoingCallerId
+    extends Services_Twilio_InstanceResource
+{
+}

+ 12 - 0
inc/lib/call/Services/Twilio/Rest/OutgoingCallerIds.php

@@ -0,0 +1,12 @@
+<?php
+
+class Services_Twilio_Rest_OutgoingCallerIds
+    extends Services_Twilio_ListResource
+{
+    public function create($phoneNumber, array $params = array())
+    {
+        return parent::_create(array(
+            'PhoneNumber' => $phoneNumber,
+        ) + $params);
+    }
+}

+ 10 - 0
inc/lib/call/Services/Twilio/Rest/Participant.php

@@ -0,0 +1,10 @@
+<?php
+
+class Services_Twilio_Rest_Participant
+    extends Services_Twilio_InstanceResource
+{
+    public function mute()
+    {
+        $this->update('Muted', 'true');
+    }
+}

+ 10 - 0
inc/lib/call/Services/Twilio/Rest/Participants.php

@@ -0,0 +1,10 @@
+<?php
+
+class Services_Twilio_Rest_Participants
+    extends Services_Twilio_ListResource
+{
+    /* Participants are identified by CallSid, not like PI123 */
+    public function getObjectFromJson($params, $idParam = "sid") {
+        return parent::getObjectFromJson($params, "call_sid");
+    }
+}

+ 10 - 0
inc/lib/call/Services/Twilio/Rest/Queue.php

@@ -0,0 +1,10 @@
+<?php
+
+class Services_Twilio_Rest_Queue
+    extends Services_Twilio_InstanceResource {
+
+    protected function init($client, $uri) {
+        $this->setupSubresources('members');
+    }
+}
+

+ 19 - 0
inc/lib/call/Services/Twilio/Rest/Queues.php

@@ -0,0 +1,19 @@
+<?php
+
+class Services_Twilio_Rest_Queues
+    extends Services_Twilio_ListResource
+{
+    /**
+     * Create a new Queue
+     *
+     * @param string $friendly_name The name of this queue
+     * @param array $params A list of optional parameters, and their values
+     * @return Services_Twilio_Rest_Queue The created Queue
+     */
+    function create($friendly_name, array $params = array()) {
+        return parent::_create(array(
+            'FriendlyName' => $friendly_name,
+        ) + $params);
+    }
+}
+

+ 9 - 0
inc/lib/call/Services/Twilio/Rest/Recording.php

@@ -0,0 +1,9 @@
+<?php
+
+class Services_Twilio_Rest_Recording
+    extends Services_Twilio_InstanceResource
+{
+    protected function init($client, $uri) {
+        $this->setupSubresources('transcriptions');
+    }
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Recordings.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Recordings
+    extends Services_Twilio_ListResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Sandbox.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Sandbox
+    extends Services_Twilio_InstanceResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/ShortCode.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_ShortCode
+    extends Services_Twilio_InstanceResource
+{
+}

+ 10 - 0
inc/lib/call/Services/Twilio/Rest/ShortCodes.php

@@ -0,0 +1,10 @@
+<?php
+
+class Services_Twilio_Rest_ShortCodes
+    extends Services_Twilio_ListResource
+{
+    public function __construct($client, $uri) {
+        $uri = preg_replace("#ShortCodes#", "SMS/ShortCodes", $uri);
+        parent::__construct($client, $uri);
+    }
+}

+ 19 - 0
inc/lib/call/Services/Twilio/Rest/Sip.php

@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * For Linux filename compatibility, this file needs to be named Sip.php, or
+ * camelize() needs to be special cased in setupSubresources
+ */
+class Services_Twilio_Rest_Sip extends Services_Twilio_InstanceResource {
+    protected function init($client, $uri) {
+        $this->setupSubresources(
+            'domains',
+            'ip_access_control_lists',
+            'credential_lists'
+        );
+    }
+
+    public function getResourceName($camelized = false) {
+        return "SIP";
+    }
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/SmsMessage.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_SmsMessage
+    extends Services_Twilio_InstanceResource
+{
+}

+ 18 - 0
inc/lib/call/Services/Twilio/Rest/SmsMessages.php

@@ -0,0 +1,18 @@
+<?php
+
+class Services_Twilio_Rest_SmsMessages
+    extends Services_Twilio_ListResource
+{
+    public function __construct($client, $uri) {
+        $uri = preg_replace("#SmsMessages#", "SMS/Messages", $uri);
+        parent::__construct($client, $uri);
+    }
+
+    function create($from, $to, $body, array $params = array()) {
+        return parent::_create(array(
+            'From' => $from,
+            'To' => $to,
+            'Body' => $body
+        ) + $params);
+    }
+}

+ 5 - 0
inc/lib/call/Services/Twilio/Rest/Token.php

@@ -0,0 +1,5 @@
+<?php
+
+class Services_Twilio_Rest_Token extends Services_Twilio_InstanceResource {
+
+}

+ 24 - 0
inc/lib/call/Services/Twilio/Rest/Tokens.php

@@ -0,0 +1,24 @@
+<?php
+
+class Services_Twilio_Rest_Tokens extends Services_Twilio_ListResource {
+
+    /**
+     * Create a new Token instance
+     *
+     * Example usage:
+     *
+     * .. code-block:: php
+     *
+     *      $client->account->tokens->create(array(
+     *          "Ttl" => 100,
+     *      ));
+     *
+     * :param array $params: a single array of parameters which is serialized and
+     *      sent directly to the Twilio API.
+     *
+     */
+    public function create($params = array()) {
+        return parent::_create($params);
+    }
+
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Transcription.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Transcription
+    extends Services_Twilio_InstanceResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/Transcriptions.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_Transcriptions
+    extends Services_Twilio_ListResource
+{
+}

+ 6 - 0
inc/lib/call/Services/Twilio/Rest/UsageRecord.php

@@ -0,0 +1,6 @@
+<?php
+
+class Services_Twilio_Rest_UsageRecord extends Services_Twilio_InstanceResource
+{
+}
+

+ 33 - 0
inc/lib/call/Services/Twilio/Rest/UsageRecords.php

@@ -0,0 +1,33 @@
+<?php
+
+class Services_Twilio_Rest_UsageRecords extends Services_Twilio_TimeRangeResource {
+
+    public function init($client, $uri) {
+        $this->setupSubresources(
+            'today',
+            'yesterday',
+            'all_time',
+            'this_month',
+            'last_month',
+            'daily',
+            'monthly',
+            'yearly'
+        );
+    }
+}
+
+class Services_Twilio_Rest_Today extends Services_Twilio_TimeRangeResource { } 
+
+class Services_Twilio_Rest_Yesterday extends Services_Twilio_TimeRangeResource { }
+
+class Services_Twilio_Rest_LastMonth extends Services_Twilio_TimeRangeResource { }
+
+class Services_Twilio_Rest_ThisMonth extends Services_Twilio_TimeRangeResource { }
+
+class Services_Twilio_Rest_AllTime extends Services_Twilio_TimeRangeResource { }
+
+class Services_Twilio_Rest_Daily extends Services_Twilio_UsageResource { }
+
+class Services_Twilio_Rest_Monthly extends Services_Twilio_UsageResource { }
+
+class Services_Twilio_Rest_Yearly extends Services_Twilio_UsageResource { }

+ 5 - 0
inc/lib/call/Services/Twilio/Rest/UsageTrigger.php

@@ -0,0 +1,5 @@
+<?php
+
+class Services_Twilio_Rest_UsageTrigger
+    extends Services_Twilio_InstanceResource { }
+

+ 27 - 0
inc/lib/call/Services/Twilio/Rest/UsageTriggers.php

@@ -0,0 +1,27 @@
+<?php
+
+class Services_Twilio_Rest_UsageTriggers extends Services_Twilio_ListResource {
+
+    public function __construct($client, $uri) {
+        $uri = preg_replace("#UsageTriggers#", "Usage/Triggers", $uri);
+        parent::__construct($client, $uri);
+    }
+
+    /**
+     * Create a new UsageTrigger
+     * @param string $category The category of usage to fire a trigger for. A full list of categories can be found in the `Usage Categories documentation <http://www.twilio.com/docs/api/rest/usage-records#usage-categories>`_.
+     * @param string $value Fire the trigger when usage crosses this value.
+     * @param string $url The URL to request when the trigger fires.
+     * @param array $params Optional parameters for this trigger. A full list of parameters can be found in the `Usage Trigger documentation <http://www.twilio.com/docs/api/rest/usage-triggers#list-post-optional-parameters>`_.
+     * @return Services_Twilio_Rest_UsageTrigger The created trigger
+     */
+    function create($category, $value, $url, array $params = array()) {
+        return parent::_create(array(
+            'UsageCategory' => $category,
+            'TriggerValue' => $value,
+            'CallbackUrl' => $url,
+        ) + $params);
+    }
+
+}
+

+ 44 - 0
inc/lib/call/Services/Twilio/RestException.php

@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * An exception talking to the Twilio API. This is thrown whenever the Twilio
+ * API returns a 400 or 500-level exception.
+ *
+ * :param int $status: the HTTP status for the exception
+ * :param string $message: a human-readable error message for the exception
+ * :param int $code: a Twilio-specific error code for the exception
+ * :param string $info: a link to more information
+ */
+class Services_Twilio_RestException extends Exception {
+
+    /**
+     * The HTTP status for the exception.
+     */
+    protected $status;
+
+    /**
+     * A URL to get more information about the error. This is not always
+     * available
+     */
+    protected $info;
+
+    public function __construct($status, $message, $code = 0, $info = '') {
+        $this->status = $status;
+        $this->info = $info;
+        parent::__construct($message, $code);
+    }
+
+    /**
+     * Get the HTTP status code
+     */
+    public function getStatus() {
+        return $this->status;
+    }
+
+    /**
+     * Get a link to more information
+     */
+    public function getInfo() {
+        return $this->info;
+    }
+}

+ 14 - 0
inc/lib/call/Services/Twilio/SIPListResource.php

@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * This subclass of ListResource is used solely to update
+ * the URI for sip resources.
+ */
+abstract class Services_Twilio_SIPListResource extends Services_Twilio_ListResource {
+    public function __construct($client, $uri) {
+        // Rename all /Sip/ uris to /SIP/
+        $uri = preg_replace("#/Sip#", "/SIP", $uri);
+        parent::__construct($client, $uri);
+    }
+}
+

+ 31 - 0
inc/lib/call/Services/Twilio/TimeRangeResource.php

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * Parent class for usage resources that expose a single date, eg 'Today', 'ThisMonth', etc
+ * @author Kevin Burke <kevin@twilio.com>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://pear.php.net/package/Services_Twilio
+ */
+class Services_Twilio_TimeRangeResource extends Services_Twilio_UsageResource {
+
+    /**
+     * Return a UsageRecord corresponding to the given category.
+     *
+     * @param string $category The category of usage to retrieve. For a full 
+     *      list of valid categories, please see the documentation at 
+     *      http://www.twilio.com/docs/api/rest/usage-records#usage-all-categories
+     * @return Services_Twilio_Rest_UsageRecord
+     * @throws Services_Twilio_RestException
+     */
+    public function getCategory($category) {
+        $page = $this->getPage(0, 1, array(
+            'Category' => $category,
+        ));
+        $items = $page->getItems();
+        if (!is_array($items) || count($items) === 0) {
+            throw new Services_Twilio_RestException(
+                400, "Usage record data is unformattable.");
+        }
+        return $items[0];
+    }
+}

+ 126 - 0
inc/lib/call/Services/Twilio/TinyHttp.php

@@ -0,0 +1,126 @@
+<?php
+/**
+ * Based on TinyHttp from https://gist.github.com/618157.
+ * Copyright 2011, Neuman Vong. BSD License.
+ */
+
+class Services_Twilio_TinyHttpException extends ErrorException {}
+
+/**
+ * An HTTP client that makes requests
+ *
+ * :param string $uri: The base uri to use for requests
+ * :param array $kwargs: An array of additional arguments to pass to the
+ *  library. Accepted arguments are:
+ *
+ *      - **debug** - Print the HTTP request before making it to Twilio
+ *      - **curlopts** - An array of keys and values that are passed to
+ *          ``curl_setopt_array``.
+ *
+ * Here's an example. This is the default HTTP client used by the library.
+ *
+ * .. code-block:: php
+ *
+ *     $_http = new Services_Twilio_TinyHttp(
+ *         "https://api.twilio.com",
+ *         array("curlopts" => array(
+ *             CURLOPT_USERAGENT => self::USER_AGENT,
+ *             CURLOPT_HTTPHEADER => array('Accept-Charset: utf-8'),
+ *             CURLOPT_CAINFO => dirname(__FILE__) . '/cacert.pem',
+ *         ))
+ *     );
+ */
+class Services_Twilio_TinyHttp {
+  var $user, $pass, $scheme, $host, $port, $debug, $curlopts;
+
+  public function __construct($uri = '', $kwargs = array()) {
+    foreach (parse_url($uri) as $name => $value) $this->$name = $value;
+    $this->debug = isset($kwargs['debug']) ? !!$kwargs['debug'] : NULL;
+    $this->curlopts = isset($kwargs['curlopts']) ? $kwargs['curlopts'] : array();
+  }
+
+  public function __call($name, $args) {
+    list($res, $req_headers, $req_body) = $args + array(0, array(), '');
+
+    $opts = $this->curlopts + array(
+      CURLOPT_URL => "$this->scheme://$this->host$res",
+      CURLOPT_HEADER => TRUE,
+      CURLOPT_RETURNTRANSFER => TRUE,
+      CURLOPT_INFILESIZE => -1,
+      CURLOPT_POSTFIELDS => NULL,
+      CURLOPT_TIMEOUT => 60,
+    );
+
+    foreach ($req_headers as $k => $v) $opts[CURLOPT_HTTPHEADER][] = "$k: $v";
+    if ($this->port) $opts[CURLOPT_PORT] = $this->port;
+    if ($this->debug) $opts[CURLINFO_HEADER_OUT] = TRUE;
+    if ($this->user && $this->pass) $opts[CURLOPT_USERPWD] = "$this->user:$this->pass";
+    switch ($name) {
+    case 'get':
+      $opts[CURLOPT_HTTPGET] = TRUE;
+      break;
+    case 'post':
+      $opts[CURLOPT_POST] = TRUE;
+      $opts[CURLOPT_POSTFIELDS] = $req_body;
+      break;
+    case 'put':
+      $opts[CURLOPT_PUT] = TRUE;
+      if (strlen($req_body)) {
+        if ($buf = fopen('php://memory', 'w+')) {
+          fwrite($buf, $req_body);
+          fseek($buf, 0);
+          $opts[CURLOPT_INFILE] = $buf;
+          $opts[CURLOPT_INFILESIZE] = strlen($req_body);
+        } else throw new Services_Twilio_TinyHttpException('unable to open temporary file');
+      }
+      break;
+    case 'head':
+      $opts[CURLOPT_NOBODY] = TRUE;
+      break;
+    default:
+      $opts[CURLOPT_CUSTOMREQUEST] = strtoupper($name);
+      break;
+    }
+    try {
+      if ($curl = curl_init()) {
+        if (curl_setopt_array($curl, $opts)) {
+          if ($response = curl_exec($curl)) {
+            $parts = explode("\r\n\r\n", $response, 3);
+            list($head, $body) = ($parts[0] == 'HTTP/1.1 100 Continue')
+              ? array($parts[1], $parts[2])
+              : array($parts[0], $parts[1]);
+            $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+            if ($this->debug) {
+              error_log(
+                curl_getinfo($curl, CURLINFO_HEADER_OUT) .
+                $req_body
+              );
+            }
+            $header_lines = explode("\r\n", $head);
+            array_shift($header_lines);
+            foreach ($header_lines as $line) {
+              list($key, $value) = explode(":", $line, 2);
+              $headers[$key] = trim($value);
+            }
+            curl_close($curl);
+            if (isset($buf) && is_resource($buf)) {
+                fclose($buf);
+            }
+            return array($status, $headers, $body);
+          } else {
+              throw new Services_Twilio_TinyHttpException(curl_error($curl));
+          }
+        } else throw new Services_Twilio_TinyHttpException(curl_error($curl));
+      } else throw new Services_Twilio_TinyHttpException('unable to initialize cURL');
+    } catch (ErrorException $e) {
+      if (is_resource($curl)) curl_close($curl);
+      if (isset($buf) && is_resource($buf)) fclose($buf);
+      throw $e;
+    }
+  }
+
+  public function authenticate($user, $pass) {
+    $this->user = $user;
+    $this->pass = $pass;
+  }
+}

+ 137 - 0
inc/lib/call/Services/Twilio/Twiml.php

@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * Exception class for Services_Twilio_Twiml.
+ */
+class Services_Twilio_TwimlException extends Exception {}
+
+/**
+ * Twiml response generator.
+ *
+ * Author:   Neuman Vong <neuman at ashmoremusic dot com>
+ * License:  http://creativecommons.org/licenses/MIT/ MIT
+ */
+class Services_Twilio_Twiml {
+
+    protected $element;
+
+    /**
+     * Constructs a Twiml response.
+     *
+     * :param SimpleXmlElement|array $arg: Can be any of
+     *
+     *   - the element to wrap
+     *   - attributes to add to the element
+     *   - if null, initialize an empty element named 'Response'
+     */
+    public function __construct($arg = null) {
+        switch (true) {
+        case $arg instanceof SimpleXmlElement:
+            $this->element = $arg;
+            break;
+        case $arg === null:
+            $this->element = new SimpleXmlElement('<Response/>');
+            break;
+        case is_array($arg):
+            $this->element = new SimpleXmlElement('<Response/>');
+            foreach ($arg as $name => $value) {
+                $this->element->addAttribute($name, $value);
+            }
+            break;
+        default:
+            throw new TwimlException('Invalid argument');
+        }
+    }
+
+    /**
+     * Converts method calls into Twiml verbs.
+     *
+     * A basic example:
+     *
+     * .. code-block:: php
+     *
+     *     php> print $this->say('hello');
+     *     <Say>hello</Say>
+     *
+     * An example with attributes:
+     *
+     * .. code-block:: php
+     *
+     *     print $this->say('hello', array('voice' => 'woman'));
+     *     <Say voice="woman">hello</Say>
+     *
+     * You could even just pass in an attributes array, omitting the noun:
+     *
+     * .. code-block:: php
+     *
+     *     print $this->gather(array('timeout' => '20'));
+     *     <Gather timeout="20"/>
+     *
+     * :param string $verb: The Twiml verb.
+     * :param array  $args:
+     *   - (noun string)
+     *   - (noun string, attributes array)
+     *   - (attributes array)
+     *
+     * :return: A SimpleXmlElement
+     * :rtype: SimpleXmlElement
+     */
+    public function __call($verb, array $args)
+    {
+        list($noun, $attrs) = $args + array('', array());
+        if (is_array($noun)) {
+            list($attrs, $noun) = array($noun, '');
+        }
+        /* addChild does not escape XML, while addAttribute does. This means if
+         * you pass unescaped ampersands ("&") to addChild, you will generate
+         * an error.
+         *
+         * Some inexperienced developers will pass in unescaped ampersands, and
+         * we want to make their code work, by escaping the ampersands for them
+         * before passing the string to addChild. (with htmlentities)
+         *
+         * However other people will know what to do, and their code
+         * already escapes ampersands before passing them to addChild. We don't
+         * want to break their existing code by turning their &amp;'s into
+         * &amp;amp;
+         *
+         * We also want to use numeric entities, not named entities so that we
+         * are fully compatible with XML
+         *
+         * The following lines accomplish the desired behavior.
+         */
+        $decoded = html_entity_decode($noun, ENT_COMPAT, 'UTF-8');
+        $normalized = htmlspecialchars($decoded, ENT_COMPAT, 'UTF-8', false);
+        $child = empty($noun)
+            ? $this->element->addChild(ucfirst($verb))
+            : $this->element->addChild(ucfirst($verb), $normalized);
+        foreach ($attrs as $name => $value) {
+            /* Note that addAttribute escapes raw ampersands by default, so we
+             * haven't touched its implementation. So this is the matrix for
+             * addAttribute:
+             *
+             * & turns into &amp;
+             * &amp; turns into &amp;amp;
+             */
+            if (is_bool($value)) {
+                $value = ($value === true) ? 'true' : 'false';
+            }
+            $child->addAttribute($name, $value);
+        }
+        return new static($child);
+    }
+
+    /**
+     * Returns the object as XML.
+     *
+     * :return: The response as an XML string
+     * :rtype: string
+     */
+    public function __toString()
+    {
+        $xml = $this->element->asXml();
+        return str_replace(
+            '<?xml version="1.0"?>',
+            '<?xml version="1.0" encoding="UTF-8"?>', $xml);
+    }
+}

+ 20 - 0
inc/lib/call/Services/Twilio/UsageResource.php

@@ -0,0 +1,20 @@
+<?php
+
+/** 
+ * Parent class for all UsageRecord subclasses
+ * @author Kevin Burke <kevin@twilio.com>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://pear.php.net/package/Services_Twilio
+ */
+class Services_Twilio_UsageResource extends Services_Twilio_ListResource {
+    public function getResourceName($camelized = false) {
+        $this->instance_name = 'Services_Twilio_Rest_UsageRecord';
+        return $camelized ? 'UsageRecords' : 'usage_records';
+    }
+
+    public function __construct($client, $uri) {
+        $uri = preg_replace("#UsageRecords#", "Usage/Records", $uri);
+        parent::__construct($client, $uri);
+    }
+}
+

+ 33 - 0
inc/lib/call/call.js

@@ -0,0 +1,33 @@
+$(document).ready(function(){
+	Twilio.Device.setup("<?php echo $token; ?>");
+	$("#call").click(function() {
+		Twilio.Device.connect();
+	});
+	$("#hangup").click(function() {
+		Twilio.Device.disconnectAll();
+	});
+	Twilio.Device.ready(function (device) {
+		$('#status').text('Ready to start call');
+	});
+	Twilio.Device.offline(function (device) {
+		$('#status').text('Offline');
+	});
+	Twilio.Device.error(function (error) {
+		$('#status').text(error);
+	});
+	Twilio.Device.connect(function (conn) {
+		$('#status').text("Successfully established call");
+		toggleCallStatus();
+	});
+	Twilio.Device.disconnect(function (conn) {
+		$('#status').text("Call ended");
+		toggleCallStatus();
+	});
+	Twilio.Device.incoming(function (conn) {
+	conn.accept();
+	});
+	function toggleCallStatus(){
+		$('#call').toggle();
+		$('#hangup').toggle();
+	}
+});

+ 38 - 0
inc/lib/call/call.php

@@ -0,0 +1,38 @@
+<?php
+include 'Services/Twilio/Capability.php';
+include 'auth.php'
+?>
+<!DOCTYPE HTML>
+<html>
+	<head>
+		<title>Phone</title>
+		<script type="text/javascript" src="//static.twilio.com/libs/twiliojs/1.1/twilio.min.js"></script>
+		<script type="text/javascript" src="jquery.min.js"></script>
+		<script type="text/javascript">
+		$(document).ready(function(){
+			Twilio.Device.setup("<?php echo $token; ?>");
+			$("#call").click(function() {Twilio.Device.connect();});
+			$("#hangup").click(function() {Twilio.Device.disconnectAll();});
+			Twilio.Device.ready(function (device) {$('#status').text('Ready to call');});
+			Twilio.Device.offline(function (device) {$('#status').text('Offline');});
+			Twilio.Device.error(function (error) {$('#status').text(error);});
+			Twilio.Device.connect(function (conn) {$('#status').text("Successfully established call");toggleCallStatus();});
+			Twilio.Device.disconnect(function (conn) {$('#status').text("Call ended");toggleCallStatus();});
+			Twilio.Device.incoming(function (conn) {conn.accept();});
+			function toggleCallStatus(){
+				$('#call').toggle();
+				$('#hangup').toggle();
+			}
+		});
+		</script>
+	</head>
+	<body>
+		<div align="center">
+			<input type="button" id="call" value="Start Call"/>
+			<input type="button" id="hangup" value="Disconnect Call" style="display:none;"/>
+			<div id="status">
+				Offline
+			</div>
+		</div>
+	</body>
+</html>

+ 15 - 0
inc/lib/call/phone.php

@@ -0,0 +1,15 @@
+<?php
+header('Content-type: text/xml');
+if (isset($_REQUEST['PhoneNumber'])) {
+$client = htmlspecialchars($_REQUEST['PhoneNumber']);
+}
+?>
+<Response>
+<?php if (empty($client)) { ?>
+ <Dial>
+<Client><?php echo $client; ?></Client>
+</Dial>
+<?php } else { ?>
+<Say>An Error Occurred. We are sorry.</Say>
+<?php } ?>
+</Response>

+ 5 - 0
inc/lib/call/twiml.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Response>
+	<Say voice="woman">Please hold on a moment while I try to reach David</Say>
+	<Dial callerId="18037124787">803-712-3283</Dial>
+</Response>