Howdy! I'm Daniel. I make stuff, here's some of the stuff I've made.

Southern Utah Goldens : 2010 - Present

Role: Backend Frontend Database Design Logo Server Admin Architect
Stack: Apache PHP Javascript MySQL HTML CSS prototype.js ImageMagick
https://southernutahgoldens.com

This is totally one of those "hey I need a favor" kinda jobs for a family member, but I totally love my mom's dogs so you get to look at this one first!

FireShot-Capture-1---Home-I-Southern-Utah-Goldens---https___www.southernutahgoldens.com_

Daqe(Database Access Query Engine) : 2012 - 2017

Role: Backend Frontend Database Design Server Admin Architect
Stack: nginx PHP Javascript MySQL HTML CSS jQuery Sendgrid Plivo Twilio Asterisk PBX

This lovely piece of enterprise software went through several iterations while I was with this employer and was actually the main reason I was hired in the first place. At the time I started, in 2012, the existing version had built up so much technical debt that the decision had already been made to start anew.

Initially, I was tasked with building out a SugarCRM based solution that our sales team immediately felt was too complicated. They made it pretty clear that they were completely unwilling to learn how to use it. This version was scrapped pretty early once we determined that simplifying the Sugar UX would be more work than restarting with something less... well, less.

We moved on to a solution built on a Ruby-on-Rails based project called Fat Free CRM. I oversaw this project and worked with a small team to get it ready with our customizations and callcenter features.

Ultimately, however, some of our sales team saw me working on a dead-simple lead distribution tool that I originally threw together in an afternoon. It was just ment for passing stale leads off to affilates, but they fell in love with it. This simple tool - originally labeled LRM(Lead Referral Manager) - grew into the final version of our Daqe CRM and callcenter system. With this version, we also replaced our aging Asterisk PBX server and mish-mash messaging integrations with a Plivo-based phone and messaging system.

I mostly worked solo on this final version of the project since it was originally a very small system and most changes were relatively small, as needed sorts of things.

<?php

class GeoTools {

private static $regionData = [
  'Alabama' => [
    'country'   => 'USA',
    'regionCode' => 'AL',
    'areaCodes' => [205, 251, 256, 334, 938],
    'zipCodes'  => []
  ],
  'Alaska' => [
    'country'   => 'USA',
    'regionCode' => 'AK',
    'areaCodes' => [907],
    'zipCodes'  => []
  ],
  'Arizona' => [
    'country'   => 'USA',
    'regionCode' => 'AZ',
    'areaCodes' => [480, 520, 602, 623, 928],
    'zipCodes'  => []
  ],
  'Arkansas' => [
    'country'   => 'USA',
    'regionCode' => 'AR',
    'areaCodes' => [479, 501, 870],
    'zipCodes'  => []
  ],
  'California' => [
    'country'   => 'USA',
    'regionCode' => 'CA',
    'areaCodes' => [209, 213, 310, 323, 408, 415, 424, 442, 510, 530, 559,
                    562, 619, 626, 628, 650, 657, 661, 669, 707, 714, 747,
                    760, 805, 818, 831, 858, 909, 916, 925, 949, 951],
    'zipCodes'  => []
  ],
  'Colorado' => [
    'country'   => 'USA',
    'regionCode' => 'CO',
    'areaCodes' => [303, 719, 720, 970],
    'zipCodes'  => []
  ],
  'Connecticut' => [
    'country'   => 'USA',
    'regionCode' => 'CT',
    'areaCodes' => [203, 475, 860, 959],
    'zipCodes'  => []
  ],
  'Delaware' => [
    'country'   => 'USA',
    'regionCode' => 'DE',
    'areaCodes' => [302],
    'zipCodes'  => []
  ],
  'Florida' => [
    'country'   => 'USA',
    'regionCode' => 'FL',
    'areaCodes' => [239, 305, 321, 352, 386, 407, 561, 727, 754, 772, 786,
                    813, 850, 863, 904, 941, 954],
    'zipCodes'  => []
  ],
  'Georgia' => [
    'country'   => 'USA',
    'regionCode' => 'GA',
    'areaCodes' => [229, 404, 470, 478, 678, 706, 762, 770, 912],
    'zipCodes'  => []
  ],
  'Hawaii' => [
    'country'   => 'USA',
    'regionCode' => 'HI',
    'areaCodes' => [808],
    'zipCodes'  => []
  ],
  'Idaho' => [
    'country'   => 'USA',
    'regionCode' => 'ID',
    'areaCodes' => [208],
    'zipCodes'  => []
  ],
  'Illinois' => [
    'country'   => 'USA',
    'regionCode' => 'IL',
    'areaCodes' => [217, 224, 309, 312, 331, 618, 630, 708, 773, 779, 815,
                    847, 872],
    'zipCodes'  => []
  ],
  'Indiana' => [
    'country'   => 'USA',
    'regionCode' => 'IN',
    'areaCodes' => [219, 260, 317, 463, 574, 765, 812, 930],
    'zipCodes'  => []
  ],
  'Iowa' => [
    'country'   => 'USA',
    'regionCode' => 'IA',
    'areaCodes' => [319, 515, 563, 641, 712],
    'zipCodes'  => []
  ],
  'Kansas' => [
    'country'   => 'USA',
    'regionCode' => 'KS',
    'areaCodes' => [316, 620, 785, 913],
    'zipCodes'  => []
  ],
  'Kentucky' => [
    'country'   => 'USA',
    'regionCode' => 'KY',
    'areaCodes' => [270, 364, 502, 606, 859],
    'zipCodes'  => []
  ],
  'Louisiana' => [
    'country'   => 'USA',
    'regionCode' => 'LA',
    'areaCodes' => [225, 318, 337, 504, 985],
    'zipCodes'  => []
  ],
  'Maine' => [
    'country'   => 'USA',
    'regionCode' => 'ME',
    'areaCodes' => [207],
    'zipCodes'  => []
  ],
  'Maryland' => [
    'country'   => 'USA',
    'regionCode' => 'MD',
    'areaCodes' => [240, 301, 410, 443],
    'zipCodes'  => []
  ],
  'Massachusetts' => [
    'country'   => 'USA',
    'regionCode' => 'MA',
    'areaCodes' => [339, 351, 413, 508, 617, 774, 781, 857, 978],
    'zipCodes'  => []
  ],
  'Michigan' => [
    'country'   => 'USA',
    'regionCode' => 'MI',
    'areaCodes' => [231, 248, 269, 313, 517, 586, 616, 734, 810, 906, 947,
                    989],
    'zipCodes'  => []
  ],
  'Minnesota' => [
    'country'   => 'USA',
    'regionCode' => 'MN',
    'areaCodes' => [218, 320, 507, 612, 651, 763, 952],
    'zipCodes'  => []
  ],
  'Mississippi' => [
    'country'   => 'USA',
    'regionCode' => 'MS',
    'areaCodes' => [228, 601, 662, 769],
    'zipCodes'  => []
  ],
  'Missouri' => [
    'country'   => 'USA',
    'regionCode' => 'MO',
    'areaCodes' => [314, 417, 573, 636, 660, 816],
    'zipCodes'  => []
  ],
  'Montana' => [
    'country'   => 'USA',
    'regionCode' => 'MT',
    'areaCodes' => [406],
    'zipCodes'  => []
  ],
  'Nebraska' => [
    'country'   => 'USA',
    'regionCode' => 'NE',
    'areaCodes' => [308, 402],
    'zipCodes'  => []
  ],
  'Nevada' => [
    'country'   => 'USA',
    'regionCode' => 'NV',
    'areaCodes' => [702, 725, 775],
    'zipCodes'  => []
  ],
  'New Hampshire' => [
    'country'   => 'USA',
    'regionCode' => 'NH',
    'areaCodes' => [603],
    'zipCodes'  => []
  ],
  'New Jersey' => [
    'country'   => 'USA',
    'regionCode' => 'NJ',
    'areaCodes' => [201, 551, 609, 732, 848, 856, 862, 908, 973],
    'zipCodes'  => []
  ],
  'New Mexico' => [
    'country'   => 'USA',
    'regionCode' => 'NM',
    'areaCodes' => [505, 575],
    'zipCodes'  => []
  ],
  'New York' => [
    'country'   => 'USA',
    'regionCode' => 'NY',
    'areaCodes' => [212, 315, 347, 516, 518, 585, 607, 631, 646, 716, 718,
                    845, 914, 917, 929, 934],
    'zipCodes'  => []
  ],
  'North Carolina' => [
    'country'   => 'USA',
    'regionCode' => 'NC',
    'areaCodes' => [252, 336, 704, 743, 828, 910, 919, 980, 984],
    'zipCodes'  => []
  ],
  'North Dakota' => [
    'country'   => 'USA',
    'regionCode' => 'ND',
    'areaCodes' => [701],
    'zipCodes'  => []
  ],
  'Ohio' => [
    'country'   => 'USA',
    'regionCode' => 'OH',
    'areaCodes' => [216, 220, 234, 330, 380, 419, 440, 513, 567, 614, 740,
                    937],
    'zipCodes'  => []
  ],
  'Oklahoma' => [
    'country'   => 'USA',
    'regionCode' => 'OK',
    'areaCodes' => [405, 539, 580, 918],
    'zipCodes'  => []
  ],
  'Oregon' => [
    'country'   => 'USA',
    'regionCode' => 'OR',
    'areaCodes' => [458, 503, 541, 971],
    'zipCodes'  => []
  ],
  'Pennsylvania' => [
    'country'   => 'USA',
    'regionCode' => 'PA',
    'areaCodes' => [215, 267, 272, 412, 484, 570, 610, 717, 724, 814, 878],
    'zipCodes'  => []
  ],
  'Rhode Island' => [
    'country'   => 'USA',
    'regionCode' => 'RI',
    'areaCodes' => [401],
    'zipCodes'  => []
  ],
  'South Carolina' => [
    'country'   => 'USA',
    'regionCode' => 'SC',
    'areaCodes' => [803, 843, 854, 864],
    'zipCodes'  => []
  ],
  'South Dokota' => [
    'country'   => 'USA',
    'regionCode' => 'SD',
    'areaCodes' => [605],
    'zipCodes'  => []
  ],
  'Tennessee' => [
    'country'   => 'USA',
    'regionCode' => 'TN',
    'areaCodes' => [423, 615, 629, 731, 865, 901, 931],
    'zipCodes'  => []
  ],
  'Texas' => [
    'country'   => 'USA',
    'regionCode' => 'TX',
    'areaCodes' => [210, 214, 254, 281, 325, 346, 361, 409, 430, 432, 469,
                    512, 682, 713, 737, 806, 817, 830, 832, 903, 915, 936,
                    940, 956, 972, 979],
    'zipCodes'  => []
  ],
  'Utah' => [
    'country'   => 'USA',
    'regionCode' => 'UT',
    'areaCodes' => [385, 435, 801],
    'zipCodes'  => []
  ],
  'Vermont' => [
    'country'   => 'USA',
    'regionCode' => 'VT',
    'areaCodes' => [802],
    'zipCodes'  => []
  ],
  'Virginia' => [
    'country'   => 'USA',
    'regionCode' => 'VA',
    'areaCodes' => [276, 434, 540, 571, 703, 757, 804],
    'zipCodes'  => []
  ],
  'Washington' => [
    'country'   => 'USA',
    'regionCode' => 'WA',
    'areaCodes' => [206, 253, 360, 425, 509],
    'zipCodes'  => []
  ],
  'West Virginia' => [
    'country'   => 'USA',
    'regionCode' => 'WV',
    'areaCodes' => [304, 681],
    'zipCodes'  => []
  ],
  'Wisconson' => [
    'country'   => 'USA',
    'regionCode' => 'WI',
    'areaCodes' => [262, 414, 608, 715, 920],
    'zipCodes'  => []
  ],
  'Wyoming' => [
    'country'   => 'USA',
    'regionCode' => 'WY',
    'areaCodes' => [307],
    'zipCodes'  => []
  ],
  'Washington DC' => [
    'country'   => 'USA',
    'regionCode' => 'DC',
    'areaCodes' => [202],
    'zipCodes'  => []
  ],

  // Canada
  'Alberta' => [
    'country'   => 'Canada',
    'regionCode' => 'AB',
    'areaCodes' => [403,587,780],
    'zipCodes'  => []
  ],
  'British Columbia' => [
    'country'   => 'CAN',
    'regionCode' => 'BC',
    'areaCodes' => [236,250,604,778],
    'zipCodes'  => []
  ],
  'Manitoba' => [
    'country'   => 'CAN',
    'regionCode' => 'MB',
    'areaCodes' => [204,431],
    'zipCodes'  => []
  ],
  'New Brunswick' => [
    'country'   => 'CAN',
    'regionCode' => 'NB',
    'areaCodes' => [506],
    'zipCodes'  => []
  ],
  'Newfoundland' => [
    'country'   => 'CAN',
    'regionCode' => 'NL',
    'areaCodes' => [709],
    'zipCodes'  => [],
  ],
  'Northwest Territories' => [
    'country'   => 'CAN',
    'regionCode' => 'NT',
    'areaCodes' => [867],
    'zipCodes'  => []
  ],
  'Nova Scotia' => [
    'country'   => 'CAN',
    'regionCode' => 'NS',
    'areaCodes' => [902],
    'zipCodes'  => []
  ],
  'Nunavut' => [
    'country'   => 'CAN',
    'regionCode' => 'NU',
    'areaCodes' => [867],
    'zipCodes'  => []
  ],
  'Ontario' => [
    'country'   => 'CAN',
    'regionCode' => 'ON',
    'areaCodes' => [226,249,289,343,416,437,519,613,647,705,807,905],
    'zipCodes'  => []
  ],
  'Quebec' => [
    'country'   => 'CAN',
    'regionCode' => 'QC',
    'areaCodes' => [418,438,450,514,579,581,819,873],
    'zipCodes'  => []
  ],
  'Saskatchewan' => [
    'country'   => 'CAN',
    'regionCode' => 'SK',
    'areaCodes' => [306,639],
    'zipCodes'  => []
  ],
  'Yukon' => [
    'country'   => 'CAN',
    'regionCode' => 'YT',
    'areaCodes' => [867],
    'zipCodes'  => []
  ],

  'US/Canada' => [
    'country'   => ['USA','CAN'],
    'regioncode' => false,
    /*
     * 822,833,880-887, and 889 are expected in future expansions for US/Canada Toll Free
     * 900 is for premium charge numbers.
     */
    'areaCodes' => [800,822,833,844,855,866,877,880,881,882,883,884,885,886,887,888,889,900],
    'zipCodes'  => []
  ],
  'Mexico' => [
    'country'   => 'MEX',
    'regionCode' => false,
    'areaCodes' => [899],
    'zipCodes'  => []
  ]
];

/*
 * getDataByCode() - Takes a 2-letter region code and returns region data.
 */
protected static function getDataByCode( $code ) {
  foreach (self::$regionData as $regionName => $data) {
    if ($code === $data['regionCode']) {
      $data['regionName'] = $regionName;
      return $data;
    }
  }
  return false;
}

/*
 * getDataByRegionName() - Takes a region name and returns region data.
 */
protected static function getDataByRegionName( $name ) {
  foreach (self::$regionData as $regionName => $data) {
    if (strtolower($name) === strtolower($regionName)) {
      $data['regionName'] = $regionName;
      return $data;
    }
  }
  return false;
}

/*
 * getDataByAreaCode() - Takes an area code and returns region data.
 */
protected static function getDataByAreaCode( $areaCode ) {
  foreach (self::$regionData as $regionName => $data) {
    if (in_array($areaCode, $data['areaCodes'])) {
      $data['regionName'] = $regionName;
      return $data;
    }
  }
  return false;
}

/*
 * getDataByZipCode() - Takes a Zip code and returns region data
 */
protected static function getDataByZipCode( $zipCode ) {
  foreach (self::$regionData as $regionName => $data) {
    if (in_array($zipCode, $data['zipCodes'])) {
      $data['regionName'] = $regionName;
      return $data;
    }
  }
  return false;
}

/*
 * searchData() - Takes a name, code, zip, or area code and returns the
 *                region data or false.
 */
public static function searchData( $searchTerm ) {
  if (is_string($searchTerm) && strlen($searchTerm) === 2) {
    return self::getDataByRegionCode($searchTerm);
  }
  if (is_string($searchTerm) && strlen($searchTerm) === 3
  && is_numeric($searchTerm)) {
    return self::getDataByAreaCode(intval($searchTerm));
  }
  if (is_string($searchTerm) && strlen($searchTerm) === 5
  && is_numeric($searchTerm)) {
    return self::getDataByZipCode($searchTerm);
  }
  if (is_string($searchTerm)) {
    return self::getDataByRegionName($searchTerm);
  }
  if (is_int($searchTerm) && $searchTerm < 1000) {
    return self::getDataByAreaCode($searchTerm);
  }
  if (is_int($searchTerm)) {
    return self::getDataByZipCode($searchTerm);
  }
  return false;
}

/*
 * getRegionCode() - Takes an area code, zip code, or region name and
 *                  returns a region code or false.
 */

public static function getRegionCode( $searchTerm ) {
  if ($regionData = self::searchData($searchTerm)) {
    return $regionData['regionCode'];
  }
  return false;
}

/*
 * getRegionName() - Takes an area code, zip code, or region code and
 *                  returns a region name or false.
 */

public static function getRegionName( $searchTerm ) {
  if ($regionData = self::searchData($searchTerm)) {
    return $regionData['regionName'];
  }
  return false;
}

/*
 * getCountry() - Takes an area code, zip code, or region code and
 *                  returns a region name or false.
 */

public static function getCountry( $searchTerm ) {
  if ($regionData = self::searchData($searchTerm)) {
    return $regionData['country'];
  }
  return false;
}

}
<?php

