Tutorial: Moderate Facebook posts and commands with Azure Content Moderator

  • 9 minutes to read

Caution

The Content Moderator Review tool is now deprecated and will be retired on 12/31/2021.

In this tutorial, you will learn how to use Azure Content Moderator to help moderate the posts and comments on a Facebook page. Facebook will send the content posted by visitors to the Content Moderator service. Then your Content Moderator workflows will either publish the content or create reviews within the Review tool, depending on the content scores and thresholds. See the Build 2017 demo video for a working example of this scenario.

Important

In 2018, Facebook implemented a more strict vetting policy for Facebook Apps. You will not be able to complete the steps of this tutorial if your app has not been reviewed and approved by the Facebook review team.

This tutorial shows you how to:

  • Create a Content Moderator team.
  • Create Azure Functions that listen for HTTP events from Content Moderator and Facebook.
  • Link a Facebook page to Content Moderator using a Facebook application.

If you don't have an Azure subscription, create a free account before you begin.

This diagram illustrates each component of this scenario:

Diagram of Content Moderator receiving information from Facebook through "FBListener" and sending information through "CMListener"

Prerequisites

  • A Content Moderator subscription key. Follow the instructions in Create a Cognitive Services account to subscribe to the Content Moderator service and get your key.
  • A Facebook account.

Create a review team

Refer to the Try Content Moderator on the web quickstart for instructions on how to sign up for the Content Moderator Review tool and create a review team. Take note of the Team ID value on the Credentials page.

Configure image moderation workflow

Refer to the Define, test, and use workflows guide to create a custom image workflow. Content Moderator will use this workflow to automatically check images on Facebook and send some to the Review tool. Take note of the workflow name.

Configure text moderation workflow

Again, refer to the Define, test, and use workflows guide; this time, create a custom text workflow. Content Moderator will use this workflow to automatically check text content. Take note of the workflow name.

Configure Text Workflow

Test your workflow using the Execute Workflow button.

Test Text Workflow

Create Azure Functions

