diff --git a/src/App/Repository/ConfigRepository.php b/src/App/Repository/ConfigRepository.php index c1dc3df020..f6ecda2e92 100644 --- a/src/App/Repository/ConfigRepository.php +++ b/src/App/Repository/ConfigRepository.php @@ -77,8 +77,8 @@ public function get($group) { $this->verifyGroup($group); - if ($group == 'multisite') { - return $this->getMultisiteGroup(); + if ($group == 'deployment_id') { + return $this->getDeploymentIdGroup(); } $config = []; @@ -112,20 +112,37 @@ public function get($group) return $this->getEntity(['id' => $group] + $config); } - protected function getMultisiteGroup() + protected function getDeploymentIdGroup() { - // The multisite group is special in that it's not persisted in the deployment's database. + // The Deployment Id group is special in that it's not persisted in the deployment's database. + $config = []; + + // Multisite IDs if (app('multisite')->enabled()) { $multi = app('multisite'); - $config = [ + $config['multisite'] = [ 'enabled' => true, 'site_id' => $multi->getSiteId(), 'site_fqdn' => $multi->site->getClientUri(), ]; } else { - $config = [ 'enabled' => false ]; + $config['multisite'] = []; } - return $this->getEntity(['id' => 'multisite'] + $config); + + $analytics_prefix = env('USH_ANALYTICS_PREFIX', null); + $analytics_id = + $config['multisite']['site_id'] ?? + env('USH_ANALYTICS_ID', null); + if ($analytics_prefix && $analytics_id) { + $config['analytics'] = [ + 'prefix' => $analytics_prefix, + 'id' => $analytics_id, + ]; + } else { + $config['analytics'] = []; + } + + return $this->getEntity(['id' => 'deployment_id'] + $config); } // UpdateRepository @@ -136,7 +153,7 @@ public function update(Entity $entity) $this->verifyGroup($group); - if ($group == 'multisite') { + if ($group == 'deployment_id') { return; /* noop */ } @@ -205,7 +222,7 @@ public function groups() return [ 'features', 'site', - 'multisite', + 'deployment_id', 'test', 'data-provider', 'map', diff --git a/src/Core/Tools/Authorizer/ConfigAuthorizer.php b/src/Core/Tools/Authorizer/ConfigAuthorizer.php index d2ab375aa6..933600e060 100644 --- a/src/Core/Tools/Authorizer/ConfigAuthorizer.php +++ b/src/Core/Tools/Authorizer/ConfigAuthorizer.php @@ -39,13 +39,13 @@ class ConfigAuthorizer implements Authorizer * Public config groups * @var [string, ...] */ - protected $public_groups = ['features', 'map', 'site', 'multisite']; + protected $public_groups = ['features', 'map', 'site', 'deployment_id']; /** * Public config groups * @var [string, ...] */ - protected $readonly_groups = ['features', 'multisite']; + protected $readonly_groups = ['features', 'deployment_id']; /* Authorizer */ public function isAllowed(Entity $entity, $privilege) diff --git a/v5/Http/Controllers/USSDController.php b/v5/Http/Controllers/USSDController.php new file mode 100644 index 0000000000..40a073a604 --- /dev/null +++ b/v5/Http/Controllers/USSDController.php @@ -0,0 +1,153 @@ + WARNING: This is a crutch <--- + * + * We are adding this specific controller for posts coming from USSD sources, + * so that incoming posts can have contact information attached, just like SMS. + * + * Eventually we should have a more evolved data source framework that allows + * bringing in structured posts with source metadata. + */ +class USSDController extends PostController +{ + public function show(int $id) + { + throw new Exception("Invalid controller method"); + } + + public function index() + { + throw new Exception("Invalid controller method"); + } + + /** + * Overriding the POST method so as to handle contact information + */ + public function store(Request $request) + { + /* TODO: USSD-specific authorization? + * + * Presently the assumption is that these come in anonymously, + * because that's how ussd-engine operates. + * But what would happen if they came in as an authenticated user? + * What would be considered the source of the post? The user, or + * the detailed shource information. + * + * As we are thinking of more loosely coupled datasources, this + * should be taken into account. + */ + + /* This method works on an additional property to the post body: + * + * { + * ..., + * source_info: { + * received: "YYYY-MM-DDTHH:MM:SSZ", + * data_source: "ussd", + * type: "phone", + * contact: "xxxxxxxxx" + * } + * ... + * } + * + * Validate this property from the request. + */ + $source_info = $request->input('source_info'); + $val_rules = [ + 'source_info' => 'required', + // TODO: couldn't get this validation just right yet + // 'source_info.received' => 'required|string|date_format:' . \DateTime::ISO8601, + 'source_info.data_source' => 'required|string|in:ussd', + 'source_info.type' => 'required|string|in:phone', + 'source_info.contact' => 'required|string|min:6' + ]; + $v = ValidatorRunner::runValidation( + ['source_info' => $source_info], + $val_rules, + [] // TODO: custom messages? + ); + if ($v->fails()) { + return self::make422($v->getErrors()); + } + + /* Call up the parent controller to get the post created */ + $post = parent::store($request); + if (!($post instanceof PostResource)) { + /* An error has happened creating the post, shortcircuit to that */ + return $post; + } + + DB::beginTransaction(); + try { + /* Lookup / create contact if not present */ + /* assert type is phone */ + $contact = Contact::firstOrCreate([ + 'data_source' => $source_info['data_source'], + 'type' => $source_info['type'], + 'contact' => $source_info['contact'] + ], [ + 'can_notify' => false + ]); + + /* Create message record */ + $message = new Message; + $message->contact()->associate($contact)->save(); + $message->post_id = $post->id; // this is not yet an eloquent-managed relationship + /* USSD doesn't technically come in with a message, we shall craft + * a stand-in. */ + $message->title = "(Fulfilled USSD survey)"; + /* TODO: anything more useful that could go here? */ + $message->message = "(Fulfilled USSD survey)"; + $message->datetime = \DateTime::createFromFormat(\DateTime::ISO8601, $source_info['received']); + $message->data_source = $source_info['data_source']; + /* TODO: anything useful from ussd-engine that could go here? */ + $message->data_source_message_id = "random-" . UUID::uuid4()->toString(); + $message->type = 'ussd'; + $message->status = 'received'; + $message->direction = 'incoming'; + $message->notification_post_id = null; + $message->save(); + + DB::commit(); + + /* The post resource should now be re-rendered, because of the + * information added to it since creation in the parent. + * This is not ideal performance-wise, but we'll take the hit + * for now. + */ + return new PostResource($post->resource); + } catch (\Exception $e) { + DB::rollback(); + return self::make500($e->getMessage()); + } + } + + public function patch(int $id, Request $request) + { + throw new Exception("Invalid controller method"); + } + + public function update(int $id, Request $request) + { + throw new Exception("Invalid controller method"); + } + + public function delete(int $id, Request $request) + { + throw new Exception("Invalid controller method"); + } +} diff --git a/v5/Http/Middleware/V5GlobalScopes.php b/v5/Http/Middleware/V5GlobalScopes.php index b4b764b28c..95f75feaf0 100644 --- a/v5/Http/Middleware/V5GlobalScopes.php +++ b/v5/Http/Middleware/V5GlobalScopes.php @@ -25,7 +25,11 @@ public function handle($request, Closure $next) * @TODO more tests maybe???????? * @TODO remove the need for isSavingPost */ - $isSavingPost = $request->path() === 'api/v5/posts' && $request->isMethod('post'); + $isSavingPost = $request->isMethod('post') && \ + in_array($request->path(), [ + 'api/v5/posts', + 'api/v5/posts/_ussd' + ]); if (!$isSavingPost) { Category::addGlobalScope(new CategoryAllowed); diff --git a/v5/Models/Contact.php b/v5/Models/Contact.php index aab731f5f7..d724f947ed 100644 --- a/v5/Models/Contact.php +++ b/v5/Models/Contact.php @@ -8,12 +8,22 @@ class Contact extends BaseModel 'messages' ]; + const CREATED_AT = 'created'; + const UPDATED_AT = 'updated'; + /** * Add eloquent style timestamps * * @var boolean */ - public $timestamps = false; + public $timestamps = true; + + /** + * The storage format of the model's date columns. + * + * @var string + */ + protected $dateFormat = 'U'; /** * Specify the table to load with Survey diff --git a/v5/Models/Message.php b/v5/Models/Message.php index db0243b6ae..53893ac23c 100644 --- a/v5/Models/Message.php +++ b/v5/Models/Message.php @@ -7,13 +7,24 @@ class Message extends BaseModel public static $relationships = [ 'contact' ]; + # --> and relationship to Post? + + const CREATED_AT = 'created'; + const UPDATED_AT = null; /** * Add eloquent style timestamps * * @var boolean */ - public $timestamps = false; + public $timestamps = true; + + /** + * The storage format of the model's date columns. + * + * @var string + */ + protected $dateFormat = 'U'; /** * Specify the table to load with Survey diff --git a/v5/routes/api.php b/v5/routes/api.php index 4a1cd6168b..e1088feba4 100644 --- a/v5/routes/api.php +++ b/v5/routes/api.php @@ -82,5 +82,6 @@ ], function () use ($router) { // Public access $router->post('/', 'PostController@store'); + $router->post('/_ussd', 'USSDController@store'); }); });