require_once CONF.'/logger.php';
require_once LIB.'/resolve_path.php';

class Logger {
  private static $LogFile;
  private static $TopFile;

  public static function Init ( $topfile ) {

    self::$TopFile = $topfile;

    if (file_exists(resolve_path(Config\Logger::$Directory, 'lastrotate.json'))) {
      $last_rotate = json_decode(file_get_contents(resolve_path(Config\Logger::$Directory, 'lastrotate.json')));
    }
    if (empty($last_rotate)) { $last_rotate = []; }
    if (empty($last_rotate[self::$TopFile])) { $last_rotate[self::$TopFile] = strtotime('now'); }

    switch (strtolower(Config\Logger::$RotationFrequency)) {
    case 'monthly':
      $freq = 'month';
      break;
    case 'weekly':
      $freq = 'weekly';
      break;
    case 'daily':
    default:
      $freq = 'day';
    }

    $next_rotate = strtotime(
      Config\Logger::$RotateAtTime
     .' '
     .date('Y-m-d', strtotime(date('Y-m-d',$last_rotate[self::$TopFile])." + 1 $freq"))
    );

    if (
       file_exists(resolve_path(Config\Logger::$Directory, self::$TopFile.'.log'))
    && (
         filesize(resolve_path(Config\Logger::$Directory, self::$TopFile.'.log')) >= Config\Logger::$RotateAtLength
       || strtotime('now') > $next_rotate
       )
    ) {
      self::Rotate();
    }
    self::Open();
  }

  public static function Close () {
    fclose (self::$LogFile);
    self::$LogFile = null;
  }

  public static function Log ( $message ) {
    if (fstat(self::$LogFile)['nlink'] == 0) {
      usleep(200000);
      self::Close();
      self::Open();
    }
    fwrite(self::$LogFile,date('Y-m-d H:i:s T')." | ".$message."\n");
  }

  public static function Warn ( $message ) {
    self::Log("WARNING: $message");
  }

  public static function Error ( $message ) {
    self::Log("ERROR: $message");
    self::Close();
    die();
  }

  public static function Open () {
    if (self::$LogFile) { return true; }
    if (!is_dir(Config\Logger::$Directory)) {
      mkdir(Config\Logger::$Directory);
    }

    self::$LogFile = fopen(
      resolve_path(
        Config\Logger::$Directory,
        self::$TopFile.".log"
      ),
      'a+'
    );
  }