Sign in to the Azure portal and follow these steps:

  1. Create an Azure Function App as shown on the Azure Functions page.

  2. Go to the newly created Function App.

  3. Within the App, go to the Platform features tab and select Configuration. In the Application settings section of the next page, select New application setting to add the following key/value pairs:

    App Setting name value
    cm:TeamId Your Content Moderator TeamId
    cm:SubscriptionKey Your Content Moderator subscription key - See Credentials
    cm:Region Your Content Moderator region name, without the spaces. You can find this name in the Location field of the Overview tab of your Azure resource.
    cm:ImageWorkflow Name of the workflow to run on Images
    cm:TextWorkflow Name of the workflow to run on Text
    cm:CallbackEndpoint Url for the CMListener Function App that you will create later in this guide
    fb:VerificationToken A secret token that you create, used to subscribe to the Facebook feed events
    fb:PageAccessToken The Facebook graph api access token does not expire and allows the function Hide/Delete posts on your behalf. You will get this token at a later step.

    Click the Save button at the top of the page.

  4. Go back to the Platform features tab. Use the + button on the left pane to bring up the New function pane. The function you are about to create will receive events from Facebook.

    Azure Functions pane with the Add Function button highlighted.

    1. Click on the tile that says Http trigger.
    2. Enter the name FBListener. The Authorization Level field should be set to Function.
    3. Click Create.
    4. Replace the contents of the run.csx with the contents from FbListener/run.csx
                    #r "Newtonsoft.Json" using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using System.Text; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Configuration;  public static async Task<IActionResult> Run(HttpRequest req, ILogger log) {     //This is the verification token you enter on the Facebook App Dashboard while subscribing your app for publishing events     var verificationToken = GetEnvironmentVariable("fb:VerificationToken");      //parse query parameter from Facebook     string hubMode = req.Query["hub.mode"];     string hubChallenge = req.Query["hub.challenge"];     string hubverify_token = req.Query["hub.verify_token"];      //This request is sent from FB when subscribing this endpoint     if(hubMode == "subscribe"){         if(hubverify_token == verificationToken)         {             return (ActionResult)new OkObjectResult(hubChallenge);         }         else         {             return new UnauthorizedObjectResult("Not Authorized");;         }        }         //Parsing the request that was sent from FB             string body = await new StreamReader(req.Body).ReadToEndAsync();     log.LogInformation($"FB Event Data: {body}");      var eventData = JsonConvert.DeserializeObject<JObject>(body);     var eventObject = eventData["entry"][0]["changes"][0]["value"];                       //This is the unique id that identifies the post on the FB graph     var postId = (string)eventObject["post_id"];                  //This tells us if this was image post or a Text post     var itemType = (string)eventObject["item"];      //Returning if it was not an Add     var verb = (string)eventObject["verb"];     if(verb.ToLower() != "add" ){         return (ActionResult)new OkObjectResult("Received");     }          //Name of the person who sent this post     var senderName = (string)eventObject["sender_name"];      switch(itemType){         case "photo":{             log.LogInformation("Pushing Image for Moderation");             var imageUrl = (string)eventObject["link"];             var jobId = await CreateContentModerationJob(log, postId,"image", imageUrl);             log.LogInformation($"CM Image JobId: {jobId}");             return (ActionResult)new OkObjectResult($"Image JobId: {jobId}");         }         case "post":{                         var text = (string)eventObject["message"];             if(!string.IsNullOrWhiteSpace(text))             {                 log.LogInformation("Pushing Text for Moderation");                 var jobId = await CreateContentModerationJob(log, postId, "text", text);                 log.LogInformation($"CM Text JobId: {jobId}");             }                          var photos = eventObject["photos"];             if(photos != null)             {                 var photoCollection = (JArray)photos;                 foreach (var p in photoCollection)                 {                     var jobId = await CreateContentModerationJob(log, postId,"image", p.Value<string>());                     log.LogInformation($"CM Image JobId: {jobId}");                 }             }              break;         }         case "status":         case "comment":             var commentId = (string)eventObject["comment_id"];             var comment = (string)eventObject["message"];             if(!string.IsNullOrWhiteSpace(comment))             {                 log.LogInformation("Pushing Text for Moderation");                 var jobId = await CreateContentModerationJob(log, commentId, "text", comment);                 log.LogInformation($"CM Text JobId: {jobId}");             }             break;             }      //responding to FB with 200 OK     return (ActionResult)new OkObjectResult("Received");     }  //This method invokes the Content Moderator Job API to create a job with workflow specified for the content type private static async Task<string> CreateContentModerationJob(ILogger log, string postId, string contentType, string contentValue) {     var subscriptionKey= GetEnvironmentVariable("cm:SubscriptionKey");     var teamId = GetEnvironmentVariable("cm:TeamId");                        var callbackEndpoint =$"{GetEnvironmentVariable("cm:CallbackEndpoint")}%26fbpostid={postId}";     var region = GetEnvironmentVariable("cm:Region");      string workflowName = "";     switch(contentType)     {         case "text": { workflowName = GetEnvironmentVariable("cm:TextWorkflow"); break;}         case "image": { workflowName = GetEnvironmentVariable("cm:ImageWorkflow"); break;}     }      var cmUrl = $"https://{region}.api.cognitive.microsoft.com/contentmoderator/review/v1.0/teams/{teamId}/jobs?ContentType={contentType}&ContentId={postId}&WorkflowName={workflowName}&CallBackEndpoint={callbackEndpoint}";                  log.LogInformation(cmUrl);      var client = new HttpClient();         client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);     var requestBodyObj = new { ContentValue = contentValue };     string requestBody = JsonConvert.SerializeObject(requestBodyObj);     log.LogInformation(requestBody);      HttpResponseMessage response = null;     string jobId = "";     int tryCount = 0;     do{             tryCount++;         var content = new StringContent(requestBody,Encoding.UTF8,"application/json");         response = await client.PostAsync(cmUrl, content);         var cmResp = await response.Content.ReadAsStringAsync();         var res = JsonConvert.DeserializeObject<JObject>(cmResp);         jobId = (string)res["JobId"];         log.LogInformation($"Response from CM: {res.ToString()}");            if(!response.IsSuccessStatusCode){             System.Threading.Thread.Sleep(2000);         }      }while(!response.IsSuccessStatusCode && tryCount < 3);      return jobId; }  //Method to read app settings public static string GetEnvironmentVariable(string name) {     return System.Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process); }                              
  5. Create a new Http trigger function named CMListener. This function receives events from Content Moderator. Replace the contents of the run.csx with the contents from CMListener/run.csx

                    #r "Newtonsoft.Json" using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using System.Text; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Configuration;  public static async Task<IActionResult> Run(HttpRequest req, ILogger log) {     string body = await new StreamReader(req.Body).ReadToEndAsync();     log.LogInformation($"CM Data: {body}");      var eventData = JsonConvert.DeserializeObject<JObject>(body);      //Tells us if the callback was of type Job or Review.                        var callbackType = (string)eventData["CallBackType"];      //Getting postid from the callback url     var postId = req.Query["fbpostid"];      switch(callbackType.ToLower())     {          case "job":{              //The callback contains the a Review Id if a manual review was created based on the criteria specified in the workflow             var reviewId = (string)eventData["ReviewId"];                                         //If you have posts hidden by default then you can make it visible if there was no Review created             if(string.IsNullOrWhiteSpace(reviewId))             {                  await UpdateFBPostVisibility(log, postId, false);             }                break;         }         case "review": {             var reviewerResult = eventData["ReviewerResultTags"];             var isAnyTagTrue = false;             foreach (var x in ((JObject)reviewerResult))             {                 string name = x.Key;                               string val = (string)reviewerResult[name];                 log.LogInformation($"Tag: {name}, Value: {val}");                 if(val.ToLower() == "true")                 {                    isAnyTagTrue = true;                     break;                      }              }                          //You can delete the POST if it has tags that do not meet your policies             //Following code deletes the post if any tag came back True              if(isAnyTagTrue)             {                 await DeleteFBPost(log, postId);             }              break;         }      }      //Respond to Content Moderator with http 200 OK     return (ActionResult)new OkObjectResult("Callback Processed"); }  //This method updates the visibility of the FB Post private static async Task UpdateFBPostVisibility(ILogger log, string postId, bool hide) {     log.LogInformation($"FB Updating Post Visibility: {postId}, Hidden: {hide}");      var fbPageAccessToken = GetEnvironmentVariable("fb:PageAccessToken");     var fbUrl = $"https://graph.facebook.com/v2.9/{postId}?access_token={fbPageAccessToken}";                    using (var client = new HttpClient())     {         using (var content = new MultipartFormDataContent())         {             content.Add(new StringContent(hide.ToString().ToLower()), "is_hidden");                                                  var result = await client.PostAsync(fbUrl, content);             log.LogInformation($"FB Response: {result.ToString()}");         }     }     }  //This method deletes the FB Post private static async Task DeleteFBPost(ILogger log, string postId) {     log.LogInformation($"FB Deleting Post: {postId}");      var fbPageAccessToken = GetEnvironmentVariable("fb:PageAccessToken");     var fbUrl = $"https://graph.facebook.com/v2.9/{postId}?access_token={fbPageAccessToken}";                    using (var client = new HttpClient())     {         var result = await client.DeleteAsync(fbUrl);         log.LogInformation($"FB Response: {result.ToString()}");             }     }  private static string GetEnvironmentVariable(string name) {     return System.Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process); }                              

