facebook @workplace

Gdy budowałem intranet używając modern SharePoint zostałem zapytany, czy byłoby możliwe, by wyświetlać w nim informacje pochodzące z ich portalu facebook @workplace. Zacząłem szukać i przeglądać dokumentację po to tylko, by dowiedzieć się, że bez znaczenia będzie fakt, czy użyję podejścia modern, czy classic w SharePoint, @workplace i tak nie posiada gotowych skryptów, które mógłbym skopiować i wkleić do portalu, by móc np. wyświetlać dane z Newsfeed.

Rozwiązanie

Zależało mi na znalezieniu takiego rozwiązania, które będzie wykorzystywać dostępne możliwości pudełkowe, nie zaś na zaprogramowaniu dedykowanego rozwiązania. Po przeczytaniu dokumentacji API @workplace na temat „custom integrations” (źródło) zdecydowałem się na stworzenie rozwiązania, które będzie wykorzystywać poniższe aplikacje i funkcjonalności:

  1. @workplace używa webhooków do wzbudzania Microsoft Flow (źródło 1: webhooks getting started, źródło 2: webhooks in @workplace
  2. Microsoft Flow otrzymuje żądanie, przetwarza je i zapisuje dane na liście SharePoint
  3. Dedykowany web part w SharePoint pokazuje dane z listy

Wymagania wstępne

Custom integration w @workplace

By w ogóle móc użyć webhooków, najpierw należy zdefiniować customową intgrację. Do wykonania tego musisz przejść na stronę „Integrations” (będąc administratorem @workplace): https://[your-company].facebook.com/work/admin/?section=apps&ref=bookmarks i utworzyć nową integrację:

@workplace Integrations page

Następnie zdefiniuj jej nazwę i opcjonalnie opis.

Teraz musisz wybrać uprawnienia, jakie ma posiadać (w moim przypadku było to wyłącznie „Read group content”), następnie wskazać grupy, z jakich dane mają być pobierane (w moim przypadku „All groups”) i na koniec skonfigurować webhooki.

Pamiętaj, że jeden adres URL może być subskrybowany do wyłącznie jednego topica w webhooku, jednak wiele topiców, w jednym webhooku, może używać tego samego adresu URL.

W moim przypadku skonfigurowałem wyłącznie webhooka dla „Groups”. Jak? Czytaj dalej.

Weryfikacja webhook

By móc zapisać dane webhooka należy go zweryfikować poprzez wysłanie żądania pod callback URL. W naszym przypadku callback URL to po prostu adres URL Microsoft Flow, którego będziemy używać do odbierania i przetwarzania danych.

Warto zwrócić uwagę, że weryfikacja adresu callback musi zostać wykonana poprzez odebranie żądania GET, tymczasem wszystkie dalsze żądania z webhooka będą realizowane przy użyciu akcji POST.

Sam Flow jest dość prosty. Najpierw akcja „Request”, która uruchamia przepływ. Następnie parsowanie danych z query stringa, używając akcji „Parse JSON”:

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

I na koniec akcja „Response”, która używa wartości parametru „hub.challenge” jako Body:

Return hub.challenge parameter

Nie zapomnij zmienić sposobu, w jaki Flow ma być wyzwalany. Ustaw „GET” (1) w  polu „method” (znajduje się w sekcji „advanced options”), opublikuj workflow i skopiuj jego adres URL (2):

Request action configuration in Flow for @workplace

Teraz wklej ten adres w polu „Callback URL”, w konfiguracji webhooka i wpisz dowolną wartość w pole „Verify Token” (ta wartość powinna być użyta do weryfikacji, czy żądanie odebrane z GET jest faktycznie wysłane z Twojego webhooka) i zapisz:

Webhook Callback URL configuration in @workplace

Gotowe! Zauważysz, na liście historii uruchomień Twojego Flow, że zakończył się z sukcesem, zaś nowo przygotowana „Custom integration” zostanie zapisana. 

Kiedy zdecydujesz o konieczności zmiany CZEGOKOLWIEK w konfiguracji swojej Custom Integration, będziesz musiał wykonać weryfikację Callback URL ponownie – czyli ponownie odebrać żądanie używając GET.

Budowa Flow

Pamiętaj, by zachować akcje do obsługi żądań GET w Twoim Flow. Nie usuwaj ich. Ja to zrobił poprzez utworzenie akcji warunkowej z ustawionym warunkiem „1 jest równe 2”, także za każdym razem Flow wykona ścieżkę „No”. W razie konieczności weryfikacji „Custom Integration” po prostu ręcznie zmieniam rodzaj wywołania mojego Flow z „POST” na „GET” i warunek na „1 równa się 1”. Po weryfikacji z powrotem ustawiam wywołanie na „POST”, a warunek na „1 jest równe 2”.

Schemat request body

W gałęzi obsługującej żądania „POST’, pierwsza akcja to „Parse JSON”, której używam do przetworzenia treści żądania. Zanim stworzyłem poprawny schemat , wykonałem dziesiątki testów pisząc różne posty i komentarze w @workplace i podglądając strukturę JSON tych żądań. Na ich podstawie przygotowałem jeden, spójny plik, który obsługuje poniższe scenariusze:

  1. Membership – gdy użytkownik dołącza do grupy
  2. Comment – gdy dodany jest nowy komentarz
  3. Post – gdy dodany jest nowy post
    1. Post z pojedynczym zdjęciem
    2. Post z wieloma zdjęciami
    3. Post bez zdjęcia (status)
    4. Wydarzenie
    5. inne

Finalnie schemat wygląda jak poniżej (zawiera atrybuty dla wszystkich scenariuszy, jednak oznaczone jako niewymagane – brak atrybutu „required”):

{
    "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"
        }
    }
}