  public static function Rotate ( $depth = 0 ) {
    if (self::$LogFile) { self::Close(); }

    if (file_exists(resolve_path(Config\Logger::$Directory, self::$TopFile.'.'.($depth+1).'.gz'))) {
      self::Rotate($depth+1);
    }
    if ($depth == Config\Logger::$LogDepth) {
      unlink(resolve_path(Config\Logger::$Directory, self::$TopFile.'.'.$depth.'.gz'));
      return;
    }
    if ($depth > 0) {
      rename(
        resolve_path(Config\Logger::$Directory, self::$TopFile.'.'.$depth.'.gz'),
        resolve_path(Config\Logger::$Directory, self::$TopFile.'.'.($depth+1).'.gz')
      );
      return;
    }

    $gzh = gzopen( resolve_path(Config\Logger::$Directory, self::$TopFile.'.1.gz'), 'w9' );
    gzwrite($gzh, file_get_contents(resolve_path(Config\Logger::$Directory, self::$TopFile.'.log')));
    gzclose($gzh);
    unlink(resolve_path(Config\Logger::$Directory, self::$TopFile.'.log'));
    self::Open();

  }
}
<?php

require_once __DIR__.'/../bootstrap.php';
require_once LIB.'/db.php';
require_once LIB.'/logger.php';
require_once VENDOR.'/plivo.php';
require_once CONF.'/plivo.php';

class Phone {

  private static $PlivoApp;
  private static $PlivoMaster;

  private static $Statements = [];

  /*
   * Sets up or returns App's Plivo instance. Call in methods that use
   * standard APIs.
   */
  private static function SetupApp() {
    if (self::$PlivoApp) { return true; }
    self::$PlivoApp = new RestAPI(
      Config\Plivo::$AppAuthId,
      Config\Plivo::$AppAuthToken
    );
  }

  /*
   * Sets up or returns Master Plivo instance. Call in methods that require
   * master Plivo account API access. I.E. for number rental.
   */
  private static function SetupMaster() {
    if (self::$PlivoMaster) { return true; }
    self::$PlivoMaster = new RestAPI(
      Config\Plivo::$MasterAuthId,
      Config\Plivo::$MasterAuthToken
    );
  }

  /*
   * Add message to SMS queue. Takes array of parameters.
   * $params = [
   *   'destination' => <fully qualified phone number>, REQUIRED
   *   'message' => <message string>, REQUIRED
   *   'from' => <fully qualified phone number>, OPTIONAL
   *   'send_at' => <timestamp>, OPTIONAL
   * ];
   *
   * Uses: DB(db.php)
   */
  public static function QueueSMS( $params ) {
    $statement = DB::Prepare(
<<<SQL
      INSERT INTO `sms_queue`
      SET
        `destination` = :destination,
        `message` = :message;
SQL
    );
    $statement->execute($params);

  }

  //SIP endpoint functions

  /*
   * GetUserSIPEndpoint - Retrieves user's stored Plivo SIP endpoint. Takes a
   * user key in the form of a string and returns an array or false.
   *
   * Uses DB(db.php)
   */
  public static function GetUserSIPEndpoint( $user ) {
    $statement = DB::Prepare("SELECT `sip_user`, `sip_password` FROM `users` WHERE `key` = UNHEX(?) LIMIT 1");
    $statement->execute([$user]);
    if ($row = $statement->fetch(PDO::FETCH_OBJ)) {
      if (empty($row->sip_user)) { return false; }
      return [
        "url"  => "sip:{$row->sip_user}@phone.plivo.com",
        "user" => $row->sip_user,
        "pass" => $row->sip_password
      ];
    }
    return false;
  }

  /*
   * GetOrCreateSIPEndpoint - Takes a user key and, if necessary, creates,
   * assigns, names, and stores a new Plivo SIP endpoint. Returns an array.
   * Throws an Exception if it can't return the SIP details.
   *
   * Uses: DB(db.php), Plivo App(self::$PlivoApp)
   */
  public static function GetOrCreateSIPEndpoint( $user ) {
    self::SetupApp();
    if ($sip_details = self::GetUserSIPEndpoint($user)) {return $sip_details; }
    $statement = DB::Prepare("SELECT HEX(`key`) as `key`, `name` FROM `users` WHERE `key` = UNHEX(?) LIMIT 1");
    $statement->execute([$user]);
    if ( !($user = $statement->fetch(PDO::FETCH_OBJ)) ) { throw new Exception("Plivo SIP endpoint requested for invalid user."); }

    $sip_user = str_replace(' ', '',$user->name);
    $sip_pass = '';
    for ($i = 0; $i < 16; $i++) {
      $sip_pass .= mt_rand(0,9);
    }

    $response = self::$PlivoApp->create_endpoint([
      'username' => $sip_user,
      'password' => $sip_pass,
      'alias'    => "LRM U ".$user->name,
      'app_id'   => Config\Plivo::$AppId
    ]);
    if ($response['status'] < 200 || $response['status'] >= 300) {
      trigger_error(json_encode($response));
      throw new Exception('Error creating endpoint.');
    }
    $statement = DB::Prepare("UPDATE `users` SET `sip_user` = :user, `sip_password` = :pass WHERE `key` = UNHEX(:key)");
    $statement->execute([
      'user' => $response['response']['username'],
      'pass' => $sip_pass,
      'key'  => $user->key
    ]);
    return [
      "url"  => "sip:{$response['response']['username']}@phone.plivo.com",
      "user" => $response['response']['username'],
      "pass" => $sip_pass
    ];

  }

  //Number functions

  /*
   * GetUserPlivoNumber - Retrieves user's stored plivo number. Takes a
   * user key in the form of a string and returns an array or false.
   *
   * Uses DB(db.php)
   */
  public static function GetUserPlivoNumber( $user ) {

    $statement = DB::Prepare("SELECT `plivo_number` FROM `users` WHERE `key` = UNHEX(?) LIMIT 1");
    $statement->execute([$user]);
    if ($row = $statement->fetch(PDO::FETCH_OBJ)) {
      return $row->plivo_number;
    }

    return false;
  }

  /*
   * GetOrRentUserPlivoNumber - Takes user key and if necessary, rents, assigns,
   * names, and stores a new Plivo number. Returns a number. Throws an Exception
   * if it can't return a number.
   *
   * Uses: DB(db.php), Phone(self)
   */
  public static function GetOrRentUserPlivoNumber( $user, $search = "Utah" ) {

    if ($number = self::GetUserPlivoNumber($user)) { return $number; }
    $statement = DB::Prepare("SELECT HEX(`key`) as `key`, `name` FROM `users` WHERE `key` = UNHEX(?) LIMIT 1");
    $statement->execute([$user]);
    if ( !($user = $statement->fetch(PDO::FETCH_OBJ)) ) { throw new Exception("Plivo number requested for invalid user."); }

    if ( !($number_data = self::FindAcceptableNumber( $search )) ) { throw new Exception("Unable to find an acceptable number for {$user->name}."); }

    $number = $number_data['number'];

    if (!self::RentNumber($number)) { throw new Exception("Unable to rent number '$number'."); }
    if (!self::AssignNumberToUser($number,$user)) { throw new Exception("Unable to assign number '$number' to {$user->name}."); }

    return $number;
  }


  /*
   * FindAcceptableNumber - Takes an optional region as a string or or
   * area code as a digit and returns the details of first number from
   * plivo that meets pricing requirements as an array or false.
   *
   * Uses: Plivo Master(self::$PlivoMaster)
   */
  public static function FindAcceptableNumber ( $region = false ) {

    self::SetupMaster();

    $params = [
      'country_iso' => 'US',
      'services' => 'voice,sms',
      'limit' => 1
    ];
    if ($region && is_int($region) && $region < 1000) {
      $params['pattern'] = (string) $region;
    }
    if ($region && is_string($region) && is_numeric($region) && (int) $region < 1000) {
      $params['pattern'] = $region;
    }
    if ($region && is_string($region) && !is_numeric($region) && $region !== 'tollfree') {
      $params['region'] = $region;
    }
    if ($region === 'tollfree') {
      unset ($params['services']);
      $params['type'] = 'tollfree';
    }
    if ($region && !array_key_exists('region',$params) && !array_key_exists('pattern',$params) && $region !== 'tollfree') {
      throw new Exception('Invalid parameter passed to Phone::FindAcceptableNumber. Most likely tried to pass invalid area code');
      return false;
    }

    while (true) {
      $response = self::$PlivoMaster->search_phone_numbers($params);
      if ($response['status'] < 200 || $response['status'] >= 300) { return false; }
      $response = $response['response'];
      foreach ($response['objects'] as $key => $number_data) {
        if ( $region !== 'tollfree' && (
          (float) $number_data['monthly_rental_rate'] > 0.8 ||
          $number_data['restriction'] !== null ||
          (float) $number_data['setup_rate'] > 0 ||
          (float) $number_data['sms_rate'] > 0 ||
          (float) $number_data['voice_rate'] > 0.0085
        ) ) {
          continue;
        }
        return $number_data;
      }
      if ($response['meta']['offset'] < $response['meta']['total_count']) {
        $params['offset'] = $response['meta']['offset'] + 1;
      } else {
        return false;
      }

    }

  }

  /*
   * RentNumber - Rents a number and returns the number or false.
   *
   * Uses: Plivo Master(self::$PlivoMaster)
   */
  public static function RentNumber( $number ) {

    self::SetupMaster();

    if (!self::IsValidUSNumber($number)) {
      throw new Exception("Invalid Number");
    }

    $response = self::$PlivoMaster->buy_phone_number([
      'number' => $number
    ]);

    if ($response['status'] < 200 || $response['status'] >= 300) {
      throw new Exception("Error renting number");
    }
    if (!self::AssignNumberToApp($number,Config\Plivo::$AppId)) { return false; }
    if (!self::AssignNumberToSubaccount($number,Config\Plivo::$AppAuthId)) { return false; }
    if (!self::SaveNumberToDatabase($number)) { return false; }
    if (!self::AssignNumberToQueue($number,1)) { return false; }
    return true;
  }

  /*
   * UnrentNumber - Unrents a number and returns the number or false.
   *
   * Uses: Plivo Master(self::$PlivoMaster)
   */
  public static function UnrentNumber( $number ) {

    self::SetupMaster();

    if (!self::IsValidUSNumber($number)) {
      throw new Exception("Invalid Number");
    }

    $response = self::$PlivoMaster->unrent_number([
      'number' => $number
    ]);

    if ($response['status'] < 200 || $response['status'] >= 300) {
      throw new Exception("Error unrenting number");
    }
    try {
      $owned = self::VerifyNumberOwnership($number);
    } catch (Exception $e) {
      throw new Exception("Error verifying that number has be unrented: ".$e->getMessage());
    }
    return true;
  }

