1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-22 13:30:55 +01:00

Remove standalone SMS support in favor of a "Mail, SMS, and other media are mostly the same thing" approach

Summary:
Ref T920. Over time, mail has become much more complex and I think considering "mail", "sms", "postcards", "whatsapp", etc., to be mostly-the-same is now a more promising avenue than building separate stacks for each one.

Throw away all the standalone SMS code, including the Twilio config options. I have a separate diff that adds Twilio as a mail adapter and functions correctly, but it needs some more work to bring upstream.

This permanently destroys the `sms` table, which no real reachable code ever wrote to. I'll call this out in the changelog.

Test Plan:
  - Grepped for `SMS` and `Twilio`.
  - Ran storage upgrade.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T920

Differential Revision: https://secure.phabricator.com/D19939
This commit is contained in:
epriestley 2019-01-01 19:58:54 -08:00
parent e856e791f3
commit cfcd35d8a3
16 changed files with 1 additions and 707 deletions

View file

@ -0,0 +1 @@
DROP TABLE {$NAMESPACE}_metamta.sms;

View file

@ -1,21 +0,0 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline(pht('manage SMS'));
$args->setSynopsis(<<<EOSYNOPSIS
**sms** __command__ [__options__]
Manage Phabricator SMS stuff.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorSMSManagementWorkflow')
->execute();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);

View file