Flow krok po kroku

Po przetworzeniu żądania, Flow realizuje poniższe kroki:

  1. Pobierz typ operacji – odczytanie, czy jest to „Comment”, „Membership”, czy „Posts”
    Robię to używając akcji „Compose” i poniższego wyrażenia:

    body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['field']
  2. Akcja „Switch” w zależności od typu operacji.
  3. Dla każdego rodzaju typu najpierw odczytuję właściwe mu ID (np. comment_id dla „Comments” czy post_id dla „Posts”)
  4. Następnie używając akcji Query SharePoint sprawdzam, czy istnieje już rekord dla podanego ID. Jeśli tak, workflow się kończy.
  5. Jeśli scenariusz dotyczy rodzaju „Post”, wtedy Flow szuka wewnętrznego rodzaju operacji używając poniższego wyrażenia:
    body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['field']
  6. W zależności od wyniku, Flow używając akcji „Switch” wykonuje realizuje odpowiedni scenariusz:
    1. Photo
    2. Status
    3. Event
    4. i inne rodzaje (choć jak dotąd, nic innego od powyższych nie widziałem)
  7. Następnie, jeśli jest to typ „Photo” Flow sprawdza, czy w żądaniu zawarte jest tylko jedno zdjęcie, czy wiele (galeria). Sprawdza to, używając poniższego wyrażenia, badając czy typ to „album”:
    body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['value']['attachments']['data']?[0]?['type']
  8. Na koniec próbuje zmapować autora informacji z istniejącym użytkownikiem SharePoint. W tym celu używa akcji „Office 365 Get user profile (V2)”, podając jako UPN złączony ciąg tekstów: imię, nazwisko i domena organizacji:
    concat(trim(replace(trim(body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['value']['from']['name']), ' ', '.')), '@the-company.com')
  9. Oczywiście, nie zawsze użytkownik musi istnieć. W takim wypadku zwykle akcja zwraca błąd, jednak z punktu widzenia logiki, nie jest to błąd, który powinien zatrzymać przepływ. By temu zapobiec, skonfigurowałem ustawienia „run after” w akcji tworzącej rekord w SharePoint, by uruchamiała się także w sytuacji, gdy akcja odczytywania profilu użytkownika zakończy się błędem:
    Flow "Configure run after" settings
  10. Na koniec Flow tworzy nowy element na liście, łącząc wszystkie informacje w całość:
    Flow create new list item action

Struktura akcji Flow, na średnim poziomie szczegółowości wygląda dla opisywanego rozwiązania jak poniżej:

