MEWS Bot = Mastodon nEWS

blog.tomaszdunia.pl 1 rok temu

W poprzednich wpisach pisałem o Mastodonie, więc pomyślałem, iż pociągnę ten temat nieco dalej i przedstawię jeden z moich małych projektów. Mastodon z każdym dniem zyskuje coraz większą popularność, jednak nie pozostało na tyle dużym i uznanym medium, żeby zwróciły na niego uwagę większe koncerny medialne, które najbardziej udzielają się tam, gdy jest największa publiczność i to ilościowo, a nie koniecznie zawsze jakościowo. W takich sytuacjach trzeba poradzić sobie samemu, co też zrobiłem. W taki sposób powstał pomysł MEWS, czyli Mastodon nEWS.

Od czego zacząć?

Skoro portale informacyjne nie publikują na Mastodonie i chyba na razie nie mają planu tego robić to trzeba zrobić bota, który będzie to robił za nie!

Taka myśl wpadła mi któregoś dnia do głowy. Tak się składa, iż API Mastodona jest dość proste do obsługi przez cURL, a skoro jest proste do obsługi przez cURL to równie proste będzie napisanie skryptu w PHP, który będzie scrapował (pobierał dane z) RSS danego portalu informacyjnego, przetwarzał dane i publikował je w formie toota na Mastodonie.

No dobrze, ale od jakiego portalu chciałbym zacząć? Najlepiej od takiego, którego najbardziej brakuje mi na Mastodonie! Najbardziej lubianym przeze mnie polskojęzycznym źródłem informacji jest Rzeczpospolita, za dostęp do której płacę niewielką miesięczną opłatę, bo znajduje się za paywallem. Na marginesie taki układ jest dla mnie w pełni zrozumiały, bo porządne dziennikarstwo nie powinno być darmowe.

Budujemy bota RSS -> Mastodon

Kompletny kod bota, który jest bohaterem tego wpisu jest dostępny na moim GitHubie pod tym linkiem. Piszę o tym, bo nie będę wrzucał tutaj całego kodu linijka po linijce, a jedynie opiszę najistotniejsze jego fragmenty. Na wstępie chciałbym też zaznaczyć, iż nie jestem z zawodu programistą, a jedynie samoukiem-hobbistą, więc mój kod może nie być perfekcyjny, czy też zgodny z jakimikolwiek przyjętymi standardami w świecie dev. Może też nie być maksymalnie zoptymalizowany, ale liczy się to, iż działa tak jak powinien.

Zaczynamy od utworzenia dwóch plików:

  • rzeczpospolita.txt – będą w nim przechowywane linki artykułów, które już przerzuciliśmy z RSS do Mastodona, żeby nie duplikować tootów,
  • rzeczpospolita.php – skrypt główny bota.

Chciałem, aby pisany przeze mnie bot był w miarę uniwersalny i można go było niewielkim nakładem pracy przerobić tak, aby działał dla innego portalu oraz był łatwy do użycia przez inne osoby, więc na początek skryptu wyciągnąłem sobie pewne zmienne (a może raczej stałe ), których w kodzie użyję dopiero później. Tak, więc na wstępie potrzebujemy określić trzy rzeczy. Pierwszą z nich jest token, czyli nasz prywatny klucz dostępowy do API Mastodona. Pozyskuje się go poprzez wejście do Ustawień konta, na którym będziemy publikować automatyczne tooty wygenerowane przez bota, następnie zakładka Tworzenie aplikacji i przycisk Nowa aplikacja. Podajemy dowolną nazwę aplikacji, ja podałem „MEWS bot” i w sekcji Zakres odznaczamy wszystko poza „write”, co oznacza, iż aplikacja, którą właśnie tworzymy będzie miała pełne uprawnienia do publikowania na tym koncie. Następną rzeczą, którą musimy określić do poprawnego działania skryptu to adres instancji, na której zarejestrowaliśmy konto bota. Trzecia zmienna to limit znaków obowiązujący na tejże instancji (rate limit). Domyślnie jest to 500, ale istnieją instancje, które dopuszczają więcej (np. na naszej rodzimej instancji 101010.pl jest to 2048 znaków).

$token = "[WKLEJ TUTAJ TOKEN]"; $instance_url = "[WKLEJ TUTAJ URL INSTANCJI]"; $instance_rate_limit = 500;

Dalej tworzymy tablicę z linkami do kanałów RSS portalu, którego artykuły chcemy publikować przez bota. Może to być jeden lub kilka linków oddzielonych przecinkami. Rzeczpospolita ma jeden główny feed RSS, więc dla niego ta instrukcja będzie wyglądała tak.