  /*
   * AssignNumberToApp - Takes a phone number and a Plivo app id. Assignes the
   * number to the app.
   *
   * Uses: Plivo Master(self::$PlivoMaster)
   */
  public static function AssignNumberToApp( $number, $app ) {

    self::SetupMaster();

    if (!self::IsValidUSNumber($number)) {
      throw new Exception("Invalid Number");
      return false;
    }

    $response = self::$PlivoMaster->modify_number([
      'number' => $number,
      'app_id' => $app
    ]);
    if ($response['status'] < 200 || $response['status'] >= 300) {
      throw new Exception("Error adding app to number");
      return false;
    }
    return true;
  }

  /*
   * AssignNumberToSubaccount - Takes a number and a Plivo subaccount id.
   * Assigns the number to the subaccount.
   *
   * Uses: Plivo Master(self::$PlivoMaster)
   */
  public static function AssignNumberToSubaccount( $number, $account ) {

    self::SetupMaster();

    if (!self::IsValidUSNumber($number)) {
      throw new Exception("Invalid Number");
      return false;
    }

    $response = self::$PlivoMaster->modify_number([
      'number' => $number,
      'subaccount' => $account
    ]);
    if ($response['status'] < 200 || $response['status'] >= 300) {
      throw new Exception("Error adding subaccount to number");
      return false;
    }
    return true;
  }

  /*
   * NameNumber - Adds an alias to a number takes a number and an alias.
   *
   * Uses: Plivo Master(self::$PlivoMaster)
   */
  public static function NameNumber( $number, $alias ) {

    self::SetupMaster();

    if (!self::IsValidUSNumber($number)) {
      throw new Exception("Invalid Number");
      return false;
    }

    $response = self::$PlivoMaster->modify_number([
      'number' => $number,
      'alias' => $alias
    ]);
    if ($response['status'] < 200 || $response['status'] >= 300) {
      throw new Exception("Error adding alias to number");
      return false;
    }
    return true;
  }

  /*
   * AssignUserNumber - Assigns a number to a specific user.
   * takes a key or id and a number returns true or false.
   *
   * Uses: DB(db.php)
   */
  public static function AssignNumberToUser( $number, $user ) {

    $statement = DB::Prepare("UPDATE `users` SET `plivo_number` = :number WHERE `key` = UNHEX(:user_key) LIMIT 1;");

    $statement->execute([
      'number' => $number,
      'user_key' => $user->key
    ]);

    if (!self::AssignNumberToQueue($number,2)) { return false; }
    if (!self::NameNumber($number,"LRM-U {$user->name}")) { throw new Exception("Unable to name plivo number '$number'. Should be 'LRM-U {$user->name}'"); }

    if ($number == self::GetUserPlivoNumber($user->key)) {
      return true;
    } else {
      return false;
    }
  }

  /*
   * SaveNumberToDatabase - Saves a number to the database.
   * takes a number and returns true or false.
   *
   * Uses: DB(db.php), Phone(this file)
   */
  public static function SaveNumberToDatabase( $number ) {

    if (!self::IsValidUSNumber($number)) {
      throw new Exception("Invalid Phone Number");
      return false;
    }

    try {
      $statement = DB::Prepare("INSERT INTO `plivo_numbers` SET `number` = ?, `queue` = 1");
      $statement->execute([$number]);
    } catch (PDOException $e) {
      throw new Exception("Could not save number to database.");
    }

    return true;

  }

  /*
   * RemoveNumberFromDatabase - Removes a number from the database.
   * Takes a number and returns true or false.
   *
   * Uses: DB(db.php), Phone(this file)
   */
  public static function RemoveNumberFromDatabase ( $number ) {

    if (!self::IsValidUSNumber($number)) {
      throw new Exception("Invalid Phone Number");
      return false;
    }

    try {
      $statement = DB::Prepare("SELECT * FROM `plivo_numbers` WHERE `number` = ?");
      $statement->execute([$number]);
    } catch (PDOException $e) {
      throw new Exception("Error checking for number: ".$e->getMessage());
    }
    if (!($result =  $statement->fetch(PDO::FETCH_OBJ))) {
      throw new Exception("Cannot remove number from database. It's not in there.");
    }

    try {
      $statement = DB::Prepare("SELECT HEX(`key`) as `key` FROM `users` WHERE `plivo_number` = ?");
      $statement->execute([$number]);
    } catch (PDOException $e) {
      throw new Exception("Error checking if number is assigned to a user: ".$e->getMessage());
    }
    if ($user = $statement->fetch(PDO::FETCH_OBJ)) {
      try {
        $statement = DB::Prepare("UPDATE `users` SET `plivo_number` = NULL WHERE `key` = UNHEX(?)");
        $statement->execute([$user->key]);
      } catch (PDOException $e) {
        throw new Exception("Error removing number '$number' from user '{$user->key}': ".$e->getMessage());
      }
    }

    try {
      $statement = DB::Prepare("DELETE FROM `plivo_numbers` WHERE `number` = ?");
      $statement->execute([$number]);
    } catch (PDOException $e) {
      throw new Exception("Error removing number from database: ".$e->getMessage());
    }

    return true;

  }

  /*
   * VerifyNumberOwnership - Takes a number. Checks Plivo Master for ownership.
   *
   * Uses DB(db.php) Plivo Master(self::$PlivoMaster)
   */
  public static function VerifyNumberOwnership ( $number ) {
    self::SetupMaster();
    if (!self::IsValidUSNumber($number)) { throw new Exception("Not a valid US number."); }

    $success_sth = DB::Prepare("UPDATE `plivo_numbers` SET `checked` = CURRENT_TIMESTAMP(), `owned` = (1) WHERE `number` = ?");
    $fail_sth = DB::Prepare("UPDATE `plivo_numbers` SET `checked` = CURRENT_TIMESTAMP(), `owned` = (0), `in_account` = (0), `in_app` = (0) WHERE `number` = ?");

    $response = self::$PlivoMaster->get_numbers(["number_startswith" => $number]);
    if ($response['status'] < 200 || $response['status'] >= 300) { throw new Exception("Unable to connect to Plivo Master."); }
    if (count($response['response']['objects']) > 0) {
      $success_sth->execute([$number]);
      return true;
    }
    $fail_sth->execute([$number]);
    return false;
  }
  /*
   * VerifyNumberAccount - Takes a number. Checks Plivo App for ownership by
   * Subaccount.
   *
   * Uses DB(db.php) Plivo App(self::$PlivoApp)
   */
  public static function VerifyNumberAccount ( $number ) {
    self::SetupApp();
    if (!self::IsValidUSNumber($number)) { throw new Exception("Not a valid US number."); }

    $success_sth = DB::Prepare("UPDATE `plivo_numbers` SET `checked` = CURRENT_TIMESTAMP(), `owned` = (1), `in_account` = (1) WHERE `number` = ?");
    $fail_sth = DB::Prepare("UPDATE `plivo_numbers` SET `checked` = CURRENT_TIMESTAMP(), `in_account` = (0), `in_app` = (0) WHERE `number` = ?");

    $response = self::$PlivoApp->get_numbers(["number_startswith" => $number]);
    if ($response['status'] < 200 || $response['status'] >= 300) { throw new Exception("Unable to connect to Plivo App."); }
    if (count($response['response']['objects']) > 0) {
      $success_sth->execute([$number]);
      return true;
    }
    $fail_sth->execute([$number]);
    return false;
  }
  /*
   * GetAllAccountNumbers - Takes a number. Checks Plivo App for ownership by
   * Subaccount.
   *
   * Uses DB(db.php) Plivo App(self::$PlivoApp)
   */
  public static function GetAllAccountNumbers ( ) {
    self::SetupApp();

    $numbers = [];
    $offset = 0;
    while (1) {
      $response = self::$PlivoApp->get_numbers(['offset'=>$offset]);
      if ($response['status'] < 200 || $response['status'] >= 300) { throw new Exception("Unable to connect to Plivo App."); }
      foreach ($response['response']['objects'] as $number_data) {
        $numbers[] = [
          'number' => $number_data['number'],
          'alias' => $number_data['alias']
        ];
      }
      $offset = count($numbers);
      if ($response['response']['meta']['offset']+count($response['response']['objects']) >= $response['response']['meta']['total_count']) {
        return $numbers;
      }
    }
  }
  /*
   * VerifyNumberApp - Takes a number. Checks Plivo App for correct application.
   *
   * Uses DB(db.php) Plivo App(self::$PlivoApp)
   */
  public static function VerifyNumberApp ( $number ) {
    self::SetupApp();
    if (!self::IsValidUSNumber($number)) { throw new Exception("Not a valid US number."); }

    $success_sth = DB::Prepare("UPDATE `plivo_numbers` SET `checked` = CURRENT_TIMESTAMP(), `owned` = (1), `in_account` = (1), `in_app` = (1) WHERE `number` = ?");
    $fail_sth = DB::Prepare("UPDATE `plivo_numbers` SET `checked` = CURRENT_TIMESTAMP(), `in_app` = (0) WHERE `number` = ?");

    $response = self::$PlivoApp->get_numbers(["number_startswith" => $number]);
    if ($response['status'] < 200 || $response['status'] >= 300) { throw new Exception("Unable to connect to Plivo App."); }
    if (count($response['response']['objects']) == 0) {
      $fail_sth->execute([$number]);
      return false;
    }
    if (strpos($response['response']['objects'][0]['application'],Config\Plivo::$AppId)) {
      $success_sth->execute([$number]);
      return true;
    }
    $fail_sth->execute([$number]);
    return false;
  }

  /*
   * ReassignUserPlivoNumber - Takes a user key, moves the current number
   * into a group named '<user name> aliases'(which will be created if it does
   * not exist), and then purchases and assigns a new number to that user.
   * Optionally takes a search term. Returns the new number or, in the event no
   * suitable number was found, returns false.
   *
   * Note: Generally this is used to get a new number more suited to a rep's
   * needs, so - even though the search term is optional - it should be used
   * in most cases. Search term can be a City, State, Area-Code or Prefix.
   *
   * Uses: DB(db.php) Phone(self)
   */
  public static function ReassignUserPlivoNumber( $user, $search = 'Utah' ) {
    try {
      $get_sth = DB::Prepare("SELECT HEX(`key`) as `key`, `name`, `plivo_number` FROM `users` WHERE `key` = UNHEX(?)");
      $get_sth->execute([$user]);
    } catch (PDOException $e) {
      throw new Exception("An error was encountered while retrieving user to re-assign: ".$e->getMessage());
    }
    if (!($user = $get_sth->fetch(PDO::FETCH_OBJ))) { throw new Exception ("Unable to reassign user plivo number. User does not exist."); }

    if (!self::FetchQueue("{$user->name} aliases")) {
      self::CreateQueue("{$user->name} aliases");
    }

    self::AssignNumberToQueue($user->plivo_number,"{$user->name} aliases");

    try {
      $clear_sth = DB::Prepare("UPDATE `users` SET `plivo_number` = NULL WHERE `key` = UNHEX(?)");
      $clear_sth->execute([$user->key]);
    } catch (PDOException $e) {
      throw new Exception("An error was encountered while trying to clear {$user->name}'s plivo number: ".$e->getMessage());
    }

    $new_number = self::GetOrRentUserPlivoNumber($user->key, $search);
    return $new_number;
  }

