Ładowanie

Mastodon API – lista obserwujących i obserwowanych

🇬🇧 Go to english version of this post / Przejdź do angielskiej wersji tego wpisu

W tym wpisie będzie nieco bardziej technicznie. Uzyskanie listy obserwowanych i/lub obserwujących dla danego konta na Mastodonie nie jest takie oczywiste. Można to zrobić używając oficjalnego API i właśnie to jak to zrobić pokażę w tym wpisie. Co ciekawe poniżej opisane zagadnienie może być użyte jako narzędzie OSINT’owe. Podkreślamy to, gdyż za sprawą ‪@avolha@infosec.exchange‬ link do mojego wpisu o Dockerze trafił do zestawienia Weekendowa Lektura: odcinek 512 [2023-03-25], które jest kierowane głównie do bezpieczników (specjalistów zajmujących się cyberbezpieczeństwem), a temat związany z OSINT’em powinien pasować do profilu zainteresowań osób pracujących w tej dziedzinie.

Zajrzyjmy do dokumentacji

Jak każdy szanujący się geekonerd wejdziemy najpierw do dokumentacji dotyczące API Mastodona i zobaczymy co ciekawego można tam wyczytać na temat, który aktualnie nas interesuje. Rozdział o pobieraniu listy obserwujących znajduje się pod tym linkiem. Natomiast ten o obserwowanych znajduje się tuż pod nim. Z ciekawych informacji jakie wyczytałem to:

  • od wersji 4.0.0 przy zapytaniach o obserwowanych/obserwujacych API nie wymaga uwierzytelnienia się tokenem API, to super, bo wystarczy zwykłe zapytanie HTTP i krok tworzenia aplikacji możemy całkowicie pominąć,
  • do zapytania potrzebujemy ID konta, o którego dane pytamy, a więc musimy wykonać dodatkowy krok i na podstawie adresu profilu lub handle użytkownika musimy ustalić w/w ID (wykonamy to przy pomocy innej funkcji API),
  • domyślna liczba rezultatów jakie możemy uzyskać to 40, jednak można ją zwiększyć do 80 poprzez ustawienie parametru limit=80, niestety taki limit to spora niedogodność, bo jeżeli konto posiada więcej niż 80 obserwujących/obserwowanych to, aby uzyskać pełną listę będziemy musieli wykonać więcej niż jedno zapytanie do API i w dodatku przeprowadzić odpowiednią paginację zapytań (coś jak podział na strony),
  • do skorzystania z paginacji wykorzystuje się parametry max_id, since_id i min_id, co istotne fraza id zawarta w nazwach tych parametrów wcale nie odnosi się do ID konta, o którym wcześniej mówiłem, a do wartości znanej jedynie dla backendu i bazy danych Mastodona, więc korzystanie z nich jest niejako brodzeniem po omacku, jednakże jest pewien sposób na uproszczenie tego procesu, o którym napiszę za chwilę,
  • jako odpowiedź od serwera otrzymamy listę kont obserwowanych/obserwujących, które dodatkowo będzie zawierała dość obszerne informacje na temat tych kont, pełna ich lista znajduje się pod tym linkiem, jednak z najciekawszych są to:
    • ID konta (np. 110012691117775438)
    • acct (np. to3k@tomaszdunia.pl)
    • display_name (np. 🙃 ɐıunp zsɐɯoʇ)
    • note (bio profilu)
    • url (np. https://mastodon.tomaszdunia.pl/@to3k)
    • avatar (link do awatara a.k.a. profilówki)
    • followers_count (liczba obserwujących to konto)
    • following_count (liczba obserwowanych przez to konto)
    • statuses_count (liczba tootów a.k.a. postów)

Podstawowe zapytanie

Weźmy mój profil jako przykład roboczy. Link do niego to – https://mastodon.tomaszdunia.pl/@to3k. Zapytanie do API, którego rezultatem będzie uzyskanie listy obserwujących, ma wyglądać następująco:

https://[adres_instancji]/api/v1/accounts/[id_użytkownika]/followers

Adres instancji to w moim przypadku będzie mastodon.tomaszdunia.pl. Natomiast skąd mam znać swój ID użytkownika? Użyjemy do tego innej funkcji API, która pozwala wyszukiwać użytkowników (i co najważniejsze podstawowe ich dane, w tym ID) po nazwie:

https://[adres_instancji]/api/v1/accounts/lookup?acct=[nazwa_użytkownika]

Skonstruujmy zatem stosowny URL – https://mastodon.tomaszdunia.pl/api/v1/accounts/lookup?acct=to3k. Po uruchomieniu go w przeglądarce otrzymamy od serwera odpowiedź w postaci obiektu JSON (wspominałem o tym formacie w tych wpisach). W przeglądarce Firefox, która jest moim podstawowym narzędziem deweloperskim, wygląda to tak:

Szukane ID wskazałem czerwoną strzałką na powyższym zrzucie ekranu. Bierzemy to ID i tworzymy link będący zapytaniem o listę obserwowanych – https://mastodon.tomaszdunia.pl/api/v1/accounts/110012691117775438/followers. W ten sposób otrzymaliśmy obiekt JSON będący tablicą z informacjami o 40 kontach, które obserwują mnie na Mastodonie. Zmodyfikujmy ten link dodając do niego na końcu parametr limit, aby otrzymać dwa razy więcej wyników (wartość maksymalna jaką możemy uzyskać to 80) – https://mastodon.tomaszdunia.pl/api/v1/accounts/110012691117775438/followers?limit=80. Co w przypadku, gdy ktoś ma więcej niż 80 obserwujących i chce uzyskać całą listę? Do tego potrzebujemy wykorzystać paginację, ale o niej w dalszej części wpisu.

Aha, jeszcze lista obserwowanych. Sprawa wygląda bardzo analogicznie z tym, że w linku frazę followers należy zamienić na followinghttps://mastodon.tomaszdunia.pl/api/v1/accounts/110012691117775438/following?limit=80.

O co chodzi z paginacją?

Pisząc to zastanawiam się czy takie słowo w ogóle istnieje w języku polskim… Może powinienem to przetłumaczyć jak stronicowanie? W każdym razie to sformułowanie pochodzi od angielskiego pagination i w tym kontekście dotyczy tego, że posiadając limit rezultatów (80), jakie otrzymamy jednosrazowo od serwera, musimy wiedzieć jak sformułować następne zapytanie do API, aby otrzymać inny (niezdublowany) wynik i tym samym rozszerzyć naszą listę aż do momenty, gdy pobierzemy wszystkie jej elementy (kompletna lista obserwujących/obserwowanych). To tak jakby przeglądać tabelę podzieloną na strony składające się z 80 elementów i przełączać się pomiędzy nimi. Jak już wspomniałem wcześniej do ogarnięcia tematu paginacji służą nam parametry max_id, since_id i min_id. Z pozoru parametry te odnoszą się do ID użytkowników, jednak w rzeczywistości tak nie jest. To konkretne ID to odniesienie do wewnętrznej bazy danych serwera, której zawartość jest znana jedynie dla backend’u. A zatem w jaki sposób mamy korzystać z tych parametrów? Zacznijmy od początku.

Załóżmy, że mam 800 obserwujących. Korzystając z linka – https://mastodon.tomaszdunia.pl/api/v1/accounts/110012691117775438/followers?limit=80, który skonstruowaliśmy wcześniej, otrzymujemy w odpowiedzi od serwera listę 80 kont, które obserwują mnie na Mastodonie. Są to konta posortowane czasowo, zaczynając od najświeższego obserwatora (osoby, która zaczęła mnie obserwować jako ostatnia). Tak, więc 1/10 listy moich obserwowanych już mamy. Jak w takim razie przejść do następnej strony i poznać obserwatorów od 81 do 160? Musimy ustalić jaki będzie URL następnej strony, a informację o tym dostajemy w nagłówku (header) odpowiedzi od API. Jest to konkretnie zawarte w parametrze nazywającym się link. W Firefox wystarczy zmienić zakładkę z JSON na Nagłówki i otrzymamy coś podobnego do tego:

Pozyskajmy wartość tego parametru nagłówkowego:

<https://mastodon.tomaszdunia.pl/api/v1/accounts/110012691117775438/following?limit=80&max_id=700>; rel=”next„, <https://mastodon.tomaszdunia.pl/api/v1/accounts/110012691117775438/following?limit=80&since_id=1183>; rel=”prev

URL znajdujący się przed rel=”next” zaznaczony na zielono to link do następnej strony z obserwującymi, którego szukaliśmy. Po skorzystaniu z niego otrzymujemy kolejną partię 80 kont, które są moimi obserwującymi.

W ten sposób powtarzamy proces jeszcze 8 razy, aby uzyskać informacje o wszystkich 800 obserwujących. Cała misterna paginacja właśnie stała się oczywista, prawda? 😎

Skrypt PHP

Ręcznie można zrobić to raz, aby zrozumieć cały mechanizm. Dalej potrzebujemy skryptu, który będzie to automatyzował, bo nie jesteśmy, do cholery, dzikusami 😂 Poniżej kod skryptu PHP, którego kolejne linijki wyjaśniam (jak zawsze) poprzez komentarze zawarte w treści.

<?php
    // Pobiera zmienną GET
    $url = trim(addslashes(strip_tags($_GET['url'])));
?>

<!-- Formularz służący do pobrania od użytkownika adresu profilu użytkownika -->
<form action="" method="GET" name="form">
    <input type="text" name="url" placeholder="Profile URL..." value="<?php echo $url; ?>" size="100"><br><br>
    <button type="submit">Get Followers/Following</button>
</form>

<?php
    if(empty($url))
    {
        // Jeżeli nie podano adresu to zakańcza działanie skryptu
        exit;
    }
    else
    {
        // Jeżeli zmienna z adresem nie jest pusta to...
        // Rozbija adres na domenę (instancji) i nazwę użytkownika
        $explode_url = explode("@", $url);
        $mastodon_domain = $explode_url[0];
        $mastodon_username = $explode_url[1];
        // Wzór regexp do walidacji formatu nazwy użytkownika
        $check = '/^[a-zA-Z0-9_]+/';

        if(filter_var($mastodon_domain, FILTER_VALIDATE_URL) AND preg_match($check, $mastodon_username))
        {
            // Jeżeli domena i nazwa użytkownika zostały zwalidowane jako prawidłowe
            $profile_url = $url;
        }
        else
        {
            // Jeżeli domena lub nazwa użytkownika nie przeszły walidacji to wyświetla błąd i zakańcza działanie skryptu
            echo "Forbidden value of GET variable";
            exit;
        }
    }


    // USTALA ID UŻYTKOWNIKA
    // Konstruuje adres do komunikacji z API
    $api_url = $mastodon_domain."/api/v1/accounts/lookup?acct=".$mastodon_username;
    // Konstruuje zapytanie cURL
    $curl = curl_init($api_url);
    curl_setopt($curl, CURLOPT_URL, $api_url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36');
    curl_setopt($curl, CURLOPT_TIMEOUT, 30);
    curl_setopt($curl, CURLOPT_HEADER, 0);
    // Wysyła zapytanie cURL i zapisuje wynik do zmiennej
    $json = curl_exec($curl);
    // Konwertuje wynik z formatu JSON na zwykłą tablicę
    $api_result = json_decode($json, true);
    // Wyciąga z wyniku ID użytkownika i zapisuje do zmiennej
    $mastodon_id = $api_result['id'];

    if(empty($mastodon_id))
    {
        // Jeżeli zmienna z ID użytkownika jest pusta to wyświetla błąd i zakańcza działanie skryptu
        echo "Error while getting account ID, failed to connect to API";
        exit;
    }


    // FUNKCJA DO WYCIĄGNIĘCIA INFORMACJI Z NAGŁÓWKA ODPOWIEDZI SERWERA API
    function HeaderLink($curl, $header_line) {
        if(str_contains($header_line, "link:"))
        {
            $GLOBALS['link'] = $header_line;
        }
        return strlen($header_line);
    }


    // POBIERA LISTĘ OBSERWUJĄCYCH
    // Licznik znalezionych obserwujących
    $followers_counter = 0;
    // Tablica do przechowywania danych znalezionych obserwujących
    $followers = array();
    // Tablica do przechowywania jedynie ID znalezionych obserwujących (potrzebne do uniknięcia duplikatów)
    $followers_ids = array();
    
    // Konstruuje adres do komunikacji z API
    $api_url = $mastodon_domain."/api/v1/accounts/".$mastodon_id."/followers?limit=80";
    // Konstruuje zapytanie cURL
    $curl = curl_init($api_url);
    curl_setopt($curl, CURLOPT_URL, $api_url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36');
    curl_setopt($curl, CURLOPT_TIMEOUT, 30);
    curl_setopt($curl, CURLOPT_HEADER, 0);
    // Odwołuje się do funkcji wyciągającej informację z nagłówka odpowiedzi serwera API
    curl_setopt($curl, CURLOPT_HEADERFUNCTION, "HeaderLink");
    // Wysyła zapytanie cURL i zapisuje wynik do zmiennej
    $json = curl_exec($curl);
    // Konwertuje wynik z formatu JSON na zwykłą tablicę
    $api_result = json_decode($json, true);
    
    // Przechodzi przez wszystkie elementy tablicy i wykonuje dla każdego następujące czynności...
    foreach($api_result as $follow)
    {
        // Sprawdza czy taki element nie był już przetwarzany (przeciwdziałanie duplikacji)
        if(!in_array($follow['id'], $followers_ids))
        {
            // Dodaje ID elementu do tablicy z ID
            $followers_ids[] = $follow['id'];
            // Dodaje nowy element do tablicy ze znalezionymi obserwującymi
            $followers[] = array(
                "id" => $follow['id'], 
                "acct" => $follow['acct'],  
                "display_name" => $follow['display_name'],  
                "url" => $follow['url'],  
                "avatar" => $follow['avatar'],  
                "followers_count" => $follow['followers_count'],  
                "following_count" => $follow['following_count'],  
                "statuses_count" => $follow['statuses_count']
            );
            // Inkrementuje licznik znalezionych obserwujących
            $followers_counter++;
        }
    }
    // Ustala adres następnej strony z obserwującymi
    preg_match("(link: <(.+?)>; rel=\"next\", <.+?>; rel=\"prev\")is", $GLOBALS['link'], $temp);
    $api_url = $temp[1];

    // Pętla, która wykonuje to samo co powyżej, dopóki jest w stanie ustalić adres następnej strony z obserwującymi
    while(!empty($api_url))
    {
        $curl = curl_init($api_url);
        curl_setopt($curl, CURLOPT_URL, $api_url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36');
        curl_setopt($curl, CURLOPT_TIMEOUT, 30);
        curl_setopt($curl, CURLOPT_HEADER, 0);
        curl_setopt($curl, CURLOPT_HEADERFUNCTION, "HeaderLink");
        $json = curl_exec($curl);
        $api_result = json_decode($json, true);

        foreach($api_result as $follow)
        {
            if(!in_array($follower['id'], $followers_ids))
            {
                $followers_ids[] = $follow['id'];
                $followers[] = array(
                    "id" => $follow['id'], 
                    "acct" => $follow['acct'],  
                    "display_name" => $follow['display_name'],  
                    "url" => $follow['url'],  
                    "avatar" => $follow['avatar'],  
                    "followers_count" => $follow['followers_count'],  
                    "following_count" => $follow['following_count'],  
                    "statuses_count" => $follow['statuses_count']
                );
                $followers_counter++;
            }
        }
        preg_match("(link: <(.+?)>; rel=\"next\", <.+?>; rel=\"prev\")is", $GLOBALS['link'], $temp);
        $api_url = $temp[1];
    }


    // POBIERA LISTĘ OBSERWOWANYCH
    // Licznik znalezionych obserwowanych
    $following_counter = 0;
    // Tablica do przechowywania danych znalezionych obserwowanych
    $following = array();
    // Tablica do przechowywania jedynie ID znalezionych obserwowanych (potrzebne do uniknięcia duplikatów)
    $following_ids = array();
    
    // Konstruuje adres do komunikacji z API
    $api_url = $mastodon_domain."/api/v1/accounts/".$mastodon_id."/following?limit=80";
    // Konstruuje zapytanie cURL
    $curl = curl_init($api_url);
    curl_setopt($curl, CURLOPT_URL, $api_url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36');
    curl_setopt($curl, CURLOPT_TIMEOUT, 30);
    curl_setopt($curl, CURLOPT_HEADER, 0);
    // Odwołuje się do funkcji wyciągającej informację z nagłówka odpowiedzi serwera API
    curl_setopt($curl, CURLOPT_HEADERFUNCTION, "HeaderLink");
    // Wysyła zapytanie cURL i zapisuje wynik do zmiennej
    $json = curl_exec($curl);
    // Konwertuje wynik z formatu JSON na zwykłą tablicę
    $api_result = json_decode($json, true);
    
    // Przechodzi przez wszystkie elementy tablicy i wykonuje dla każdego następujące czynności...
    foreach($api_result as $follow)
    {
        // Sprawdza czy taki element nie był już przetwarzany (przeciwdziałanie duplikacji)
        if(!in_array($follow['id'], $following_ids))
        {
            // Dodaje ID elementu do tablicy z ID
            $following_ids[] = $follow['id'];
            // Dodaje nowy element do tablicy ze znalezionymi obserwowanymi
            $following[] = array(
                "id" => $follow['id'], 
                "acct" => $follow['acct'],  
                "display_name" => $follow['display_name'],  
                "url" => $follow['url'],  
                "avatar" => $follow['avatar'],  
                "followers_count" => $follow['followers_count'],  
                "following_count" => $follow['following_count'],  
                "statuses_count" => $follow['statuses_count']
            );
            // Inkrementuje licznik znalezionych obserwowanych
            $following_counter++;
        }
    }
    // Ustala adres następnej strony z obserwowanymi
    preg_match("(link: <(.+?)>; rel=\"next\", <.+?>; rel=\"prev\")is", $GLOBALS['link'], $temp);
    $api_url = $temp[1];

    // Pętla, która wykonuje to samo co powyżej, dopóki jest w stanie ustalić adres następnej strony z obserwowanymi
    while(!empty($api_url))
    {
        $curl = curl_init($api_url);
        curl_setopt($curl, CURLOPT_URL, $api_url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36');
        curl_setopt($curl, CURLOPT_TIMEOUT, 30);
        curl_setopt($curl, CURLOPT_HEADER, 0);
        curl_setopt($curl, CURLOPT_HEADERFUNCTION, "HeaderLink");
        $json = curl_exec($curl);
        $api_result = json_decode($json, true);

        foreach($api_result as $follow)
        {
            if(!in_array($follow['id'], $following_ids))
            {
                $following_ids[] = $follow['id'];
                $following[] = array(
                    "id" => $follow['id'], 
                    "acct" => $follow['acct'],  
                    "display_name" => $follow['display_name'],  
                    "url" => $follow['url'],  
                    "avatar" => $follow['avatar'],  
                    "followers_count" => $follow['followers_count'],  
                    "following_count" => $follow['following_count'],  
                    "statuses_count" => $follow['statuses_count']
                );
                $following_counter++;
            }
        }
        preg_match("(link: <(.+?)>; rel=\"next\", <.+?>; rel=\"prev\")is", $GLOBALS['link'], $temp);
        $api_url = $temp[1];
    }
?>
<!-- WYŚWIETLENIE WYNIKÓW -->
<h1>Followers</h1>
<b>Number of followers found:</b> <?php echo $followers_counter; ?><br><br>
<table>
    <tr>
        <th>Lp.</th>
        <th>Avatar</th>
        <th>ID</th>
        <th>Handle</th>
        <th>Name</th>
        <th>Followers</th>
        <th>Following</th>
        <th>Toots</th>
        <th>URL</th>
    </tr>
<?php
    $i = 1;
    foreach($followers as $follow)
    {
        echo "<tr>";
        echo "<td>".$i."</td>";
        echo "<td><img src=\"".$follow['avatar']."\" style=\"max-width: 50px; max-height: 50px;\" /></td>";
        echo "<td>".$follow['id']."</td>";
        echo "<td>".$follow['acct']."</td>";
        echo "<td>".$follow['display_name']."</td>";
        echo "<td>".$follow['followers_count']."</td>";
        echo "<td>".$follow['following_count']."</td>";
        echo "<td>".$follow['statuses_count']."</td>";
        echo "<td><a href=\"".$follow['url']."\">".$follow['url']."</a></td>";
        echo "</tr>";

        $i++;
    }
?>
</table>

<h1>Following</h1>
<b>Number of following found:</b> <?php echo $following_counter; ?><br><br>
<table>
    <tr>
        <th>Lp.</th>
        <th>Avatar</th>
        <th>ID</th>
        <th>Handle</th>
        <th>Name</th>
        <th>Followers</th>
        <th>Following</th>
        <th>Toots</th>
        <th>URL</th>
    </tr>
<?php
    $i = 1;
    foreach($following as $follow)
    {
        echo "<tr>";
        echo "<td>".$i."</td>";
        echo "<td><img src=\"".$follow['avatar']."\" style=\"max-width: 50px; max-height: 50px;\" /></td>";
        echo "<td>".$follow['id']."</td>";
        echo "<td>".$follow['acct']."</td>";
        echo "<td>".$follow['display_name']."</td>";
        echo "<td>".$follow['followers_count']."</td>";
        echo "<td>".$follow['following_count']."</td>";
        echo "<td>".$follow['statuses_count']."</td>";
        echo "<td><a href=\"".$follow['url']."\">".$follow['url']."</a></td>";
        echo "</tr>";

        $i++;
    }
?>
</table>

Wynik działania skryptu:

Skrypt jest również dostępny na moim GitHub’ie.


Jeżeli podobał Ci się ten wpis to możesz mnie wesprzeć! 🙂

Tomasz Dunia

🇵🇱 Z wykształcenia Mechatronik. Z zawodu Główny Konstruktor w PAK-PCE Polski Autobus Wodorowy (Neso Bus). Po pracy Ojciec Roku. W nocy Wannabe Programista. Wszystko to daje przepis na zwykłego nerda :) 🇬🇧 Mechatronics by education. By profession Chief Constructor in PAK-PCE Polish Hydrogen Bus (Neso Bus). After work Father of the Year. At night Wannabe Programmer. All this gives a recipe for an ordinary nerd :)

svg

Co myślisz?

Pokaż komentarze / Napisz komentarz

4 komentarze

Skomentuj Tomasz Dunia Anuluj pisanie odpowiedzi

svg
Szybka nawigacja
  • 01

    Mastodon API – lista obserwujących i obserwowanych