@workplace integration with Microsoft Flow

Widoczne obok strzałek ikonki (i) oznaczają relacje, w których kolejne akcje wykonywane są nawet w przypadku, gdy poprzednia zakończy się niepowodzeniem.

Budowa pojedynczego bloku, zapisującego dane do list SharePoint, wygląda jak na poniższym przykładzie (dla zapisu danych nowego posta):

Saving data from @workplace to SharePoint list

Struktura danych

Dla przechowywania komentarzy używam jednej listy w SharePoint zbudowanej z poniższych kolumn:

  1. Title – przechowuje informację o typie wiadomości.
  2. Author – kolumna tekstowa, do przechowywania imienia i nazwiska autora wiadomości.
  3. Date – data wystąpienia zdarzenia (pobrana z webhooka).
  4. Post/ Comment – wielowierszowe pole tekstowe, pozwalające na formatowanie HTML, służące do przechowywania treści wiadomości.
  5. Image – ponownie jest to wielowierszowe pole tekstowe, ponownie zezwalające na formatowanie HTML, służące do przechowywania tagu <img> z adresem URL obrazu, dołączonego do wiadomości. Jest to spowodowane faktem, że Flow nie wspiera aktualnie zapisu do pól „Picture/ Hyperlink” z ustawionym typem „Picture”.
  6. SourceURL – pole „Picture/ Hyperlink” z ustawionym typem „Hyperlink”.
  7. AuthorPPL – drugie pole dla przechowywania danych autora, tym razem jest to typ „Person or group”. Workflow próbuje zmapować dane tekstowe autora z istniejącym kontem użytkownika w SharePoint.
  8. ItemId – identyfikator wiadomości, w zależności od typu: comment_id, post_id lub member_id.

Lista zasilona danymi wygląda jak poniżej:

Data from @workplace in SharePoint list

Tematy do zapamiętania

Podczas tego projektu nauczyłem się poniższych rzeczy. Sugeruję o nich pamiętać, gdy będziesz podążać krokami mojego rozwiązania:

  1. @workplace będzie wysyłać żądania GET do FLOW za każdym razem, gdy jakakolwiek zmiana w „Custom integration” zostaje dokonana i zapisana.
  2. @workplace będzie wysyłać pojedyncze żądania POST tak długo, aż nie dostanie zwrotnie odpowiedzi „200 OK”. Należy pamiętać o tym, by weryfikować czy przetwarzana wiadomość już istnieje na liście i nie dodawać jej po raz kolejny.
  3. @workplace nie zawsze czeka na odpowiedź. W moim przepływie widzę dziesiątki nieudanych uruchomień tylko dlatego, że @workplace nie czeka na odpowiedź. Nie wiem dlaczego i zachowanie to jest całkowicie przypadkowe (czasem przepływy trwające dłużej kończą się poprawnie, a krótsze błędem). W każdym razie – gdy @workplace nie otrzyma odpowiedzi, będzie ponawiać wysyłanie wiadomości tak długo, aż dostanie „200 OK”, z tymże będzie robić to coraz rzadziej. Po pewnym czasie zaprzestanie wysyłania danej wiadomości. 
  4. Używanie „run after” w konfiguracji akcji w Flow jest naprawdę użyteczne – zarówno do zapobiegania błędom w wykonywaniu akcji w przypadku braku wymaganych danych, czy braku użytkownika – takich, które w logice przepływu nie powinny powodować jego zakończenia, czy np. w celu tworzenia warunków w logice. W moim Flow używałem ich naprawdę często.

 

Dzięki za dotarcie aż do tego zdania! Mam nadzieję, że ten opis Ci pomoże. W razie czego, nie krępuj się i pytaj lub pozostaw komentarz poniżej.

Cześć! Nazywam się Tomasz. Jestem wielkim fanem automatyzacji procesów i analizy biznesowej. Skupiam się na rozwijaniu moich umiejętności w pracy z produktami Nintex i Microsoft: w szczególności Office 365, SharePoint, Flow, PowerApps. Posiadam ponad 8 lat doświadczenia w pracy z SharePoint.