facebook @workplace

Building an intranet using modern SharePoint I was asked by a customer, if this is possible to show information from their facebook @workplace portal. I started digging and reading the documentation only to realize, that no matter if I were using modern or classic approach, still there are no ready-to-use scripts from facebook, allowing to copy-paste and then be able to display ex. information from Newsfeed.

Solution

I wanted very much to not use custom programming, rather to focus on ootb possibilities. After reading facebook API’s documentation on custom integrations (source) I decided to create a solution working in a following scenario:

  1. @workplace uses webhooks to call Microsoft Flow (source1: webhooks getting started, source2: webhooks in @workplace
  2. Microsoft Flow receives a call, parses it and stores data in a SharePoint list
  3. Dedicated web part in SharePoint shows data from the list

Prerequisites

Custom integration in @workplace

To even be able, to use a webhook, first it needs to be defined. To do that you need to go to “Integrations” page (being a @workplace administrator): https://[your-company].facebook.com/work/admin/?section=apps&ref=bookmarks and then to create new custom integration:

@workplace Integrations page

Then you have to define its name and optionally a description.

Now you have to select what permissions does the integration need (in my case this is only “Read group content”), then select groups, to which it will have access (in my cases this is “All groups”) and finally configure the webhooks.

Remember, that you can only subscribe one URL per webhook topic, but you may use the same URL for multiple topics.

In my case I only configured webhooks for the “Groups”. How? Read below.

Webhook verification

To save a webhook it has to have verified connection with the callback URL. The callback URL is in fact a URL of the Microsoft Flow, which is going to receive and parse data.

The tricky thing is, that the callback verification must be done using GET request, whereas further webhooks’ requests are done using POST calls.

To do that simply create a Flow with a “Request” action as the trigger. Then parse the header parameters, using the “Parse JSON” action:

  1. Content: triggerOutputs()[‘queries’]
  2. Schema:
{
    "type": "object",
    "properties": {
        "hub.mode": {
            "type": "string"
        },
        "hub.challenge": {
            "type": "string"
        },
        "hub.verify_token": {
            "type": "string"
        }
    }
}

Finally add the “Response” action, and use “hub.challenge” value as a Body:

Return hub.challenge parameter

Do not forget, to change how the Flow can be triggered – set the method to “GET” (1) in trigger action (it’s under the “advanced options”), publish your workflow and finally copy its URL (2):

Request action configuration in Flow for @workplace

Now paste that URL in a “Callback URL” field in webhook configuration and type any “Verify Token” value (it should be used to check, if the GET request really comes from your webhook) and hit “Save”:

Webhook Callback URL configuration in @workplace

Now you’re ready to go – you will notice in your Flow a new run, completed successfully and your new “Custom integration” will be saved. 

Whenever you decide to change ANYTHING in your Custom Integration configuration, you will need to verify Callback URL again – so again to send a GET request to your Flow.

Building a Flow

Remember to keep the actions for GET response within your workflow. I did it by adding a condition action, with a condition: “1 is equal to 2”, so that Flow always goes the other way. I am changing its behavior between receiving GET and POST requests, which is impossible to automate right now. So when I modifies my @workplace Custom integration settings, I change the method to “GET” and the condition to “1 is equal 1”. After verification passes, I am turning them back to “POST” and “1 is equal to 2”.

Request body schema

In my “POST” branch, the first action is “Parse JSON”, used to parse the request body. Before I made a valid schema I did dozens of calls from @workplace to Flow, to see how specific actions are represented in JSON. I have named the following scenarios:

  1. Membership – when a new user joins a group
  2. Comment – when a new comment is created
  3. Post – when a new post is written
    1. Post with a single photo
    2. Post with multiple photos
    3. Post without photo (a status)
    4. Event
    5. all others

So the schema looks like below (it contains properties for all scenarios, set to not be mandatory):

{
    "type": "object",
    "properties": {
        "entry": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "changes": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "field": {
                                    "type": "string"
                                },
                                "value": {
                                    "type": "object",
                                    "properties": {
                                        "from": {
                                            "type": "object",
                                            "properties": {
                                                "id": {
                                                    "type": "string"
                                                },
                                                "name": {
                                                    "type": "string"
                                                }
                                            }
                                        },
                                        "member": {
                                            "type": "object",
                                            "properties": {
                                                "id": {
                                                    "type": "string"
                                                },
                                                "name": {
                                                    "type": "string"
                                                }
                                            }
                                        },
                                        "update_time": {
                                            "type": "string"
                                        },
                                        "verb": {
                                            "type": "string"
                                        },
                                        "community": {
                                            "type": "object",
                                            "properties": {
                                                "id": {
                                                    "type": "string"
                                                }
                                            }
                                        },
                                        "actor": {
                                            "type": "object",
                                            "properties": {
                                                "id": {
                                                    "type": "string"
                                                },
                                                "name": {
                                                    "type": "string"
                                                }
                                            }
                                        },
                                        "attachments": {
                                            "type": "object",
                                            "properties": {
                                                "data": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "object",
                                                        "properties": {
                                                            "url": {
                                                                "type": "string"
                                                            },
                                                            "subattachments": {
                                                                "type": "object",
                                                                "properties": {
                                                                    "data": {
                                                                        "type": "array",
                                                                        "items": {
                                                                            "type": "object",
                                                                            "properties": {
                                                                                "url": {
                                                                                    "type": "string"
                                                                                },
                                                                                "media": {
                                                                                    "type": "object",
                                                                                    "properties": {
                                                                                        "image": {
                                                                                            "type": "object",
                                                                                            "properties": {
                                                                                                "src": {
                                                                                                    "type": "string"
                                                                                                },
                                                                                                "width": {
                                                                                                    "type": "number"
                                                                                                },
                                                                                                "height": {
                                                                                                    "type": "number"
                                                                                                }
                                                                                            }
                                                                                        }
                                                                                    }
                                                                                },
                                                                                "type": {
                                                                                    "type": "string"
                                                                                },
                                                                                "target": {
                                                                                    "type": "object",
                                                                                    "properties": {
                                                                                        "url": {
                                                                                            "type": "string"
                                                                                        },
                                                                                        "id": {
                                                                                            "type": "string"
                                                                                        }
                                                                                    }
                                                                                },
                                                                                "title": {
                                                                                    "type": "string"
                                                                                }
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            },
                                                            "media": {
                                                                "type": "object",
                                                                "properties": {
                                                                    "image": {
                                                                        "type": "object",
                                                                        "properties": {
                                                                            "src": {
                                                                                "type": "string"
                                                                            },
                                                                            "width": {
                                                                                "type": "number"
                                                                            },
                                                                            "height": {
                                                                                "type": "number"
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            },
                                                            "type": {
                                                                "type": "string"
                                                            },
                                                            "description": {
                                                                "type": "string"
                                                            },
                                                            "target": {
                                                                "type": "object",
                                                                "properties": {
                                                                    "url": {
                                                                        "type": "string"
                                                                    },
                                                                    "id": {
                                                                        "type": "string"
                                                                    }
                                                                }
                                                            },
                                                            "title": {
                                                                "type": "string"
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        },
                                        "type": {
                                            "type": "string"
                                        },
                                        "target_type": {
                                            "type": "string"
                                        },
                                        "comment_id": {
                                            "type": "string"
                                        },
                                        "post_id": {
                                            "type": "string"
                                        },
                                        "created_time": {
                                            "type": "string"
                                        },
                                        "message": {
                                            "type": "string"
                                        },
                                        "permalink_url": {
                                            "type": "string"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "id": {
                        "type": "string"
                    },
                    "time": {
                        "type": "number"
                    }
                },
                "required": [
                    "changes",
                    "id",
                    "time"
                ]
            }
        },
        "object": {
            "type": "string"
        }
    }
}

Step by step

After parsing request body using the schema, Flow is doing the following steps:

  1. Get operation field – whether post is a “Comment”, “Membership” or “Posts”
    I do it using the “Compose” action and the following expressions:

    body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['field']
  2. Switch branches based on the operation type.
  3. For each type I am getting a proper ID (ex. comment_id for “Comments” or post_id for “Posts”)
  4. Then I am querying SharePoint to check, whether there is already a record with that ID. If there is, then obviously workflow ends its execution.
  5. If this is a “Post” type, then I am looking an internal operation type property using the following expression:
    body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['field']
  6. Based on the outcome, Flow switches between:
    1. Photo
    2. Status
    3. Event
    4. and other requests (haven’t seen anything “other” yet)
  7. Then for “Photo” type it also checks, whether there is only one or multiple photos (gallery) checking, if the type is “album” by evaluating the below expression:
    body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['value']['attachments']['data']?[0]?['type']
  8. Finally, for each message type it is also trying to match user with an existing one, by using action “Office 365 Get user profile (V2)” and concatenating users first and last name together with company’s domain as a UPN:
    concat(trim(replace(trim(body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['value']['from']['name']), ' ', '.')), '@the-company.com')
  9. Obviously, if user is not found, the action triggers error, so for that case the next action, which is “Create list item” has configured “run after” settings also to “has failed”, so that no matter what happens, Flow wont fail for that reason:
    Flow "Configure run after" settings
  10. In the end Flow is creating new item on a list, combining all information together:
    Flow create new list item action

Structure of Flow’s actions, on a medium level of details, looks as below for the described solution:

@workplace integration with Microsoft Flow

Icons (i) visible next to arrows mark relations, in which the next action is executed even if the previous one ends up with failure.

Structure of a single block, used to save data from @workplace into a SharePoint list, looks as on the example below (it shows saving data for a new post):

Saving data from @workplace to SharePoint list

Data structure

For storing comments I am using a single SharePoint list built from the following columns:

  1. Title – holds type of the information.
  2. Author – a text column to keep name and last name of an author.
  3. Date – when the event occurred (from webhook).
  4. Post/ Comment – a multiline text field, allowing HTML formatting, to keep body of the message.
  5. Image – again, a multiline text field, allowing HTML formatting, to store <img> tag with the URL of the image, attached to a message – this is because Flow does not support “Picture/ Hyperlink” fields having set type to “Picture”.
  6. SourceURL – a “Picture/ Hyperlink” field with a type set to “Hyperlink”.
  7. AuthorPPL – again, an author field, but this time as a “Person or group” field. My Flow is trying to map Author data to an existing SP User.
  8. ItemId – an identifier of the message: comment_id, post_id or a member_id.

The list, filled up with data, looks as below:

Data from @workplace in SharePoint list

Things to remember

During that project I learnt the following things. Please, keep them in mind when trying to follow my steps:

  1. @workplace will send GET request to FLOW every time a “Custom integration” is modified and saved.
  2. @workplace will be sending requests for a single event as long as it won’t receive a “200 OK” response – be prepared to verify if the content has already been added.
  3. @workplace is not always waiting for the response. I see dozens of failed flows just because @workplace abandoned waiting for the response. I have no idea why, it seems to be really random. Anyway – in case it fails waiting, it will try to send the same data again and again until receiving “200 OK”. Only the frequency will be lower and after couple of tries it will stop.
  4. Using the “run after” configuration in Flow’s actions can be really useful – both for overcoming missing information or actions which can end up with errors which we don’t really care about, as well as doing conditional blocks. In my Flow I was using this configuration very often.

 

Thanks for reaching this sentence. I hope you find it useful. Don’t hesitate to contact me in case you have any questions or leave your comment below!