  /*
   * AssignNumberToQueue - Assigns a number to a queue. Takes a number and a
   * queue(name or id) returns true. Throws exception on failure.
   *
   * Uses: DB(db.php), Phone(this file)
   */
  public static function AssignNumberToQueue ( $number, $queue_identifier ) {

    if (!self::IsValidUSNumber($number)) { throw new Exception("Invalid Phone Number"); }

    if(!($queue = self::GetQueueDetails($queue_identifier))) {
      throw new Exception ("Could not get details for queue '$queue_identifier'.");
    }

    try {
      $statement = DB::Prepare("UPDATE `plivo_numbers` SET `queue` = :queue WHERE `number` = :number");
      $statement->execute([
        "number" => $number,
        "queue"  => $queue->id
      ]);
    } catch (PDOException $e) {
      throw new Exception ("Could not update number's queue value in database: ".$e->getMessage());
    }

    if ($queue->name !== 'user') {
      if (!self::VerifyNumberOwnership($number)) { throw new Exception('Number not owned'); }
      if (!self::NameNumber($number,"LRM-Q {$queue->id}")) {
        throw new Exception ("Could not name $number.");
      }
    }

    return true;

  }

  /*
   * CreateQueue - Create a queue in the database. Takes a name and, optionally
   * a queue id. Returns the queue id or false.
   *
   * Uses DB(db.php), Phone(this file)
   */
  public static function CreateQueue ( $args ) {

    $queue_name = false;
    $queue_id = false;
    $queue_users = false;
    $queue_type = 'ring_all';
    $queue_timeout = 120;
    $queue_next_action = 'hangup';

    if (is_string($args) && !is_numeric($args)) { $queue_name = $args; }
    if (is_int($args) || (is_string($args) && is_numeric($args))) { $queue_id = (int)$args; }
    if (is_array($args) || is_object($args)) {
      foreach ($args as $key => $value) {
        if ($key == 'name') { $queue_name = $value; }
        if ($key == 'id') { $queue_id = (int)$value; }
        if ($key == 'users') { $queue_users = $value; }
        if ($key == 'type') { $queue_type = $value; }
        if ($key == 'timeout') { $queue_timeout = $value; }
        if ($key == 'next_action') { $queue_next_action = $value; }
      }
    }

    if ($queue_name === false  && $queue_id === 1) { $queue_name = 'default'; }
    if ($queue_name == 'default' && $queue_id === false) { $queue_id = 1; }
    if ($queue_name == 'default' && $queue_id != 1) { throw new Exception("'default' is a special queue and must be queue #1."); }
    if ($queue_name !== 'default' && $queue_id == 1) { throw new Exception("Queue #1 is a special queue and must be named 'default'."); }

    if ($queue_name === false  && $queue_id === 2) { $queue_name = 'user'; }
    if ($queue_name == 'user' && $queue_id === false) { $queue_id = 2; }
    if ($queue_name == 'user' && $queue_id != 2) { throw new Exception("'user' is a special queue and must be queue #2."); }
    if ($queue_name !== 'user' && $queue_id == 2) { throw new Exception("Queue #2 is a special queue and must be named 'user'."); }

    if (!$queue_name) { throw new Exception ("Queue must have a name"); }

    if ($queue_id !== false && self::FetchQueue($queue_id)) {
        throw new Exception("A queue with id '$queue_id' already exists.");
    }

    if (self::FetchQueue($queue_name)) {
      throw new Exception("A queue named '$queue_name' already exists.");
    }

    if (is_array($queue_users)) {
      $statement = DB::Prepare("SELECT `name` FROM `users` WHERE `key` = UNHEX(?)");
      $user_keys = [];
      foreach ($queue_users as $user_key) {
        try { $statement->execute([$user_key]); }
        catch (PDOException $e) { throw new Exception("Error looking for user in database: ".$e->getMessage()); }
        if (!($result = $statement->fetch(PDO::FETCH_OBJ))) { continue; } // Just ignore fictitious users.
        $user_keys[] = $user_key;
      }
    }
    if ($queue_users === false) {
      try {
        $statement = DB::Prepare("SELECT HEX(`key`) as `key` FROM `users` WHERE `organization` = 'Efficient Marketing' AND `disabled` = (0)");
        $statement->execute();
        $user_keys = [];
        while($row = $statement->fetch(PDO::FETCH_OBJ)) { $user_keys[] = $row->key; }
      } catch (PDOException $e) {
        throw new Exception("Unable to generate default user list for '$queue_name'.");
      }
    }

    if (empty($user_keys)) { throw new Exception("No valid users passed to CreateQueue"); }

    try {
      $statement = DB::Prepare("INSERT INTO `plivo_queues` SET ".($queue_id !== false ? 'id = :id, ' : '')."`name` = :name, `type` = :type, `timeout` = :timeout, `next_action` = :next_action, `users` = :users");
      $parameters = [
        'name' => $queue_name,
        'type' => $queue_type,
        'timeout' => $queue_timeout,
        'next_action' => $queue_next_action,
        'users' => json_encode($user_keys)
      ];
      if ($queue_id !== false) { $parameters['id'] = $queue_id; }
      $statement->execute($parameters);
    } catch(PDOException $e) {
      throw new Exception("Unable to insert queue into database.");
    }
  }

  /*
   * DeleteQueue - Takes a queue(id or name). Deletes the queue from the
   * database and moves all numbers to the 'default' queue.
   *
   * Uses: DB(db.php) Phone(self)
   */
  public static function DeleteQueue ( $queue_identifier ) {
    if (!($queue = self::GetQueueDetails($queue_identifier))) { throw new Exception("Unable to delete queue '$queue_identifier'. It doesn't exist."); }
    if (!($numbers = self::GetQueueNumbers($queue->id))) { $numbers = []; }
    foreach ($numbers as $number) {
      self::AssignNumberToQueue($number,1);
    }
    $statement = DB::Prepare("DELETE FROM `plivo_queues` WHERE `id` = ?");
    $statement->execute([$queue->id]);
  }

  /*
   * RenameQueue - Takes a queue(id or name). Renames the queue.
   *
   * Uses: DB(db.php) Phone(self)
   */
  public static function RenameQueue ( $queue_identifier, $new_name ) {
    if (!($queue = self::GetQueueDetails($queue_identifier))) { throw new Exception("Unable to rename queue '$queue_identifier'. It doesn't exist."); }
    $statement = DB::Prepare("UPDATE `plivo_queues` SET `name` = :name WHERE `id` = :id");
    $statement->execute([
      'id' => $queue->id,
      'name' => $new_name
    ]);
  }

  /*
   * ChangeQueueType - Takes a queue(id or name). Changes the queue type.
   *
   * Uses: DB(db.php) Phone(self)
   */
  public static function ChangeQueueType ( $queue_identifier, $new_type ) {
    if (!($queue = self::GetQueueDetails($queue_identifier))) { throw new Exception("Unable to change queue type '$queue_identifier'. It doesn't exist."); }
    $statement = DB::Prepare("UPDATE `plivo_queues` SET `type` = :type WHERE `id` = :id");
    $statement->execute([
      'id' => $queue->id,
      'type' => $new_type
    ]);
  }

  /*
   * AddUserToQueue - Adds a number to a queue. Takes a user(name,key, or
   * phone number) and a queue(id or name).
   *
   * Uses: DB(db.php) Phone(this)
   */
  public static function AddUserToQueue ( $user, $queue ) {
    $user  = self::CompleteUserData($user);
    if (!$user) { throw new Exception("Invalid user. Cannot add to queue."); }

    $queue = self::GetQueueDetails($queue);
    if (!$queue) { throw new Exception("Invalid queue. Cannot add users."); }

    $queue->users[] = $user;

    self::PutQueueUsers ( $queue );
  }

  /*
   * RemoveUserFromQueue - Removes a number from a queue. Takes a user
   * (name,key, or phone number) and a queue(id or name).
   *
   * Uses: DB(db.php) Phone(this)
   */
  public static function RemoveUserFromQueue ( $user, $queue ) {
    $user  = self::CompleteUserData($user);
    if (!$user) { throw new Exception("Invalid user. Cannot add to queue."); }

    $queue = self::GetQueueDetails($queue);
    if (!$queue) { throw new Exception("Invalid queue. Cannot add users."); }

    foreach($queue->users as $key => $value) {
      if ($value->key == $user->key) { unset($queue->users[$key]); }
    }

    self::PutQueueUsers ( $queue );
  }

  /*
   * PutQueueUsers - Takes a queue object. Updates the queue users in the
   * database and returns true or false.
   *
   * Uses: DB(db.php)
   */
   public static function PutQueueUsers ( $queue ) {

    $statement = DB::Prepare('UPDATE `plivo_queues` SET `users` = :users WHERE id = :id');

    $users = [];
    foreach ($queue->users as $user) {
      $users[] = $user->key;
    }
    try {
      $statement->execute([
        'id' => $queue->id,
        'users' => json_encode($users)
      ]);
    } catch (PDOException $e) {
      throw new Exception("Unable to save changes to queue.");
    }

    return true;
  }

