艁adowanie

MEWS Bot = Mastodon nEWS

馃嚞馃嚙 Go to english version of this post / Przejd藕 do angielskiej wersji tego wpisu

W poprzednich wpisach pisa艂em o Mastodonie, wi臋c pomy艣la艂em, 偶e 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 jest jeszcze 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, 偶e 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膰, 偶e 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, 偶e 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, 偶e 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 je偶eli 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, 偶e 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臋, 偶e zaczynamy od ko艅ca, ale to specjalne dzia艂anie ze wzgl臋du na to, 偶e nie ma potrzeby pobiera膰 reszty je偶eli oka偶e si臋, 偶e dany link znajduje si臋 ju偶 w pliku rzeczpospolita.txt, a to oznacza艂oby, 偶e 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, 偶e 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; // Je偶eli wyst臋puje to pomi艅 ten item i kontynuuj wykonywanie p臋tli
}
else
{
    ... // Je偶eli nie wyst臋puje to wykonaj dalsz膮 cz臋艣膰 kodu, o kt贸rej w dalszej cz臋艣ci wpisu
}

Gdy wiemy ju偶, 偶e 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, 偶e 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艣膰, 偶e 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 – – 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 je偶eli oka偶e si臋, 偶e 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 je偶eli 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, 偶e 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 jest jeszcze 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臋, 偶e 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, 偶e 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臋 – je偶eli 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 馃槈


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

Odpowiedz

svg
Szybka nawigacja
  • 01

    MEWS Bot = Mastodon nEWS