@ -4239,19 +4239,6 @@ phutil_register_library_map(array(
'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php',
'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
'PhabricatorSMS' => 'infrastructure/sms/storage/PhabricatorSMS.php',
'PhabricatorSMSConfigOptions' => 'applications/config/option/PhabricatorSMSConfigOptions.php',
'PhabricatorSMSDAO' => 'infrastructure/sms/storage/PhabricatorSMSDAO.php',
'PhabricatorSMSDemultiplexWorker' => 'infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php',
'PhabricatorSMSImplementationAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php',
'PhabricatorSMSImplementationTestBlackholeAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php',
'PhabricatorSMSImplementationTwilioAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php',
'PhabricatorSMSManagementListOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php',
'PhabricatorSMSManagementSendTestWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php',
'PhabricatorSMSManagementShowOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php',
'PhabricatorSMSManagementWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php',
'PhabricatorSMSSendWorker' => 'infrastructure/sms/worker/PhabricatorSMSSendWorker.php',
'PhabricatorSMSWorker' => 'infrastructure/sms/worker/PhabricatorSMSWorker.php',
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php',
@ -10269,19 +10256,6 @@ phutil_register_library_map(array(
'PhabricatorResourceSite' => 'PhabricatorSite',
'PhabricatorRobotsController' => 'PhabricatorController',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorSMS' => 'PhabricatorSMSDAO',
'PhabricatorSMSConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSMSDAO' => 'PhabricatorLiskDAO',
'PhabricatorSMSDemultiplexWorker' => 'PhabricatorSMSWorker',
'PhabricatorSMSImplementationAdapter' => 'Phobject',
'PhabricatorSMSImplementationTestBlackholeAdapter' => 'PhabricatorSMSImplementationAdapter',
'PhabricatorSMSImplementationTwilioAdapter' => 'PhabricatorSMSImplementationAdapter',
'PhabricatorSMSManagementListOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
'PhabricatorSMSManagementSendTestWorkflow' => 'PhabricatorSMSManagementWorkflow',
'PhabricatorSMSManagementShowOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
'PhabricatorSMSManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSMSSendWorker' => 'PhabricatorSMSWorker',
'PhabricatorSMSWorker' => 'PhabricatorWorker',
'PhabricatorSQLPatchList' => 'Phobject',
'PhabricatorSSHKeyGenerator' => 'Phobject',
'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel',

View file

@ -1,60 +0,0 @@
<?php
final class PhabricatorSMSConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('SMS');
}
public function getDescription() {
return pht('Configure SMS.');
}
public function getIcon() {
return 'fa-mobile';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$adapter_description = pht(
'Adapter class to use to transmit SMS to an external provider. A given '.
'external provider will most likely need more configuration which will '.
'most likely require registration and payment for the service.');
return array(
$this->newOption(
'sms.default-sender',
'string',
null)
->setDescription(pht('Default "from" number.'))
->addExample('8675309', 'Jenny still has this number')
->addExample('18005555555', 'Maybe not a real number'),
$this->newOption(
'sms.default-adapter',
'class',
null)
->setBaseClass('PhabricatorSMSImplementationAdapter')
->setSummary(pht('Control how SMS is sent.'))
->setDescription($adapter_description),
$this->newOption(
'twilio.account-sid',
'string',
null)
->setDescription(pht('Account ID on Twilio service.'))
->setLocked(true)
->addExample('gf5kzccfn2sfknpnadvz7kokv6nz5v', pht('30 characters')),
$this->newOption(
'twilio.auth-token',
'string',
null)
->setDescription(pht('Authorization token from Twilio service.'))
->setHidden(true)
->addExample('f3jsi4i67wiwt6w54hf2zwvy3fjf5h', pht('30 characters')),
);
}
}

View file

@ -1,88 +0,0 @@
<?php
abstract class PhabricatorSMSImplementationAdapter extends Phobject {
private $fromNumber;
private $toNumber;
private $body;
public function setFrom($number) {
$this->fromNumber = $number;
return $this;
}
public function getFrom() {
return $this->fromNumber;
}
public function setTo($number) {
$this->toNumber = $number;
return $this;
}
public function getTo() {
return $this->toNumber;
}
public function setBody($body) {
$this->body = $body;
return $this;
}
public function getBody() {
return $this->body;
}
/**
* 16 characters or less, to be used in database columns and exposed
* to administrators during configuration directly.
*/
abstract public function getProviderShortName();
/**
* Send the message. Generally, this means connecting to some service and
* handing data to it. SMS APIs are generally asynchronous, so truly
* determining success or failure is probably impossible synchronously.
*
* That said, if the adapter determines that the SMS will never be
* deliverable, or there is some other known failure, it should throw
* an exception.
*
* @return null
*/
abstract public function send();
/**
* Most (all?) SMS APIs are asynchronous, but some do send back some
* initial information. Use this hook to determine what the updated
* sentStatus should be and what the provider is using for an SMS ID,
* as well as throw exceptions if there are any failures.
*
* @return array Tuple of ($sms_id and $sent_status)
*/
abstract public function getSMSDataFromResult($result);
/**
* Due to the asynchronous nature of sending SMS messages, it can be
* necessary to poll the provider regarding the sent status of a given
* sms.
*
* For now, this *MUST* be implemented and *MUST* work.
*/
abstract public function pollSMSSentStatus(PhabricatorSMS $sms);
/**
* Convenience function to handle sending an SMS.
*/
public static function sendSMS(array $to_numbers, $body) {
PhabricatorWorker::scheduleTask(
'PhabricatorSMSDemultiplexWorker',
array(
'toNumbers' => $to_numbers,
'body' => $body,
),
array(
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
}
}

View file

@ -1,31 +0,0 @@
<?php
/**
* This is useful for testing, but otherwise your SMS ends up in a blackhole.
*/
final class PhabricatorSMSImplementationTestBlackholeAdapter
extends PhabricatorSMSImplementationAdapter {
public function getProviderShortName() {
return 'testtesttest';
}
public function send() {
// I guess this is what a blackhole looks like
}
public function getSMSDataFromResult($result) {
return array(
Filesystem::readRandomCharacters(40),
PhabricatorSMS::STATUS_SENT,
);
}
public function pollSMSSentStatus(PhabricatorSMS $sms) {
if ($sms->getID()) {
return PhabricatorSMS::STATUS_SENT;
}
return PhabricatorSMS::STATUS_SENT_UNCONFIRMED;
}
}

View file

@ -1,99 +0,0 @@
<?php
final class PhabricatorSMSImplementationTwilioAdapter
extends PhabricatorSMSImplementationAdapter {
public function getProviderShortName() {
return 'twilio';
}
/**
* @phutil-external-symbol class Services_Twilio
*/
private function buildClient() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/twilio-php/Services/Twilio.php';
$account_sid = PhabricatorEnv::getEnvConfig('twilio.account-sid');
$auth_token = PhabricatorEnv::getEnvConfig('twilio.auth-token');
return new Services_Twilio($account_sid, $auth_token);
}
/**
* @phutil-external-symbol class Services_Twilio_RestException
*/
public function send() {
$client = $this->buildClient();
try {
$message = $client->account->sms_messages->create(
$this->formatNumberForSMS($this->getFrom()),
$this->formatNumberForSMS($this->getTo()),
$this->getBody(),
array());
} catch (Services_Twilio_RestException $e) {
$message = sprintf(
'HTTP Code %d: %s',
$e->getStatus(),
$e->getMessage());
// Twilio tries to provide a link to more specific details if they can.
if ($e->getInfo()) {
$message .= sprintf(' For more information, see %s.', $e->getInfo());
}
throw new PhabricatorWorkerPermanentFailureException($message);
}
return $message;
}
public function getSMSDataFromResult($result) {
return array($result->sid, $this->getSMSStatus($result->status));
}
public function pollSMSSentStatus(PhabricatorSMS $sms) {
$client = $this->buildClient();
$message = $client->account->messages->get($sms->getProviderSMSID());
return $this->getSMSStatus($message->status);
}
/**
* See https://www.twilio.com/docs/api/rest/sms#sms-status-values.
*/
private function getSMSStatus($twilio_status) {
switch ($twilio_status) {
case 'failed':
$status = PhabricatorSMS::STATUS_FAILED;
break;
case 'sent':
$status = PhabricatorSMS::STATUS_SENT;
break;
case 'sending':
case 'queued':
default:
$status = PhabricatorSMS::STATUS_SENT_UNCONFIRMED;
break;
}
return $status;
}
/**
* We expect numbers to be plainly entered - i.e. the preg_replace here
* should do nothing - but try hard to format anyway.
*
* Twilio uses E164 format, e.g. +15551234567
*/
private function formatNumberForSMS($number) {
$number = preg_replace('/[^0-9]/', '', $number);
$first_char = substr($number, 0, 1);
switch ($first_char) {
case '1':
$prepend = '+';
break;
default:
$prepend = '+1';
break;
}
return $prepend.$number;
}
}

View file

@ -1,54 +0,0 @@
<?php
final class PhabricatorSMSManagementListOutboundWorkflow
extends PhabricatorSMSManagementWorkflow {
protected function didConstruct() {
$this
->setName('list-outbound')
->setSynopsis(pht('List outbound SMS messages sent by Phabricator.'))
->setExamples('**list-outbound**')
->setArguments(
array(
array(
'name' => 'limit',
'param' => 'N',
'default' => 100,
'help' => pht(
'Show a specific number of SMS messages (default 100).'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$sms_messages = id(new PhabricatorSMS())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d',
$args->getArg('limit'));
if (!$sms_messages) {
$console->writeErr("%s\n", pht('No sent SMS.'));
return 0;
}
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
->addColumn('id', array('title' => pht('ID')))
->addColumn('status', array('title' => pht('Status')))
->addColumn('recv', array('title' => pht('Recipient')));
foreach (array_reverse($sms_messages) as $sms) {
$table->addRow(array(
'id' => $sms->getID(),
'status' => $sms->getSendStatus(),
'recv' => $sms->getToNumber(),
));
}
$table->draw();
return 0;
}
}

View file

@ -1,47 +0,0 @@
<?php
final class PhabricatorSMSManagementSendTestWorkflow
extends PhabricatorSMSManagementWorkflow {
protected function didConstruct() {
$this
->setName('send-test')
->setSynopsis(
pht(
'Simulate sending an SMS. This may be useful to test your SMS '.
'configuration, or while developing new SMS adapters.'))
->setExamples("**send-test** --to 12345678 --body 'pizza time yet?'")
->setArguments(
array(
array(
'name' => 'to',
'param' => 'number',
'help' => pht('Send SMS "To:" the specified number.'),
'repeat' => true,
),
array(
'name' => 'body',
'param' => 'text',
'help' => pht('Send SMS with the specified body.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$tos = $args->getArg('to');
$body = $args->getArg('body');
PhabricatorWorker::setRunAllTasksInProcess(true);
PhabricatorSMSImplementationAdapter::sendSMS($tos, $body);
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/sms list-outbound \n\n",
pht(
'Send completed! You can view the list of SMS messages sent by '.
'running this command:'));
}
}

View file

@ -1,72 +0,0 @@
<?php
final class PhabricatorSMSManagementShowOutboundWorkflow
extends PhabricatorSMSManagementWorkflow {
protected function didConstruct() {
$this
->setName('show-outbound')
->setSynopsis(pht('Show diagnostic details about outbound SMS.'))
->setExamples(
'**show-outbound** --id 1 --id 2')
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('Show details about outbound SMS with given ID.'),
'repeat' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
pht(
"Use the '%s' flag to specify one or more SMS messages to show.",
'--id'));
}
$messages = id(new PhabricatorSMS())->loadAllWhere(
'id IN (%Ld)',
$ids);
if ($ids) {
$ids = array_fuse($ids);
$missing = array_diff_key($ids, $messages);
if ($missing) {
throw new PhutilArgumentUsageException(
pht(
'Some specified SMS messages do not exist: %s',
implode(', ', array_keys($missing))));
}
}
$last_key = last_key($messages);
foreach ($messages as $message_key => $message) {
$info = array();
$info[] = pht('PROPERTIES');
$info[] = pht('ID: %d', $message->getID());
$info[] = pht('Status: %s', $message->getSendStatus());
$info[] = pht('To: %s', $message->getToNumber());
$info[] = pht('From: %s', $message->getFromNumber());
$info[] = null;
$info[] = pht('BODY');
$info[] = $message->getBody();
$info[] = null;
$console->writeOut('%s', implode("\n", $info));
if ($message_key != $last_key) {
$console->writeOut("\n%s\n\n", str_repeat('-', 80));
}
}
}
}

View file

@ -1,4 +0,0 @@
<?php
abstract class PhabricatorSMSManagementWorkflow
extends PhabricatorManagementWorkflow {}

View file

@ -1,75 +0,0 @@
<?php
final class PhabricatorSMS
extends PhabricatorSMSDAO {
const MAXIMUM_SEND_TRIES = 5;
/**
* Status constants should be 16 characters or less. See status entries
* for details on what they indicate about the underlying SMS.
*/
// in the beginning, all SMS are unsent
const STATUS_UNSENT = 'unsent';
// that nebulous time when we've sent it from Phabricator but haven't
// heard anything from the external API
const STATUS_SENT_UNCONFIRMED = 'sent-unconfirmed';
// "success"
const STATUS_SENT = 'sent';
// "fail" but we'll try again
const STATUS_FAILED = 'failed';
// we're giving up on our external API partner
const STATUS_FAILED_PERMANENTLY = 'permafailed';
const SHORTNAME_PLACEHOLDER = 'phabricator';
protected $providerShortName;
protected $providerSMSID;
// numbers can be up to 20 digits long
protected $toNumber;
protected $fromNumber;
protected $body;
protected $sendStatus;
public static function initializeNewSMS($body) {
// NOTE: these values will be updated to correct values when the
// SMS is sent for the first time. In particular, the ProviderShortName
// and ProviderSMSID are totally garbage data before a send it attempted.
return id(new PhabricatorSMS())
->setBody($body)
->setSendStatus(self::STATUS_UNSENT)
->setProviderShortName(self::SHORTNAME_PLACEHOLDER)
->setProviderSMSID(Filesystem::readRandomCharacters(40));
}
protected function getConfiguration() {
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'providerShortName' => 'text16',
'providerSMSID' => 'text40',
'toNumber' => 'text20',
'fromNumber' => 'text20?',
'body' => 'text',
'sendStatus' => 'text16?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_provider' => array(
'columns' => array('providerSMSID', 'providerShortName'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function getTableName() {
// Slightly non-standard, but otherwise this class needs "MetaMTA" in its
// name. :/
return 'sms';
}
public function hasBeenSentAtLeastOnce() {
return ($this->getProviderShortName() !=
self::SHORTNAME_PLACEHOLDER);
}
}

View file

@ -1,11 +0,0 @@
<?php
abstract class PhabricatorSMSDAO
extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'metamta';
}
}

View file

@ -1,30 +0,0 @@
<?php
final class PhabricatorSMSDemultiplexWorker extends PhabricatorSMSWorker {
protected function doWork() {
$viewer = PhabricatorUser::getOmnipotentUser();
$task_data = $this->getTaskData();
$to_numbers = idx($task_data, 'toNumbers');
if (!$to_numbers) {
// If we don't have any to numbers, don't send any sms.
return;
}
foreach ($to_numbers as $number) {
// NOTE: we will set the fromNumber and the proper provider data
// in the `PhabricatorSMSSendWorker`.
$sms = PhabricatorSMS::initializeNewSMS($task_data['body']);
$sms->setToNumber($number);
$sms->save();
$this->queueTask(
'PhabricatorSMSSendWorker',
array(
'smsID' => $sms->getID(),
));
}
}
}

View file

@ -1,85 +0,0 @@
<?php
final class PhabricatorSMSSendWorker extends PhabricatorSMSWorker {
public function getMaximumRetryCount() {
return PhabricatorSMS::MAXIMUM_SEND_TRIES;
}
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return phutil_units('1 minute in seconds');
}
protected function doWork() {
$viewer = PhabricatorUser::getOmnipotentUser();
$task_data = $this->getTaskData();
$sms = id(new PhabricatorSMS())
->loadOneWhere('id = %d', $task_data['smsID']);
if (!$sms) {
throw new PhabricatorWorkerPermanentFailureException(
pht('SMS object was not found.'));
}
// this has the potential to be updated asynchronously
if ($sms->getSendStatus() == PhabricatorSMS::STATUS_SENT) {
return;
}
$adapter = PhabricatorEnv::getEnvConfig('sms.default-adapter');
$adapter = newv($adapter, array());
if ($sms->hasBeenSentAtLeastOnce()) {
$up_to_date_status = $adapter->pollSMSSentStatus($sms);
if ($up_to_date_status) {
$sms->setSendStatus($up_to_date_status);
if ($up_to_date_status == PhabricatorSMS::STATUS_SENT) {
$sms->save();
return;
}
}
// TODO - re-jigger this so we can try if appropos (e.g. rate limiting)
return;
}
$from_number = PhabricatorEnv::getEnvConfig('sms.default-sender');
// always set the from number if we get this far in case of configuration
// changes.
$sms->setFromNumber($from_number);
$adapter->setTo($sms->getToNumber());
$adapter->setFrom($sms->getFromNumber());
$adapter->setBody($sms->getBody());
// give the provider name the same treatment as phone number
$sms->setProviderShortName($adapter->getProviderShortName());
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
$sms->save();
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Phabricator is running in silent mode. See `%s` '.
'in the configuration to change this setting.',
'phabricator.silent'));
}
try {
$result = $adapter->send();
list($sms_id, $sent_status) = $adapter->getSMSDataFromResult($result);
} catch (PhabricatorWorkerPermanentFailureException $e) {
$sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
$sms->save();
throw $e;
} catch (Exception $e) {
$sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
$sms->save();
throw new PhabricatorWorkerPermanentFailureException(
$e->getMessage());
}
$sms->setProviderSMSID($sms_id);
$sms->setSendStatus($sent_status);
$sms->save();
}
}

View file

@ -1,4 +0,0 @@
<?php
abstract class PhabricatorSMSWorker
extends PhabricatorWorker {}