  /*
   * SetQueueTimeout - Takes a queue(id or name) and a timeout value. Sets it
   * in the database.
   *
   * Uses DB(db.php)
   */
  public static function SetQueueTimeout ( $queue_identifier, $timeout ) {

    $column = 'name';
    if (is_int($queue_identifier) || is_numeric($queue_identifier)) {
      $column = 'id';
    }
    try {
      $statement = DB::Prepare("SELECT `id` FROM `plivo_queues` WHERE $column = ?");
      $statement->execute([$queue_identifier]);
    } catch (PDOException $e) {
      throw new Exception("Error selecting queue from database: ".$e->getMessage());
    }

    if (!($queue = $statement->fetch(PDO::FETCH_OBJ))) { throw new Exception("Could not find a queue identified by '$queue_identifier'."); }

    try {
      $statement = DB::Prepare("UPDATE `plivo_queues` SET `timeout` = :timeout WHERE `id` = :id");
      $statement->execute([
        'timeout' => $timeout,
        'id' => $queue->id
      ]);
    } catch (PDOException $e) {
      throw new Exception("Error updating timeout for queue {$queue->id}: ".$e->getMessage());
    }

    return true;

  }

  /*
   * SetQueueNextAction - Takes a queue(id or name) and an action. The action
   * can be a queue(id or name), 'voicemail', or 'hangup'. Fails with an
   * exception if the action is invalid.
   *
   * Uses DB(db.php) Phone(self);
   */
   public static function SetQueueNextAction ( $queue_identifier, $requested_action ) {

     if (!$queue = self::GetQueueDetails($queue_identifier)) { throw new Exception("Invalid queue. Cannot set action."); }

     $action = false;
     if ($requested_action === 'voicemail') { $action = $requested_action; }
     else if ($requested_action === 'hangup') { $action = $requested_action; }
     else if ($target_queue = self::GetQueueDetails($requested_action)) {
       $action = $target_queue->id;
     }
     if (!$action) { throw new Exception("Invalid action '$requested_action' requested."); }

     try {
       $statement = DB::Prepare("UPDATE `plivo_queues` SET `next_action` = :action WHERE `id` = :id");
       $statement->execute([
         'action' => $action,
         'id' => $queue->id
       ]);
     } catch (PDOException $e) {
       throw new Exception("Error updating queue next action: ".$e->getMessage());
     }
     return true;
   }

  /*
   * SetQueueRingingSound - Takes a queue(id or name) and a filename. The
   * filename can be NULL or any file in public/ringing_sounds. Fails with an
   * exception if the queue or filename is invalid.
   *
   * Uses DB(db.php) Phone(self);
   */
   public static function SetQueueRingingSound ( $queue_identifier, $requested_filename ) {

     if (!$queue = self::GetQueueDetails($queue_identifier)) { throw new Exception("Invalid queue. Cannot set ringing sound."); }

     $filename = false;
     if (empty($requested_filename)) { $filename = null; }
     else if (file_exists(PUB.'/ringing_sounds/'.$requested_filename) && !is_dir(PUB.'/ringing_sounds/'.$requested_filename)) {
       $filename = $requested_filename;
     }
     if (!$filename) { throw new Exception("Invalid filename '$requested_filename' requested."); }

     try {
       $statement = DB::Prepare("UPDATE `plivo_queues` SET `ringing_sound` = :filename WHERE `id` = :id");
       $statement->execute([
         'filename' => $filename,
         'id' => $queue->id
       ]);
     } catch (PDOException $e) {
       throw new Exception("Error updating queue ringing sound: ".$e->getMessage());
     }
     return true;
   }

  /*
   * SetQueueVoicemailGreeting - Takes a queue(id or name) and a filename. The
   * filename can be NULL or any file in public/voicemail_greetings. Fails with
   * an exception if the queue or filename is invalid.
   *
   * Uses DB(db.php) Phone(self);
   */
   public static function SetQueueVoicemailGreeting ( $queue_identifier, $requested_filename ) {

     if (!$queue = self::GetQueueDetails($queue_identifier)) { throw new Exception("Invalid queue. Cannot set voicemail greeting."); }

     $filename = false;
     if (empty($requested_filename)) { $filename = null; }
     else if (file_exists(PUB.'/voicemail_greetings/'.$requested_filename) && !is_dir(PUB.'/voicemail_greetings/'.$requested_filename)) {
       $filename = $requested_filename;
     }
     if (!$filename) { throw new Exception("Invalid filename '$requested_filename' requested."); }

     try {
       $statement = DB::Prepare("UPDATE `plivo_queues` SET `voicemail_greeting` = :filename WHERE `id` = :id");
       $statement->execute([
         'filename' => $filename,
         'id' => $queue->id
       ]);
     } catch (PDOException $e) {
       throw new Exception("Error updating queue voicemail greeting: ".$e->getMessage());
     }
     return true;
   }

  /*
   * SetQueueForwardNumber - Takes a queue(id or name) and a phone number. Sets
   * it in the database.
   *
   * Uses DB(db.php)
   */
  public static function SetQueueForwardNumber ( $queue_identifier, $number ) {

    if ($number) {
      $number = self::SanitizeNumber($number);
      if (!$number) { throw new Exception("Invalid phone number"); }
    }

    $column = 'name';
    if (is_int($queue_identifier) || is_numeric($queue_identifier)) {
      $column = 'id';
    }
    try {
      $statement = DB::Prepare("SELECT `id` FROM `plivo_queues` WHERE $column = ?");
      $statement->execute([$queue_identifier]);
    } catch (PDOException $e) {
      throw new Exception("Error selecting queue from database: ".$e->getMessage());
    }

    if (!($queue = $statement->fetch(PDO::FETCH_OBJ))) { throw new Exception("Could not find a queue identified by '$queue_identifier'."); }

    try {
      if ($number) {
        $statement = DB::Prepare("UPDATE `plivo_queues` SET `forward` = :number WHERE `id` = :id");
        $statement->execute([
          'number' => $number,
          'id' => $queue->id
        ]);
      } else {
        $statement = DB::Prepare("UPDATE `plivo_queues` SET `forward` = NULL WHERE `id` = ?");
        $Statement->execute([$queue->id]);
      }
    } catch (PDOException $e) {
      throw new Exception("Error updating forwarding number for queue {$queue->id}: ".$e->getMessage());
    }

    return true;

  }

  /*
   * SetQueueForwardCondition - Takes a queue(id or name) and a condition name.
   * Sets it in the database.
   *
   * Uses DB(db.php)
   */
  public static function SetQueueForwardCondition ( $queue_identifier, $condition ) {

    if (!$condition || !in_array($condition,['no call','no call 3mo','no conv','no conv 3mo'])) {
      if (!$number) { throw new Exception("Invalid condition '$condition'"); }
    }

    $column = 'name';
    if (is_int($queue_identifier) || is_numeric($queue_identifier)) {
      $column = 'id';
    }
    try {
      $statement = DB::Prepare("SELECT `id` FROM `plivo_queues` WHERE $column = ?");
      $statement->execute([$queue_identifier]);
    } catch (PDOException $e) {
      throw new Exception("Error selecting queue from database: ".$e->getMessage());
    }

    if (!($queue = $statement->fetch(PDO::FETCH_OBJ))) { throw new Exception("Could not find a queue identified by '$queue_identifier'."); }

    try {
      if ($condition) {
        $statement = DB::Prepare("UPDATE `plivo_queues` SET `forward_condition` = :condition WHERE `id` = :id");
        $statement->execute([
          'condition' => $condition,
          'id' => $queue->id
        ]);
      } else {
        $statement = DB::Prepare("UPDATE `plivo_queues` SET `forward_condition` = NULL WHERE `id` = ?");
        $Statement->execute([$queue->id]);
      }
    } catch (PDOException $e) {
      throw new Exception("Error updating forwarding condition for queue {$queue->id}: ".$e->getMessage());
    }

    return true;

  }

  /*
   * SetQueueCallerIdNumber - Takes a queue(id or name) and a phone number. Sets
   * it in the database.
   *
   * Uses DB(db.php)
   */
  public static function SetQueueCallerIdNumber ( $queue_identifier, $number ) {

    if ($number) {
      $number = self::SanitizeNumber($number);
      if (!$number) { throw new Exception("Invalid phone number"); }
    }

    $column = 'name';
    if (is_int($queue_identifier) || is_numeric($queue_identifier)) {
      $column = 'id';
    }
    try {
      $statement = DB::Prepare("SELECT `id` FROM `plivo_queues` WHERE $column = ?");
      $statement->execute([$queue_identifier]);
    } catch (PDOException $e) {
      throw new Exception("Error selecting queue from database: ".$e->getMessage());
    }

    if (!($queue = $statement->fetch(PDO::FETCH_OBJ))) { throw new Exception("Could not find a queue identified by '$queue_identifier'."); }

    try {
      if ($number) {
        $statement = DB::Prepare("UPDATE `plivo_queues` SET `cid_number` = :number WHERE `id` = :id");
        $statement->execute([
          'number' => $number,
          'id' => $queue->id
        ]);
      } else {
        $statement = DB::Prepare("UPDATE `plivo_queues` SET `cid_number` = NULL WHERE `id` = ?");
        $Statement->execute([$queue->id]);
      }
    } catch (PDOException $e) {
      throw new Exception("Error updating caller id number for queue {$queue->id}: ".$e->getMessage());
    }

    return true;

  }

  /*
   * SetQueueCallerIdName - Takes a queue(id or name) and a string. Sets
   * it in the database.
   *
   * Uses DB(db.php)
   */
  public static function SetQueueCallerIdName ( $queue_identifier, $name ) {

    if (strlen($name) > 15) {
      if (!$number) { throw new Exception("Invalid caller id name. Must be between 0 and 15 letters."); }
    }

    $column = 'name';
    if (is_int($queue_identifier) || is_numeric($queue_identifier)) {
      $column = 'id';
    }
    try {
      $statement = DB::Prepare("SELECT `id` FROM `plivo_queues` WHERE $column = ?");
      $statement->execute([$queue_identifier]);
    } catch (PDOException $e) {
      throw new Exception("Error selecting queue from database: ".$e->getMessage());
    }

    if (!($queue = $statement->fetch(PDO::FETCH_OBJ))) { throw new Exception("Could not find a queue identified by '$queue_identifier'."); }

    try {
      if ($name) {
        $statement = DB::Prepare("UPDATE `plivo_queues` SET `cid_name` = :name WHERE `id` = :id");
        $statement->execute([
          'name' => $name,
          'id' => $queue->id
        ]);
      } else {
        $statement = DB::Prepare("UPDATE `plivo_queues` SET `cid_name` = NULL WHERE `id` = ?");
        $Statement->execute([$queue->id]);
      }
    } catch (PDOException $e) {
      throw new Exception("Error updating caller id name for queue {$queue->id}: ".$e->getMessage());
    }

    return true;

  }