Configure the Facebook page and App

  1. Create a Facebook App.

    facebook developer page

    1. Navigate to the Facebook developer site
    2. Go to My Apps.
    3. Add a New App.
    4. Provide a name
    5. Select Webhooks -> Set Up
    6. Select Page in the dropdown menu and select Subscribe to this object
    7. Provide the FBListener Url as the Callback URL and the Verify Token you configured under the Function App Settings
    8. Once subscribed, scroll down to feed and select subscribe.
    9. Select the Test button of the feed row to send a test message to your FBListener Azure Function, then hit the Send to My Server button. You should see the request being received on your FBListener.
  2. Create a Facebook Page.

    Important

    In 2018, Facebook implemented a more strict vetting of Facebook apps. You will not be able to execute sections 2, 3 and 4 if your app has not been reviewed and approved by the Facebook review team.

    1. Navigate to Facebook and create a new Facebook Page.
    2. Allow the Facebook App to access this page by following these steps:
      1. Navigate to the Graph API Explorer.
      2. Select Application.
      3. Select Page Access Token, Send a Get request.
      4. Select the Page ID in the response.
      5. Now append the /subscribed_apps to the URL and Send a Get (empty response) request.
      6. Submit a Post request. You get the response as success: true.
  3. Create a non-expiring Graph API access token.

    1. Navigate to the Graph API Explorer.
    2. Select the Application option.
    3. Select the Get User Access Token option.
    4. Under the Select Permissions, select manage_pages and publish_pages options.
    5. We will use the access token (Short Lived Token) in the next step.
  4. We use Postman for the next few steps.

    1. Open Postman (or get it here).

    2. Import these two files:

      1. Postman Collection
      2. Postman Environment
    3. Update these environment variables:

      Key Value
      appId Insert your Facebook App Identifier here
      appSecret Insert your Facebook App's secret here
      short_lived_token Insert the short lived user access token you generated in the previous step
    4. Now run the 3 APIs listed in the collection:

      1. Select Generate Long-Lived Access Token and click Send.
      2. Select Get User ID and click Send.
      3. Select Get Permanent Page Access Token and click Send.
    5. Copy the access_token value from the response and assign it to the App setting, fb:PageAccessToken.

The solution sends all images and text posted on your Facebook page to Content Moderator. Then the workflows that you configured earlier are invoked. The content that does not pass your criteria defined in the workflows gets passed to reviews within the review tool. The rest of the content gets published automatically.

Next steps

In this tutorial, you set up a program to analyze product images, tag them by product type, and allow a review team to make informed decisions about content moderation. Next, learn more about the details of image moderation.