Using Slack for content approval workflow in eZ Platform & Symfony

Extending the use of Slack to include content approval in addition to notifications and internal communication can be quite efficient. The goal is to have a streamlined notification, review and approval process that does not slow down content production. In this post I show how you can handle content approval from eZ Platform using Slack to receive notifications as well as doing the actual approval.

This is a followup on how to achieve content approval in eZ Platform using the method described in the blog post covering Node.js. The scenario I needed to solve included user generated content that was location based. To get a preview of the location I used the Google Maps API to generate a preview image.

Configuring Slack

We need to register a new App in Slack, define the permissions, get our access token and define the request URL. Check out the details in my previous post on how to get this done.

Sending the Slack content approval Message

To send the Slack content approval message we first need to construct an array that defines our message. Details on how to do this can be found in the API documentation for interactive message buttons on Slack. Note that we added the content object ID as part of the message.

Once our array is done we build the HTTP POST body that we send to slack. You can use the http_build_query() function to build URL Encoded form data with our token, channel, text and attachments. In this example I am shipping this over to Slack as a HTTP POST call using curl.

The message in Slack is Shown in the screenshot below. Pretty neat?

no-alt

Code

The code for sending the interactive Slack message is shown below.

function sendSlackMessage( $lat, $long, $objectId ) { $message = "Do you want to approve this locaiton?"; $token = "xxx-yyy-xxx"; $attachments = array( array(  "fallback" => "Location approval",  "callback_id" => "location_approve_42",  "image_url" => "http://maps.googleapis.com/maps/api/staticmap?center=" . $lat . "," . $long . "&zoom=12&size=1024x768&sensor=false&maptype=hybrid&markers=color:red%7Clabel:K%7C" . $lat . "," . $long ."",  "fields" => array(    array(     "title" => "Latitude",     "value" => $lat,     "short" => true    ),    array(     "title" => "Longitude",     "value" => $long,     "short" => true    )  ),  "actions" => array(    array(     "name" => "approve_action",      "text" => "Approve",      "type" => "button",      "value" => $objectId    ),    array(     "name" => "reject_action",     "text" => "Reject",     "style" => "danger",     "type" => "button",     "value" => $objectId,     "confirm" => array(      "title" => "Are you sure?",      "text" => "Are you sure you want to delete this submission?",      "ok_text" => "Yes",      "dismiss_text" => "No"     )   )  ) )); $slackMessage = array(  "token" => $token,  "channel" => "CXXXYYY",  "text" => $message,  "attachments" => json_encode( $attachments) ); $payload = http_build_query($slackMessage); $ch = curl_init("https://slack.com/api/chat.postMessage"); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); curl_close($ch); return json_decode($result);}

Preparing your endpoint

For this example we need a public accessible endpoint, I am using /myapi/approve as the example here. We need to define the access control in Symfony which is done in security.yml by adding the following.

access_control: - { path: ^/myapi/approve, roles: [IS_AUTHENTICATED_ANONYMOUSLY] }

This basically means that the endpoint will be accessible without the need to authenticate. This is useful as we will use this for the callback from Slack.

Approving the content using a callback

Now we have shipped off our approval notification message to Slack. We included two action buttons; one for approval and one for rejection. Our /myapi/approve endpoint is where we have configured Slack to send the callback. The callback happens once someone pushes the approve or reject button in Slack. The approveAction() method is part of our controller and is where the magic happens.

The first thing we do is to json_decode the message from Slack and verify that our $payload->token is the same as the one from Slack. This is the authentication check we need to do.

Then we initialize our content and location service from eZ Platform and fetch the content object from the repository. The object ID is fetched from the Slack Message.

Then we come to the business part where we check the action, either reject_action or approve_action. In the case of a reject action I am simply deleting the object from the repository. This is what makes sense for my use case as this user generated content that I want to moderate.

The approve action simply moves the object to a different location from an "incoming" location. You could of course use other methods like visibility or object states. But the principle is the same.

In both cases we also update the original message from Slack and sends it back. This modifies the message in Slack so you can see who approved it and when. Quite useful.

no-alt

Callback code

The code for the content approval callback is shown below.

/** * @Route("/approve") * @Method({"POST"}) * * @return Response */public function approveAction(Request $request): Response { $payload = json_decode($request->get('payload')); // Authorization check if ( $payload->token!= 'XXX-token-YYY' ) {  return $this->getResponse("No Authorization"); } // Fetch service objects $repository = $this->get( 'ezpublish.api.repository' ); $contentService = $repository->getContentService(); $locationService = $repository->getLocationService(); // Fetch object and $contentObjectId = $payload->actions[0]->value; $object = $contentService->loadContent($contentObjectId); $contentInfo = $contentService->loadContentInfo($contentObjectId); if ( $payload->actions[0]->name == "reject_action" ) {  // Delete object from eZ Platform repository  $repository->sudo(   function () use ($contentService, $contentInfo) {    $contentService->deleteContent($contentInfo);   }  );  $originalMessage = $payload->original_message;  $originalMessage->text = "Message rejected and deleted by " . $payload->user->name;  $originalMessage->attachments[0]->actions = [];  $response = new Response(json_encode( $originalMessage ));  $response->headers->set('Content-Type', 'application/json');  return $response; } if ( $payload->actions[0]->name == "approve_action" ) {  $mainLocation = $locationService->loadLocation($contentInfo->mainLocationId);  $destLocation = $locationService->loadLocation(42);  // Move content to the right location  $repository->sudo(   function () use ($locationService, $mainLocation, $destLocation) {    $locationService->moveSubtree($mainLocation,$destLocation);   }  );  $originalMessage = $payload->original_message;  $originalMessage->text = "Message approved by " . $payload->user->name;  $originalMessage->attachments[0]->actions = [];  $response = new Response(json_encode( $originalMessage ));  $response->headers->set('Content-Type', 'application/json');  return $response; }}