  /*
   * SetQueuePressOne - Takes a queue(id or name) and a boolean value. Sets
   * it in the database.
   *
   * Uses DB(db.php)
   */
  public static function SetQueuePressOne ( $queue_identifier, $press_one ) {

    if ($press_one === true || in_array($press_one,[1,'true','1','yes'])) {
      $press_one = 1;
    } else if ($press_one === false || in_array($press_one,[0,'false','0','no'])) {
      $press_one = 0;
    } else {
      throw new Exception("Invalid value '$press_one'.");
    }

    $column = 'name';
    if (is_int($queue_identifier) || is_numeric($queue_identifier)) {
      $column = 'id';
    }
    try {
      $statement = DB::Prepare("SELECT `id` FROM `plivo_queues` WHERE $column = ?");
      $statement->execute([$queue_identifier]);
    } catch (PDOException $e) {
      throw new Exception("Error selecting queue from database: ".$e->getMessage());
    }

    if (!($queue = $statement->fetch(PDO::FETCH_OBJ))) { throw new Exception("Could not find a queue identified by '$queue_identifier'."); }

    try {
      $statement = DB::Prepare("UPDATE `plivo_queues` SET `press_one` = :press_one WHERE `id` = :id");
      $statement->execute([
        'press_one' => $press_one,
        'id' => $queue->id
      ]);
    } catch (PDOException $e) {
      throw new Exception("Error updating press one to answer setting for queue {$queue->id}: ".$e->getMessage());
    }

    return true;

  }

  /*
   * SetQueueCallerIdPassthrough - Takes a queue(id or name) and a boolean
   * value. Sets it in the database.
   *
   * Uses DB(db.php)
   */
  public static function SetQueueCallerIdPassthrough ( $queue_identifier, $passthrough ) {

    if ($passthrough === true || in_array($passthrough,[1,'true','1','yes'])) {
      $passthrough = 1;
    } else if ($passthrough === false || in_array($passthrough,[0,'false','0','no'])) {
      $passthrough = 0;
    } else {
      throw new Exception("Invalid value '$passthrough'.");
    }

    $column = 'name';
    if (is_int($queue_identifier) || is_numeric($queue_identifier)) {
      $column = 'id';
    }
    try {
      $statement = DB::Prepare("SELECT `id` FROM `plivo_queues` WHERE $column = ?");
      $statement->execute([$queue_identifier]);
    } catch (PDOException $e) {
      throw new Exception("Error selecting queue from database: ".$e->getMessage());
    }

    if (!($queue = $statement->fetch(PDO::FETCH_OBJ))) { throw new Exception("Could not find a queue identified by '$queue_identifier'."); }

    try {
      $statement = DB::Prepare("UPDATE `plivo_queues` SET `cid_passthrough` = :passthrough WHERE `id` = :id");
      $statement->execute([
        'passthrough' => $passthrough,
        'id' => $queue->id
      ]);
    } catch (PDOException $e) {
      throw new Exception("Error updating caller ID passthrough setting for queue {$queue->id}: ".$e->getMessage());
    }

    return true;

  }

  /*
   * ValidateQueueRingingSound - Takes a queue(id or name) and an optional,
   * boolean 'autocorrect' value.
   * Returns true if the queue's ringing sound is valid or false if it isn't. If
   * autocorrect is true, and the ringing sound is invalid, the ringing sound
   * will automatically be changed to NULL.
   *
   * Uses Phone(self) DB(db.php indirectly)
   */
  public static function ValidateQueueRingingSound ( $queue_identifier, $autocorrect = false ) {
    if (!$queue = self::GetQueueDetails($queue_identifier)) { throw new Exception("Invalid queue. Cannot validate ringing sound."); }
    if (empty($queue->ringing_sound)) { return true; }
    if (!file_exists(PUB.'/ringing_sounds/'.$queue->ringing_sound) || is_dir(PUB.'/ringing_sound/'.$queue->ringing_sound)) {
      if ($autocorrect) { self::SetQueueRingingSound($queue_identifier,NULL); }
      return false;
    }
    return true;
  }

  /*
   * ValidateQueueVoicemailGreeting - Takes a queue(id or name) and an optional,
   * boolean 'autocorrect' value.
   * Returns true if the queue's voicemail greeting is valid or false if it
   * isn't. If autocorrect is true, and the voicemail greeting is invalid, the
   * voicemail greeting will automatically be changed to NULL.
   *
   * Uses Phone(self) DB(db.php indirectly)
   */
  public static function ValidateQueueVoicemailGreeting ( $queue_identifier, $autocorrect = false ) {
    if (!$queue = self::GetQueueDetails($queue_identifier)) { throw new Exception("Invalid queue. Cannot validate ringing sound."); }
    if (empty($queue->voicemail_greeting)) { return true; }
    if (!file_exists(PUB.'/voicemail_greetings/'.$queue->voicemail_greeting) || is_dir(PUB.'/voicemail_greetings/'.$queue->voicemail_greeting)) {
      if ($autocorrect) { self::SetQueueVoicemailGreeting($queue_identifier,NULL); }
      return false;
    }
    return true;
  }

  /*
   * FetchQueue - Fetches a queue. Non-fatally returns false if queue does not
   * exist.
   *
   * Uses DB(db.php)
   */
  public static function FetchQueue ( $id_or_name ) {
    $result = false;
    if (is_int($id_or_name) || is_numeric($id_or_name)) {
      $statement = DB::Prepare('SELECT * FROM `plivo_queues` WHERE `id` = ?');
      $statement->execute([$id_or_name]);
      $result = $statement->fetch(PDO::FETCH_OBJ);
    }
    if (!$result && (is_string($id_or_name)||is_numeric($id_or_name))) {
      $statement = DB::Prepare('SELECT * FROM `plivo_queues` WHERE `name` = ?');
      $statement->execute([$id_or_name]);
      $result = $statement->fetch(PDO::FETCH_OBJ);
    }
    if (!$result && (strlen($id_or_name) == 11 && substr($id_or_name,0,1) == 1)) {
      $statement = DB::Prepare("SELECT `plivo_queues`.* FROM `plivo_queues`, `plivo_numbers` WHERE `plivo_numbers`.`number` = ? AND `plivo_queues`.`id` = `plivo_numbers`.`queue`");
      $statement->execute([$id_or_name]);
      $result = $statement->fetch(PDO::FETCH_OBJ);
    }
    if (!$result) { return false; }
    return $result;
  }

  /*
   * GetQueueDetails - Get queue details from database.
   *
   * Uses: DB(db.php)
   */
  public static function GetQueueDetails ( $queue_identifier, $recursed = false ) {

    $queue = self::FetchQueue($queue_identifier);

    if (!$queue && (int)$queue_identifier !== 1 && (int)$queue_identifier !== 2 && $queue_identifier !== 'default' && $queue_identifier !== 'user') {
      throw new Exception ("Queue not found.");
    }
    if (!$queue && ((int)$queue_identifier === 1 || $queue_identifier === 'default') && $recursed) {
      throw new Exception ("Default queue could not be found or created!");
    }
    if (!$queue && ((int)$queue_identifier === 2 || $queue_identifier === 'user') && $recursed) {
      throw new Exception ("User queue could not be found or created!");
    }
    if (!$queue && ((int)$queue_identifier === 1 || $queue_identifier === 'default')) {
      self::CreateQueue([
        'name'=>'default',
        'id' => 1
      ]);
      return self::GetQueueDetails(1,true);
    }
    if (!$queue && ((int)$queue_identifier === 2 || $queue_identifier === 'user')) {
      self::CreateQueue([
        'name' => 'user',
        'id' => 2
      ]);
      return self::GetQueueDetails(2,true);
    }

    $queue->users = json_decode($queue->users);
    if (empty($queue->users)) { $queue->users = []; }

    $keys_to_users = function( $keys ) {
      $return_data = [];
      foreach ($keys as $user_key) {
        $user = self::CompleteUserData($user_key);
        if (!$user) { continue; }
        if (!Phone::IsValidUSNumber($user->phone)) { continue; }
        $return_data[] = $user;
      }
      if (empty($return_data)) { return []; }

      return $return_data;
    };

    $queue->users = $keys_to_users($queue->users);
    if (!empty($queue->last_user)) {
      $queue->last_user = self::CompleteUserData($queue->last_user);
    }

    return $queue;
  }

  /*
   * GetAllQueueDetails - Takes no arguments. Returns an array of queue details.
   *
   * Uses DB(db.php) Phone(self)
   */
  public static function GetAllQueueDetails () {
    $id_sth = DB::Prepare("SELECT `id` FROM `plivo_queues` ORDER BY `name` ASC");
    $id_sth->execute();
    $queue_ids = $id_sth->fetchAll(PDO::FETCH_OBJ);
    $queues = [];
    foreach ($queue_ids as $id) {
      $queues[(int)($id->id)] = self::GetQueueDetails($id->id);
    }
    if (empty($queues[1])) { $queues[1] = self::GetQueueDetails(1); }
    if (empty($queues[2])) { $queues[2] = self::GetQueueDetails(2); }

    return $queues;
  }

  /*
   * GetQueueNumbers - Takes a queue(id or name) and returns an array of numbers
   * assigned to that queue or false.
   *
   * Uses DB(db.php)
   */
  public static function GetQueueNumbers ( $queue_identifier ) {
    if (!($queue = self::GetQueueDetails($queue_identifier))) {
      throw new Exception("Invalid queue passed to GetQueueNumbers.");
    }
    $statement = DB::Prepare("SELECT `number` FROM `plivo_numbers` WHERE `queue` = ?");
    $statement->execute([$queue->id]);
    $results = $statement->fetchAll(PDO::FETCH_OBJ);
    $numbers = [];
    foreach ($results as $result) {
      $numbers[] = $result->number;
    }
    if (empty($numbers)) { $numbers = false; }

    return $numbers;
  }

