Tutorial: Moderate Facebook posts and commands with Azure Content Moderator

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.

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"

Important

In 2018, Facebook implemented a more strict vetting of 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.

Prerequisites

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.
    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. Click on My Apps.
    3. Add a New App.
    4. name it something
    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. Click on 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. Click 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 for the purpose of tagging them by product type and allowing a review team to make informed decisions about content moderation. Next, learn more about the details of image moderation.