$urls = array( "https://www.rp.pl/rss_main" );

Ale o ile chcielibyśmy odfiltrować treści tematycznie to możemy ograniczyć się do podrzędnych tematycznych feedów RSS, których będzie więcej, i zrobić to tak.

$urls = array( "https://moto.rp.pl/rss/2651-motoryzacja", "https://cyfrowa.rp.pl/rss/2991-cyfrowa", "https://energia.rp.pl/rss/4351-energetyka" );

Wczytujemy zawartość pliku rzeczpospolita.txt, żeby później odfiltrować te artykuły z kanału RSS, które już wcześniej udostępniliśmy.

$file = file_get_contents("rzeczpospolita.txt");

Przy użyciu pętli foreach przechodzimy przez wszystkie linki do kanałów RSS podane w tablicy $urls.

foreach($urls as $url) {...}

Przy użyciu funkcji simplexml_load_file() konwertujemy zawarty kanału RSS do formy tablicy wielopoziomowej o nazwie $feeds.

$feeds = simplexml_load_file($url);

Znowu używamy pętli foreach, ale tym razem dzielimy feed RSS na poszczególne artykuły (item’y).

foreach ($feeds->channel->item as $item)

Przyjrzyjmy się teraz jak wygląda składnia takiego przykładowego item’u w kanale RSS:

<item> <guid isPermaLink="true">https://cyfrowa.rp.pl/technologie/art37858421-chinski-robot-jak-terminator-zmienia-ksztalt-i-przelewa-sie-przez-kraty</guid> <mainProfile><![CDATA[Technologie]]></mainProfile> <title><![CDATA[Chiński robot jak Terminator. Zmienia kształt i przelewa się przez kraty]]></title> <link><![CDATA[https://cyfrowa.rp.pl/technologie/art37858421-chinski-robot-jak-terminator-zmienia-ksztalt-i-przelewa-sie-przez-kraty]]></link> <description><![CDATA[Zespołowi badaczy z Chin udało się opracować rozwiązanie niczym z filmów science fiction. Stworzyli zmiennokształtnego robota, umieścili go w zminiaturyzowanym modelu więzienia i pokazali, jak potrafi wydostać się on zza krat.]]></description> <category>Technologie</category> <pubDate>Sat, 28 Jan 2023 11:56:00 +0100</pubDate> <enclosure length="0" type="image/jpeg" url="https://i.gremicdn.pl/image/free/497cf5a2a1609a24bd425fe122641ed9/?t=resize:fill:600:300,enlarge:1"/> <author>Michał Duszczyk</author> <redirectUrl/> <pay_status>Preview</pay_status> </item>

Specyfika plików XML jest taka, iż informacje zawarte są pomiędzy odpowiednimi znacznikami, których nazwy określają to co przechowują. Ustalmy jak chcielibyśmy, aby wyglądał nasz toot, a więc co jest nam potrzebne do jego skonstruowania. Moja wizja była taka:

TYTUŁ
SEPARATOR (5 MYŚLNIKÓW)
HASHTAGI TEMATYCZNE
SEPARATOR (5 MYŚLNIKÓW)
KRÓTKI OPIS (JEŻELI TRZEBA TO SKRÓCONY ZGODNIE Z LIMITEM ZNAKÓW INSTANCJI)
SEPARATOR (ZNAK NOWEJ LINII)
LINK

Skoro już wiemy co jest nam potrzebne to zacznijmy wyciągać te informacje z pliku XML. Zaczniemy od linku. Wydaje się, iż zaczynamy od końca, ale to specjalne działanie ze względu na to, iż nie ma potrzeby pobierać reszty o ile okaże się, iż dany link znajduje się już w pliku rzeczpospolita.txt, a to oznaczałoby, iż został już przez nas przetworzony wcześniej, a artykuł, do którego odsyła był już wrzucany przez bota na Mastodona. Link znajduje się pomiędzy znacznikami <link>…</link>, więc z racji tego, iż użyliśmy wcześniej funkcji simplexml_load_file(), to możemy się do niego dobrać używając jedynie prostej notacji $item->link. Pozostaje nam jeszcze przeformatować pobrane dane na typ string przy użyciu funkcji strval() oraz usunąć niepotrzebne elementy z ciągu.

$link = strval($item->link); // Pobieramy link z XML i formatujemy na ciąg $link = str_replace("<![CDATA[", "", $link); // Usuwamy "<![CDATA[" z początku ciągu $link = str_replace("]]>", "", $link); // Usuwamy "]]>" z końca ciągu