  /*
   * CompleteUserData - Takes a phone number, key, plivo sip user, or name and returns an object
   * with all three.
   *
   * Uses DB(db.php)
   */
  public static function CompleteUserData ( $given_data ) {
    $where = false;
    $parameters = [];
    $pmatches = [];
    if (empty($where) && is_string($given_data) && preg_match('/[a-zA-Z]+[0-9]{12}/',$given_data,$pmatches)) {
      $where = "`sip_user` = ?";
      $parameters = [$pmatches[0]];
    }
    if (empty($where) && is_string($given_data) && strlen($given_data) == 32) {
      $where = "`key` = UNHEX(?)";
      $parameters = [$given_data];
    }
    if (empty($where) && (is_int($given_data) || is_numeric($given_data))) {
      $where = "`ringto` = ? OR `plivo_number` = ?";
      $parameters = [$given_data,$given_data];
    }
    if (is_string($given_data) && empty($where)) {
      $where = "`name` = ?";
      $parameters = [$given_data];
    }
    if (!empty($where)) {
      try {
        $statement = DB::Prepare("SELECT HEX(`key`) as `key`, `name`, `ringto` as phone, `sip_user`, `sip_password`, `use_sip`+0 as `use_sip`, `plivo_number`, `email` FROM `users` WHERE $where LIMIT 1");
        $statement->execute($parameters);
      } catch (PDOException $e) {
        throw new Exception("Unable to query user.");
      }
      $result = $statement->fetch(PDO::FETCH_OBJ);
      $statement->closeCursor();
      return $result;
    }
  }

  // SMS Functions

  /*
   * Fetch next Queued SMS. Takes no parameters, returns an array or false.
   *
   * Uses: DB(db.php)
   */
  public static function FetchNextQueuedSMS () {
    $statement = DB::Prepare(
<<<SQL
      SELECT
        `id`,
        UNIX_TIMESTAMP(`send_at`) as `send_at`,
        `destination`,
        `from`,
        `message`
      FROM
        `sms_queue`
      ORDER BY `send_at` ASC
      LIMIT 1;
SQL
    );

    $statement->execute();
    if ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
      return $row;
    }

    return false;

  }

  /*
   * Send SMS. Takes an array of parameters, returns true on success or
   * false on error. Error will also throw an error.
   * $params = [
   *   0|'src'|'sender'|'from' => <fully qualified phone number, REQUIRED
   *   1|'dst'|'destination'|'to' => <fully qualified phone number>, REQUIRED
   *   2|'text'|'message' => <message string> REQUIRED
   * ];
   *
   * Uses: Plivo App(self::$PlivoApp)
   */
  public static function SendSMS( $params ) {

    self::SetupApp();

    $plivo_params = [];

    if (isset($params['from'])) { $plivo_params['src'] = $params['from']; }
    if (isset($params['sender'])) {
      $plivo_params['src'] = $params['sender'];
    }
    if (isset($params['src'])) { $plivo_params['src'] = $params['src']; }
    if (isset($params[0])) { $plivo_params['src'] = $params[0]; }

    if (isset($params['to'])) { $plivo_params['dst'] = $params['to']; }
    if (isset($params['destination'])) {
      $plivo_params['dst'] = $params['destination'];
    }
    if (isset($params['dst'])) { $plivo_params['dst'] = $params['dst']; }
    if (isset($params[1])) { $plivo_params['dst'] = $params[1]; }

    if (isset($params['message'])) {
      $plivo_params['text'] = $params['message'];
    }
    if (isset($params['text'])) { $plivo_params['text'] = $params['text']; }
    if (isset($params[2])) { $plivo_params['text'] = $params[2]; }

    if (
      !array_key_exists('src',$plivo_params) ||
      !self::IsValidUSNumber($plivo_params['src'])
    ) {
      throw new Exception("Sender number is not a valid US phone number");
      return false;
    }
    if (
      !array_key_exists('dst',$plivo_params) ||
      !self::IsValidUSNumber($plivo_params['dst'])
    ) {
      throw new Exception(
        "Destination number is not a valid US phone number"
      );
      return false;
    }
    if (!array_key_exists('text',$plivo_params) || !$plivo_params['text']) {
      throw new Exception("No message to send");
      return false;
    }

    $response = self::$PlivoApp->send_message($plivo_params);
    if (array_shift(array_values($response)) !== 202) {
      throw new Exception("Error sending message");
      return false;
    }
    return true;
  }

  public static function SaveSMS ( $direction, $from, $to, $message, $time = false ) {
    $statement = DB::Prepare("INSERT INTO sms SET `direction` = :direction, `from` = :from, `to` = :to, `message` = :message, `time` = :time");
    if (!$time) { $time = date("Y-m-d H:i:s"); }

    $statement->execute([
      'direction' => $direction,
      'from' => $from,
      'to' => $to,
      'message' => $message,
      'time' => $time
    ]);

    return true;
  }

  /*
   * Remove SMS from Queue. Takes a queued sms id.
   *
   * Uses: DB(db.php)
   */
  public static function RemoveSMSFromQueue ( $id ) {
    $statement = DB::Prepare("DELETE FROM sms_queue WHERE id = ? LIMIT 1;");
    $statement->execute([$id]);
    return true;
  }

  /*
   * Checks to see if a user is on an active call.
   *
   * Uses: DB(db.php)
   */
  public static function UserOnCall( $key ) {
    $statement = DB::Prepare("SELECT * FROM `call_data` WHERE `date` > NOW() - INTERVAL 4 HOUR AND `user` = UNHEX(?) AND status IN ('ringing','connected')");
    $statement->execute([$key]);
    if ($statement->fetch()) { return true; }
    return false;
  }

  /*
   * Check if number is a valid, fully qualified US phone number. Takes a
   * single string and returns true or false.
   *
   * Uses: none;
   */
  public static function IsValidUSNumber ( $number ) {
    if (!is_string($number)) return false;
    if (strlen($number) !== 11) return false;
    if (substr($number,0,1) !== '1') return false;

    // TODO: Expand with geo tools to validate area code.

    return true;
  }

  public static function GetRemoteHistory ( $number ) {
    $ch = curl_init("https://www.daqe.com/get_callhist.php");
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, [
      'number'=>$number,
      'token'=>'2309ejlksdnflkmn023uoijdslofijiodhjf9823ujkldvsjkljsi2039uwerysdiufh',
      'start'=>'1 month ago'
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $res = curl_exec($ch);
    $error = curl_error($ch);
    return json_decode($res);
  }

  public static function SanitizeNumber( $number, $return_malformed = false ) {
    $number = preg_replace('/\D/', '', $number);
    if (strlen($number) > 11) { return ($return_malformed ? $number : false); }
    if (strlen($number) < 10) { return ($return_malformed ? $number : false); }
    if (strlen($number) == 10) { return '1'.$number; }
    return $number;
  }


}

Parent Tools : 2013 - 2017

Role: Backend Frontend Video Editor Design Assist Architect
Used: PHP Javascript HTML CSS Google Analytics Photoshop Inkscape iMovie MachForm Stripe
https://parenttools.org - Defunct

This was a revamp of an existing product. The company had a number of existing assets they wanted used and they contracted a video production company to produce updated materials which I was responsible for editing, generating preview content for, and formatting for the web.

Initially we used Machform to implement the site's order system and I built custom hooks to connect it back to account creation and ordering. Eventually, we also looped in ordering a physical version of their product(a 'Parenting Kit') via Amazon and I rebuilt the onsite system using Stripe.

I built out user management tools, login flows, and a streamlined product delivery page.

Screenshot-2018-4-17-Parent-Tools---The-Resource-Kit-every-Parent-Should-Have

Proxy Block : 2012

Role: Engineer Architect
Used: Python IPTables

Proxy Block was a fun little project I took on to secure our PBX and CRM servers against access via anonymous proxies which, at the time, were hammering them pretty hard with random authentication requests.

Proxy Block was an extention to our firewalls that actively scanned inbound connections for traffic from daily-updated lists of proxies, killed connections, and loaded the worst offenders into IPTables in order to head off unwanted connection attempts.

def check_ipchain(self,sanit_try=False):
    print 'Checking IP Chains...'
    exists = False
    first = True
    tests = [
        'iptables -L | grep "Chain ProxyBlock-Blacklist"',
        'iptables -L | grep "Chain ProxyBlock-Whitelist"',
        'iptables -L ProxyBlock-Blacklist | grep "RETURN"',
        'iptables -L ProxyBlock-Whitelist | grep "RETURN"',
        'iptables -L INPUT | grep "ProxyBlock-Blacklist"',
        'iptables -L INPUT | grep "ProxyBlock-Whitelist"',
    ]
    for test in tests:
        if not subprocess.call(test, shell=True, stdout=open(os.devnull, 'wb'), stderr=open(os.devnull, 'wb')):
            if not exists and not first:
                print 'Breakage detected in ip chain.'
                if self.test:
                    self.exit(1)
                self.sanitize_ipchain(sanit_try)
                return False
            else:
                exists = True
                first = False
        else:
            if exists and not first:
                print 'Breakage detected in ip chain.'
                if self.test:
                    self.exit(1)
                self.sanitize_ipchain(sanit_try)
                return False
            else:
                first = False
                continue

    if exists:
        return True
    else:
        return False

US Youth Services : 2012 - 2017

Role: Backend Frontend Design Logo
Used: PHP HTML Javascript CSS Wordpress Photoshop Inkscape

We built a lot of materials for US Youth Services. Originally Red River Academy, we helped them with rebranding and marketing, only some of which is included here.

Screenshot-2018-4-17-US-Youth-Services---Non-Profit-Boarding-School-For-Struggling-Teens

Fast Track Program : 2012-2017

Role: Backend Frontend Design Logo Animation
Used: Javascript HTML CSS PHP Photoshop Inkscape Wordpress

This was actually my first assignment on day-1 with Efficient Marketing. The client was a new summer catch-up program for struggling middle and high-school students that needed the works. I designed theur logo, built their main website, marketing materials, and remade video assets provided by the client as HTML5 animations.

Screenshot-2018-4-18-Fast-Track

Screenshot-2018-4-18-Struggling-Teen---Fast-Track-Program-Faster-Results-Less-Cost

Various Marketing Materials : 2012 - 2017

Roles: Backend Frontend Design Server Admin
Used: HTML PHP Javascript Node.js CSS Photoshop Inkscape

Working at Efficient Marketing, about 1/3 of my job was designing and building marketing materials in various mediums for various clients. Many of them were derivative so I'll just include a handful here.

Screenshot-2018-4-18-Non-Profit-Options-for-Struggling-Teens

Screenshot-2018-4-18-http-boarding-school-advisor-com

FireShot-Capture-2---180-Parenting---Change-Your-Parenting.-Change-Your----http___180parenting.com

FireShot-Capture-3---Beach-House-School---http___beachhouseprogram.com_