W ten sposób zapisaliśmy w zmiennej o nazwie $link ciąg znaków, który jest linkiem do artykułu. Teraz trzeba jeszcze musimy sprawdzić czy występuje on w pliku rzeczpospolita.txt. Użyjemy do tego funkcji str_contains(), która zwraca wartość true (prawda), gdy w ciągu $file zawiera się ciąg $link, a false (fałsz), gdy się w nim nie zawiera.

if(str_contains($file, $link)) { continue; // o ile występuje to pomiń ten item i kontynuuj wykonywanie pętli } else { ... // o ile nie występuje to wykonaj dalszą część kodu, o której w dalszej części wpisu }

Gdy wiemy już, iż nie publikowaliśmy wcześniej toota o danym artykule to przechodzimy do pozyskania pozostałych rzeczy z feedu RSS. Tytuł i opis artykułu pobieramy analogicznie do tego jak robiliśmy to z linkiem, usuwając przy tym zbędne śmieci.

$title = strval($item->title); $title = str_replace("<![CDATA[", "", $title); $title = str_replace("]]>", "", $title); $description = strval(strip_tags($item->description)); $description = str_replace("<![CDATA[", "", $description); $description = str_replace("]]>", "", $description);

Pozostają nam jeszcze tematyczne hashtagi, które będą odpowiednikami kategorii do jakich został zakwalifikowany danych artykuł. Dla hashtagów sytuacja jest nieco inne niż dla wcześniej pobranych danych, bo o ile artykuły Rzeczpospolitej przypisywane są przeważnie jedynie do jednej kategorii, tak dla innych portali często artykuł należy do więcej niż jednej i jest więcej niż jeden parametr <category>…</category> do pobrania. Tworząc bota MEWS stwierdziłem, iż hashtagi są dość istotną częścią, bo będą umożliwiały obserwującym łatwe odfiltrowanie tematów, które ich interesuje lub wręcz przeciwnie – nie interesują ich. Do tego muszą być unikatowe, dlatego na ich końcu dopisuję MEWS, wtedy użytkownik ma pewność, iż blokując dany hashtag blokuje tylko tooty pochodzące od bota MEWS.

Przygotowanie ciągu hastagów rozpoczynamy od utworzenia tablicy $hashtag. Następnie ponownie korzystamy z pętli foreach i w ten sposób zbieramy wszystkie wartości znajdujące się pod parametrem category danego artykułu. Zebrane dane obrabiam odpowiednio. Dodaję każdej kategorii prefix # i suffix MEWS. Na koniec wrzucam wszystkie hashtagi w utworzoną wcześniej tablicę, dodając przy tym na końcu jeszcze jeden hashtag – #MEWS – nie będący kategorią, a jedynie będący wspólnym hashtagiem dla wszystkich tootów bota, i łączę wszystkie elementy tej tablicy w jeden ciąg, separując je odstępem (spacja).

$hashtag = array(); foreach($item->category as $category) { $category = ucwords(strtolower(strval($category))); $category = str_replace(" ", "", $category); $category = "#".$category."MEWS"; $hashtag[] = $category; } $hashtag[] = "#MEWS"; $hashtags = implode(" ", $hashtag);

W ten sposób pod zmienną $hashtags przechowuję ciąg znaków ze wszystkimi hashtagami, które załączę za chwilę do toota.

Teraz, gdy już znamy długość wszystkich części składowych musimy wyliczyć czy zmieści nam się to wszystko do jednego toota. Natomiast o ile okaże się, iż wiadomość jest w takiej formie dłuższa niż przyjęty na początku limit to będziemy musieli skrócić opis zapisany w zmiennej $description tak, aby zmieścić się w limicie. Zacznijmy od wyliczenia limitu dla opisu ze wzoru:

Limit dla opisu = Dopuszczalna liczba znaków dla jednego toota – Długość tytułu – Dwa separatory po 5 znaków – Sześć znaczników nowej linii – Długość ciągu z hashtagami – Długość linku – Trzy kropki jako zakończenie skróconego opisu – 10 znaków rezerwowych na wszelki wypadek.

$description_limit = $instance_rate_limit - strlen($title) - 10 - 6 - strlen($hashtags) - strlen($link) - 3 - 10;

Teraz pozostaje już tylko sprawdzić czy długość opisu jest większa od limitu i o ile tak to skrócić go do długości wyliczonego limitu oraz dodać trzy kropki na końcu. Użyjemy do tego dwóch funkcji: strlen() wyliczającej długość ciągu oraz substr() wycinającej z większego ciągu mniejszy o konkretnej długości zaczynając od znaku 0 (pierwszego).

if(strlen($description) > $description_limit) { $description = substr($description,0,$description_limit); $description .= "..."; }

OK, możemy przystąpić do komponowania treści toota.

$status_message = $title."\r\n"; $status_message .= "-----"."\r\n"; $status_message .= $hashtags."\r\n"; $status_message .= "-----"."\r\n"; $status_message .= $description."\r\n\r\n"; $status_message .= $link;

Wiadomość gotowa, więc pora przejść do ustawiania parametrów zapytania cURL, tj. komunikacji z API Mastodona. Zaczniemy od zdefiniowania danych, które wyślemy w zapytaniu, czyli:

  • status – treść toota, którą przygotowaliśmy,
  • language – język,
  • visibility – widoczność toota, dostępne opcje to public, unlisted, private i direct, ja wybrałem unlisted, bo jednocześnie nie chcę spamować ludziom na lokalnej i globalnej osi czasu, ale też chcę, aby wszystkie opublikowane tooty były widoczne na profilu bota.
$status_data = array( "status" => $status_message, "language" => "pl", "visibility" => "unlisted" );

Teraz nagłówek, który dla funkcji API, której zamierzamy użyć, tj. publikacja statusu, nie musi być obszerny, bo wystarczy, iż będzie składał się tylko z instrukcji niezbędnej do autoryzacji, tj. zawierającej nasz token zdefiniowany na początku.

$headers = [ "Authorization: Bearer ".$token ];

Wszystko gotowe, więc budujemy i wykonujemy zapytanie cURL. Najpierw inicjalizacja zapytania. Potem określamy URL, do którego będziemy kierować zapytanie. W przypadku chęci skorzystania z funkcji API „publikuj status (toot)” jest to – [URL INSTANCJI]/api/v1/statuses. Następnie nakazujemy, aby cURL użył standardowej metody POST po HTTP oraz została zwrócona do nas informacja o rezultacie jaki udało się osiągnąć poprzez zapytanie (powodzenie lub np. porażka i kod błędu). Na koniec dołączamy wcześniej zdefiniowane nagłówek oraz treść zasadniczą. Ostatnie dwie linijki to uruchomienie zapytania cURL, zapisanie wyniku w zmiennej $output_status, niewymagane ale przydatne w celach diagnostycznych, oraz rozłączenie połączenia.

$ch_status = curl_init(); curl_setopt($ch_status, CURLOPT_URL, $instance_url."/api/v1/statuses"); curl_setopt($ch_status, CURLOPT_POST, 1); curl_setopt($ch_status, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch_status, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch_status, CURLOPT_POSTFIELDS, $status_data); $output_status = json_decode(curl_exec($ch_status)); curl_close ($ch_status);

Na koniec pętli dodajemy jeszcze link, którego przetwarzanie zakończyliśmy, do listy przeprocedowanych linków.

$file .= $link."\n";

Ostatnią linijką przed zakończeniem skryptu pozostało aktualizacja pliku rzeczpospolita.txt o zawartość zmiennej $file, w której przechowywaliśmy linki do wcześniej opublikowanych artykułów oraz tych, które zostały opublikowane podczas tego konkretnego uruchomienia skryptu.

Bot gotowy!

Teraz pozostaje już tylko umieścić kod bota na jakimś hostingu lub serwerze (z np. nginx lub apache). Dobrze by było także ustawić zadanie crona, które będzie wywoływało uruchomienie skryptu co jakiś określony interwał czasowy (np. co 30 minut). Większość hostingów ma taką funkcję, nazywać się ona będzie właśnie zadania crona, zadania cykliczne lub coś podobnego.

Wyszedł z tego wpisu niezły blok tekstu, ale mam nadzieję, iż wszystko zostało przeze mnie opisane w sposób klarowny. Dla różnych portali skrypt bota będzie wymagał drobnych modyfikacji, co wynika z tego, iż kanały RSS, a raczej ich formatowanie, są czasem trochę inne. Nie jest to jednak przeszkoda nie do pokonania. Wystarczy jedynie przejrzeć treść pliku XML danego feedu RSS i wprowadzić korekty.

Nie przedłużając wrzucę jeszcze tylko poniżej linki do botów, które sam uruchomiłem. Koy źródłowe wszystkich trzech poniższych botów są dostępne na moim GitHubie, więc można je sobie podejrzeć. Opublikowane są one na licencji MIT, czyli w zasadzie możecie zrobić z nimi co tylko chcecie. Mam tylko jedną prośbę – o ile użyjecie mojego kodu i stworzycie swojego bota tego typu to dajcie znać, chętnie zobaczę jak Wam to wyszło, a i może będę zainteresowany obserwowaniem go

Idź do oryginalnego materiału