Telegram Bot: Broadcast – Użycie Extensions.Polling

W porzednim wpisie zmusiliśmy bota do pobierania wiadomości przy użyciu bardzo prymitywnego i chamskiego pollingu. Odpytywaliśmy nowe wiadomości z API co sekundę.

To bardzo toporne rozwiązanie. Nic nie potrafi poza odczytywaniem wiadomości i np. obsługę błędów musimy sobie sami napisać.

Telegram.Bot.Extensions.Polling

Z pomocą przyjdzie nam fajny dodatek Telegram.Bot.Extensions.Polling, który ładnie uporządkuje nam kod i doda kilka usprawnień.

Otwieramy nuget’a i instalujemy Polling

Robimy porządki

Przypomnijmy jak wygląda główna pętla programu

        public async Task Run()
        {
            await _bot.Init(); // inicjalizujemy bota
            await Task.Run(async () =>
            {
                while (true)
                {
                    await _bot.Process(); // tutaj się wszystko rozgrywa
                    await Task.Delay(1000); // czekamy sekundę
                }
            });
        }

Prawda, że paskudna? Zatąpimy ją teraz elementami z w/w liba. Nie będziemy już potrzebować pętli, wszystko zinicjujemy w _bot.Init()

        public async Task Run()
        {
            await _bot.Init();
        }

Od razu się można poczuć lepiej widząc taką ilośc kodu!

Zobaczmy na Init() co tam mu w duszy gra…

        public async Task Init()
        { 
            const int lastUpdate = -1;
            
            var messages = await GetMessages(-1); // pobieramy ostatnie wiadomości
            if (!messages.Any())
            {
                return;
            }

            _messageOffset = messages.Last().Id; // ost. id wiadomości to offset
        }

Pamiętacie? Na początku odczytywaliśmy wszystkie ostatnie wiadomości aby wyciągnąc na wstępie offset, którego użyliśmy do pobierania najnowszych wiadomości w metodzie Process(). Fuj.

Co powiecie na takiego zgrabnego inita?

        public async Task Init()
        {
            var receiverOptions = new ReceiverOptions
            {
                // chcemy wyłącznie wiadomości
                AllowedUpdates = new[] { UpdateType.Message },
                // pobieramy wszystkie wiadomości jakich nie przetworzyliśmy
                ThrowPendingUpdates = true
            };

            // włączamy odbieranie wiadomości
            await _client.ReceiveAsync(
                UpdateHandlerAsync,
                ErrorHandlerAsync,
                receiverOptions
            );
        }

        private async Task UpdateHandlerAsync(ITelegramBotClient client, Update update, CancellationToken token)
        {
            throw new NotImplementedException();
        }

        private async Task ErrorHandlerAsync(ITelegramBotClient client, Exception ex, CancellationToken token)
        {
            throw new NotImplementedException();
        }

Mamy tutaj 3 kroki:
1️⃣ włączamy nasłuch na nowe wiadomości
2️⃣ nowa wiadomość to trafi do metody UpdateHandlerAsync
3️⃣ wszelkie błędy ze strony API uruchomią metodę ErrorHandlerAsync

Poprzednio obsługę otrzymywania oraz procesowania nowych wiadomości załatwialiśmy w metodzie Process.

public async Task Process()
{
	// pobieramy nowe wiadomości
	var newUpdates = await GetMessages(_messageOffset);

        // latalmy po zebranych wiadomościach
	foreach (var update in newUpdates)
	{
		// kazdą wiadomość wysyłamy do naszych grup (CHAT_ID)
		foreach (var chatId in _config.Chats)
		{
		       // wysłanie wiadomości dalej
		}
	}



      // aktualizujemy offset
      if (messages.Any())
      {
           _messageOffset = messages.Last().Id;
      }
}

Teraz przetwarzanie musimy przenieść do metody UpdateHandlerAsync

  private async Task UpdateHandlerAsync(ITelegramBotClient client, Update update, CancellationToken token)
        {
             // kazdą wiadomość wysyłamy do naszych grup (CHAT_ID)
             foreach (var chatId in _config.Chats)
	     {
		// wysłanie wiadomości dalej
	     }
        }

Pozbyliśmy się pętli, nie musimy się martwić o _messageOffset . Pobieranie wiadomości zrzuciliśmy na coś co na pewno lepiej działa od poprzedniej metody.

⚠️ Ważne: nie musimy wymyślać wszystkiego na nowo (DRY!)

A co z obsługą błędów? Możemy ja bezproblemu zaimplementowac w metodzie ErrorHandlerAsync. Mamy tam zwykły `Extension` i możemy sobie z nim zrobić co tylko chcemy. Wrzucić do loga, powiadomić kogoś o błędzię czy też wysłać użytkownikowi wiadomość, że „something is no yes” i aby spróbował ponownie.

Podsumowanie

Dzisiaj zastąpiliśmy naszą prymitywną pętlę requestująca co sekundę API bota libem, który ładnie i sprawnie nam to opakował. Co za tym idzie uprościliśmy kod.

Polling nie jest oczywiście jedyną metodą pobierania wiadomości z API. Poza tym ma wady:
👎 opóźnienie
👎 wysyłanie cały czas requestów do serwera przez co zaśmiecamy łącze
👎 niepotrzebnie obciążamy server zbędnymi requestami

👍 Dla mnie plusem pollingu jest prostota. Bardzo łatwo go zrozumieć i wdrożyć.

Mamy jakaś alternatywe do pollingu? Jasne, że tak: webhooki. W tej metodzie już nie odpytujemy API o nowe wiadomości. To API nam wysyła na nasz URL wszelkie informacje. Ale to temat na osobny post…

Tymczasem zapraszam do dodania się do newslettera aby niczego nie przegapić!

👉 https://high-five.cc/newsletter

Telegram Bot: Broadcast – rozgłaszamy wiadomości na inne grupy

Dzisiaj zajmiemy się pisaniem prostego bota do rozgłaszania wiadomości na inne grupy, które nie mają ze sobą żadnego kontaktu. Punktem styku grup będzie bot. Każdą wiadomość weźmie i roześle na wszystkie grupy. Dzięki takiemu mechanizmowi ludzie z różnych grup mogą się porozumieć między sobą.

🧾 Założenia projektu

Bot ma rozesłać wiadomość do wszystkich zdefiniowanych kanałów (chat_id) wtedy gdy:

✔️ napiszemy coś bezpośrednio do bota
✔️ napiszemy wiadomość na jedną z naszych grup

🤴👸 Tworzymy grupy testowe

W telegramie klikamy w hamburgera

i wybieramy: New Group

Wpisujemy nazwę grupy i klikamy: NEXT

Następnie wpisujemy nazwę naszego bota w wyszukiwarkę (uwaga! boty są dostępne dla wszystkich publicznie)

klikamy w bota, po czym w button: CREATE

Tworzymy min. 2 grupy do testów.



CHAT_ID pobieramy indentycznie jak to opisałem w pierwszym poście. Jedyną różnicą jest to, że w każdej z grup musimy skierować wiadomość do bota:

/ – slash oznacza, że takie wiadomości bot zobaczy (są to komendy, ale o tym poźniej)

Po wysłaniu w/w wiadomości wchodzimy na znany nam adres:
👉 https://api.telegram.org/botTOKEN/getUpdates

I pobieramy stamtad CHAT_ID.

Na ten moment mamy niezbędne tokeny/idki:
✔️ TOKEN
✔️ CHAT_ID Grupy 1
✔️ CHAT_ID Grupy 2

Możemy przystąpić do programowania 👍

✍️ Piszemy (najprostszy) kod bota!

Musimy zaprogramować bota, aby był wstanie czytać wiadomości. Dzisiaj zajmiemy się metodą zwaną „pollingiem„. Co 1 sekundę, będziemy odpytywać wiadomości w pętli. Napiszmy najpierw szkielet samego kodu:

    class Bot
    {
        private readonly BotConfig _config;
        private readonly ITelegramBotClient _client;

        public static Bot Create(BotConfig config)
        {
            return new Bot(config);
        }

        private Bot(BotConfig config)
        {
            _config = config;
            _client = new TelegramBotClient(config.Token);
        }

        public async Task Init()
        {
        }
      
        public async Task Process() 
        {
        }
    }

Zaznaczam, że piszemy możliwie najprostszy kod, a nie najbardziej elegancki.

Bot będzie implementować dwie publiczne metody:
✔️ Init() – inicjalizacja bota
✔️ Process – przetwarzanie wiadomości, uruchamiane co sekundę

Bota zamkniemy w specjalnym kontekście aby nie wrzucać wszystkiego do Program.cs

    class RunningContext
    {
        private readonly Bot _bot;

        public RunningContext(Bot bot)
        {
            _bot = bot;
        }

        public async Task Run()
        {
            await _bot.Init();

            await Task.Run(async () =>
             {
                 while (true)
                 {
                     await _bot.Process();
                     await Task.Delay(1000);
 // czekamy sekundę aby znów odpytać telegrama
                 }
             });
        }
    }

Metodą z liba Telegram.Bot z jakiej skorzystamy będzie await _client.GetUpdatesAsync. Uruchomiona bez parametrów zwraca 100 ostatnich wiadomości. Problem w tym, że chcemy wyłącznie mieć nowe updaty. Rozwiazać to możemy przez przesunięcie/offset. Pierwszym parametrem dla w/w metody jest właśnie offset, określający ID ostatniej wiadomości, od której chcemy pobierać kolejne informacje.

Napiszemy metodę, która zwróci wyłącznie zwykłe, ostatnie wiadomości (allowedUpdates: UpdateType.Messages) na podstawie offsetu (messageOffset).

        private async Task<Update[]> GetMessages(int messageOffset)
        {
            var messages = await _client.GetUpdatesAsync(messageOffset, allowedUpdates: new []
            {
                UpdateType.Message,
            });

            return messages;
        }

Ograniczamy się wyłącznie do Message, ponieważ nie chcemy rozgłaszać innych treści.

Przejdźmy do Init(). Pobierzemy ID ostatniej wiadomości (lastUpdate). Będzie to nasz offset wykorzystywany w dalszej części.

        public async Task Init()
        {
            const int lastUpdate = -1; // ✨magiczna wartosc -1✨ 

            var messages = await GetMessages(lastUpdate);
            if (!messages.Any())
            {
                return;
            }

            _messageOffset = messages.Last().Id;
        }

Świetnie, w _messageOffset mamy offsert.

Możemy się zająć implementacją broadcastowania wiadomości, który będzie w metodzie Process.

Działanie metody Process:
✔️ pobranie najnowszych wiadomości na postawie offset
✔️ wysłanie do zdefiniowanych grup (CHAT_ID)
✔️ aktualizacja offset

        public async Task Process()
        {
            // pobieramy nowe wiadomości
            var newUpdates = await GetMessages(_messageOffset);

            foreach (var update in newUpdates)
            {
                // kazdą wiadomość wysyłamy do naszych grup (CHAT_ID)
                foreach (var chatId in _config.Chats)
                {
                    var message = update.Message;
                    var messageChatId = message.Chat.Id;
                    // robimy zwykłego forwarda do naszych grup
                    await _client.ForwardMessageAsync(chatId, messageChatId, message.MessageId);
                }
            }

            // aktualizujemy offset
            if (newUpdates.Any())
            {
                _messageOffset = newUpdates.Last().Id;
            }
        }

Teraz gdy napiszemy bezpośrednio wiadomość do bota

To każda zostanie rozgłoszona (forwardowana) do wszystkich grup. Mamy załatwione nasze pierwsze założenie:
✔️ bot ma rozsyłać wiadomości bezpośrednio skierowane do bota

Jeszcze chcelibyśmy móc rozgaszać wiadomości, które zostały bezpośrednio napisane na grupach. Domyślnie bot nie widzi co piszemy (to ze względu na domyślny tryb: private, niech tak zostanie). Natomiast gdy użyjemy komendy bot ją zobaczy.

Czym jest komenda? To wiadomość zaczynająca się od / (slash):
/komenda [dalsza część wiadomości]

Napiszemy teraz kawałek kodu rozgłaszającego wiadomości. Przyjmiemy, że każda zaczynająca się od: /bc wiadomość zostanie przesłana wszędzie

Wracamy do metody Process

        public async Task Process()
        {
            // pobieramy nowe wiadomości
            var newUpdates = await GetMessages(_messageOffset);

            foreach (var update in newUpdates)
            {
                // każda wiadomość wysyłamy do naszych grup (CHAT_ID)
                foreach (var chatId in _config.Chats)
                {
                    var message = update.Message;
                    var messageChatId = message.Chat.Id;

                    // nie rozgłaszamy na grupę skąd pochodzi wiadomość, nie ma sensu powielać jej tutaj
                    if (chatId == messageChatId)
                    {
                        continue;
                    }

                    // pobieramy treść
                    var text = message.Text;

                    // UWAGA! bardzo prymitywne sprawdzenie, można o wiele lepiej
                    // ale to nie jest cel tego posta. Przypominam, że ma byc prosto :)
                    if (!string.IsNullOrEmpty(text))
                    {
                        // jeśli zaczyna się od / znaczy ze jest komendą
                        if (text.StartsWith("/"))
                        {
                            // jeśli to "inna" komenda, to nic nie rób
                            if (!text.StartsWith("/bc "))
                            {
                                continue;
                            }
                        }
                    }

                    // robimy zwykłego forwarda
                    await _client.ForwardMessageAsync(chatId, messageChatId, message.MessageId);
                }
            }

            // aktualizujemy offset
            if (newUpdates.Any())
            {
                _messageOffset = newUpdates.Last().Id;
            }
        }

Oczywiście nie jest to kod do wykorzystania na produkcji, ponieważ brakuje mu zabezpieczeń, np. teraz każdy nieproszony gość może spamować grupy wrzucając wiadomości do bota.

✔️ Podsumowanie

Nie jest to jedyny możliwy realizacji broadcasta. Tutaj użyliśmy najprostszego sposoby. Moglibyśmy napisać to napisać zupełnie inaczej np.:
– kopiując 1:1 wiadomości (wtedy użytkownicy staną się anonimowi) i wrzucić wszędzie
– przechwytywać treść, zmienić ją i przesłać dalej

Możliwości botów telegramowych są na prawdę ogrome. Użylismy w tym poście zaledwie wycinka ich możliwości.

Napisaliśmy nasz własny polling, co prawda chamski, ale działa. W następnym poście przerobimy sposób odczytywania wiadomości. Użyjemy do tego fantastycznego liba Telegram.Bot.Extensions.Polling, którego stosowanie jest bardzo wygodne.

Jeśli chciałbyś zautomatyzować/usprawnić procesy w swojej firmie za pomocą botów (telegram, slack, inne) albo po prostu Twój biznes potrzebuje bota to skontaktuj się ze mną.

Tymczasem zapraszam do dodania się do newslettera aby niczego nie przegapić!

👉 https://high-five.cc/newsletter

Telegram Bot: Jak zacząć?

Cześć ✋!
Dzisiaj będziemy pisać własnego bota do telegrama. Jeśli nie wiesz co to jest telegram, to zapraszam na oficjalną stronę po więcej informacji.
Dla tych, którym nie chce się kliknąc w linka 😉 to telegram w skrócie to: lekki, prosty w użytkowaniu, bezpieczny komunikator internetowy.

🤔 Ok, jak zacząć „programować” telegrama?

Telegrama „programuje się” pisząc boty. Boty można traktować jako programy zainstalowane w środowisku telegrama. Zachęcam do odwiedzenia strony https://core.telegram.org/bots, gdzie są przestawione ich możliwości (również polecam przeczytanie sekcji BotFather).


Ok, z w/w źródeł powinniśmy mieć już jakieś pojęcie na temat botów: czym są, jak się z nimi komunikujemy, jakie mają możliwości (z grubsza przynajmniej). Możemy iść dalej…

Najpierw musimy utworzyć bota i zdobyć jego API Token, aby móc się z nim komunikować. Robimy to właśnie za pomocą BotFathera.

Uruchamiamy telegrama i w wyszukiwarce wpisujemy: BotFather


Po wybraniu BotFathera pojawia się zwykle okno do komunikacji.
Wrzucamy tam komendę: /newbot

BotFather ładnie nas poinformuje, że czeka na podanie nazwy naszego bota.

Możemy tutaj wpisać dowolną nazwę. Będzie to imię bota, które będzie widoczne podczas komunikacji.

Następnie BotFather zapyta się o jego nazwę (@username). Musi to być unikatowa nazwa w całym systemie telegrama oraz musi się kończyć na: bot

Ja wybieram H5BroadcastBot ponieważ w następnym poście pokażę jak napisać prostego bota rozgłaszającego wiadomości na różne kanały, które nie są ze sobą w żaden sposób połączone. Prywatnie potrzebuję takiej funkcjonalności, więc chętnie opiszę proces jej powstawania.

BotFather po utworzeniu bota odeśle nam wiadomość, gdzie znajduje się token HTTP API (zachowajcie go TYLKO dla siebie).


Świetnie! Wszystko gotowe, możemy zacząć programować.

✋ Hello World (w .NETcie)

Do komunikacji z botem, możemy użyć dowolnego języka/technologii. Wszystko odbywa się przez API telegrama. Z racji tego, że siedzę w .NETcie od „N”-lat, więc to dla mnie jest naturalny wybór.

Na początku utworzymy zwykły projekt .NETowy. Nie ważne czy to core, czy framework >= 4, kto co woli. Ja wybieram core 3.1.

Skorzystamy z najpopularniejszej biblioteki do telegrama: telegram.bot

Biblioteka jest bardzo prosta w użyciu. Na początku wyślemy standardowy: Hello World, bo jakże zacząć od czegoś innego?

static async Task Main(string[] args)
{
    ITelegramBotClient client = new TelegramBotClient(TOKEN);
    await client.SendTextMessageAsync(CHAT_ID, "Hello World!");
}

I to prawie wszystko… gdyby nie jedna, mała rzecz… TOKEN! wiemy czym jest. Dostaliśmy go od BotFathera. Natomiast czym jest CHAT_ID!?

💬 Pobieramy CHAT_ID

CHAT_ID to nic innego jak ID kanału, na jakim bot ma działać. Może to być grupa, kanał lub bezpośrednia rozmowa. „Hello worda” zrobimy jako zwykłą rozmowę. Dobrze, wróćmy do BotFathera


Klikamy w zaznaczony wyżej adres (u Was będzie oczywiście inny).

Wybieramy START:


i już możemy komunikować się z botem. Napiszmy mu zwykłe „hi


dzięki temu, że coś wysłaliśmy do bota jesteśmy w stanie przechwycić CHAT_ID. Wystarczy wejść na adres:

👉 https://api.telegram.org/botTOKEN/getUpdates

W odpowiedzi znajdziecie CHAT_ID:

Wrzucamy tę wartość do consta w kodzie i uruchamiamy aplikację

Działa 👌

W następnym poście rozbudujemy możliwości bota.

Dodaj się do newslettera aby niczego nie przegapić!
👉 https://high-five.cc/newsletter

✨Piszemy – Automatyczny klikacz Pajacyk.pl

Niedawno wrzuciłem na wykop.pl zapytanie odnośnie stworzenia automatycznego klikacza (raz dziennie) w brzuszek pajacyka na pajacyk.pl w formie rozszerzenia do chroma.

Wpis można zobaczyć tutaj: https://www.wykop.pl/wpis/62046557/pytanie-do-was-mirki-i-mirabelki-czy-bylibyscie-ch/

Powiedzmy że był pozytywny oddzew, więc chętnie zabieram się do pracy!

🤔 Założenia klikacza

  • Uruchamiany wyłącznie raz dziennie
  • Automatyczne klikanie w brzuszek
  • Nie może być inwazyjny dla użytkownika (nie może przeszkadzać)
  • Obsługa banalnie prosta: raz zainstalowane rozszerzenie ma działać, mamy zapomnieć o istnieniu extensiona.

✍️ Piszemy kod!

Cały projet będzie zawierać wyłącznie 2 pliki:

  • manifest.json w wersji 3
  • background.js, gdzie wrzucimy cały kod

manifest.json – czyli czego potrzebujemy

{
  "name": "Automatyczny klikacz Pajacyk.pl",
  "description": "Automatyczne klikanie raz dziennie w brzuszek pajacyka",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "host_permissions": ["http://*/",  "https://*/"],
  "permissions": ["scripting", "tabs", "storage"]
}

 „host_permissions”: [„http://*/”,  „https://*/”] – oznacza, że rozszerzenie będzie uruchamiane na KAŻDEJ stronie. Dlaczego tak? Nie możemy ustawić ograniczenia wyłącznie np. google czy facebooka, bo extension odpaliłby się dopiero po wejsciu na te strony. W tym przypadku potrzebujemy uruchamiać rozszerzenie zawsze i wszedzie.

„permissions”: [„scripting”, „tabs”, „storage”] – dlaczego aż tak dużo pozwoleń?

  • scripting – do klikania w pajacyka
  • tabs – do otwarcia taba z pajacyk.pl
  • storage – aby móc zapisać informację, że dzisiaj już kliknęliśmy

background.js czyli „🧠” całej operacji

Zaczynamy od prostego bloku, który będzie uruchamiać kod, kiedy wgra się cała strona, na której jestesmy

chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
  if (changeInfo.status == "complete") {
      // tutaj bedzie cały kod!
  }
});

Teraz musimy rozbić na czynniki pierwsze w jaki sposób ma się zachowywać rozszerzenie.

  • po otwarciu strony, chrome powinien sprawdzić czy dzisiaj już odwiedził pajacyka
    – jeśli odwiedził, to ma zakończyć działanie
  • jeśli nie odwiedził, to ma otworzyć pajacyk.pl w nowej zakładce
  • następnie ma kliknąć w brzuszek pajacyka
  • zamknąć zakładkę
  • zapisać informację o odwiedzinach
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
  if (changeInfo.status == "complete") {
      // otwieramy zakładkę z pajacykiem
    chrome.tabs.create({
      url: "https://www.pajacyk.pl/",
    });
  }
});

Otwieramy zakładkę i musimy wykonać eventa click na .pajacyk__clickbox

Trzeba jakoś wstrzyknąć kod javascript:

const $clickBox = document.querySelector(".pajacyk__clickbox");
$clickBox.click();

Funkcja chrome.tabs.create nie posiada możliwości wrzucenia js’a, więc trzeba skorzystać z  chrome.scripting.executeScript:

  chrome.tabs.create(
    {
      url: "https://www.pajacyk.pl/",
    },
    function (newTab) {

      // wstrzykujemy JS'a do nowego taba
      chrome.scripting.executeScript({
        target: {
          tabId: newTab.id,
          allFrames: false,
        },
        func: function () {
          const $clickBox = document.querySelector(".pajacyk__clickbox");
          $clickBox.click();
        },
      });
    }
  );

Powinno być wszystko OK, lecz! Nie działa jak powinno:

Trzeba podejść inaczej do wstrzykiwania kodu. Jeśli otwieramy nowego taba, to extension również się uruchamia. Musimy sprawdzić czy istnieje już otwarty pajacyk.pl:

  • jesli nie, to otwórz
  • jesli tak, to wstrzyknij kod

Użyjemy funkcji chrome.tabs.query, która zwróci wszystkie otwarte zakładki w chromie. Poszukamy takiej, która ma w url’u „pajacyk.pl”

  chrome.tabs.query({}, function (tabs) {
    const pajacykTab = tabs.find((t) => {
      return t.url.indexOf("pajacyk.pl") > -1;
    });

    const isPajacykTabExists = pajacykTab !== undefined;
  });

Pod isPajacykTabExists mamy porządaną przez nas informację

  chrome.tabs.query({}, function (tabs) {
    const pajacykTab = tabs.find((t) => {
      return t.url.indexOf("pajacyk.pl") > -1;
    });

    const isPajacykTabExists = pajacykTab !== undefined;

    if (!isPajacykTabExists) {
 // nie istnieje, więc otwieramy taba pajacyk
      chrome.tabs.create({
        url: "https://www.pajacyk.pl/",
      });
    } else { // istnieje tab pajacyk
      chrome.scripting.executeScript({
        target: {
          tabId: pajacykTab.id, // wstrzykujemy js'a do taba pajacyk
          allFrames: false,
        },
        func: function () {
          // nasz js klikający w brzuszek!
          const $clickBox = document.querySelector(".pajacyk__clickbox");
          $clickBox.click();
          window.close(); // zamykamy pajacyka zaraz po kliknięciu
        },
      });
    }
  });

Wszystko PRAWIE gra jak powinno. Kod js klika, lecz nie widzimy co się dzieje. Aby się upewnić czy wszystko działa dodamy proste opóźnienie setTimeout.

  func: function () {
            setTimeout(function () {
              const $clickBox = document.querySelector(".pajacyk__clickbox");
              $clickBox.click();

              setTimeout(function () {
                window.close();
              }, CLOSE_TAB_DELEY);
            }, CLICK_THE_BELLY_DELEY);
          },

Musimy jeszcze dodać zabezpieczenie aby klikanie się wykonywało tylko raz dziennie.

  if (changeInfo.status == "complete") {
    chrome.storage.local.get(STORAGE_KEY, function (data) {
      const today = getToday();
      let lastExecutionDate = data.lastExecutionDate || DEFAULT_DATE;

      const hasAlreadyBeenClickedToday = lastExecutionDate == today;
      if (hasAlreadyBeenClickedToday) {
        return;
      }

      clickTheBelly(today);
    });
  }

Co tutaj się dzieje?

  • odwołujemy się do storage local, gdzie trzymamy date ostatniego kliknięcia
  • pobieramy dzisiejszą datę w formacie: yyyy-mm-dd
  • sprawdzamy czy istnieje data zapisana w storage local, jeśli nie, to ustawiamy DEFAULT_DATE
  • sprawdzamy czy zapisana data równa się z dzisiejszą
  • jeśli tak, to znaczy że już kliknęliśmy w brzuszek
  • jeśli nie, to wykonujemy funkcję clickTheBelly

Jeszcze trzeba dopisać kod odpowiadający za zapis daty. Musimy ten kod wykonać zaraz PO zakończeniu skryptu klikającego

 chrome.scripting.executeScript({}, callback: function() {});

Kod zapisujący wrzucimy do callbacka executeScript

     chrome.scripting.executeScript(
        {
          target: {
            // usunięte dla czytelności
          },
          func: function () {
            // usunięte dla czytelności
          },
        },
        function () { // to jest callback
          chrome.storage.local.set(
            {
              lastExecutionDate: today,
            },
            function () {}
          );
        }
      );

Teraz wszystko działa!

Zainteresowanych zapraszam na githuba, gdzie znajduje się cały kod rozszerzenia na githubie

A pobrać rozszerzenie można tutaj z Chrome Web Store

✨Feature: Floating Video

Chętnie zabrałem się za ten feature z dwóch powodów:

  • jest bardzo przydatny, sam używam floating video na youtubie
  • nigdy czegoś takiego nie pisałem, więc chętnie się pobawię

Na samym początku trzeba rozgryźć jak zhakować wykop aby uzyskać ten efekt.

🐱‍💻 Hakowanie

Zobaczmy jak wygląda kod html gdzie player jest umieszczony.

Hmm widać, że to nie jest zaembedowany youtube, lecz preview złożony z obrazka oraz linka. Po kliknieciu w preview, leci wywołanie ajaxem a obrazek zastępowany jest przez iframe youtuba

Elementy mamy namierzone. Teraz musimy poszukać czegoś co się stanie floatem. .videoWrapper wygląda tutaj bardzo sensownie. F12 i dodajemy w konsoli style

Efekt mnie zaskoczył. Wszystko zninęło. Nawet po ustawieniu Height: z 0 na 1000px nic się nie pojawia

Szukamy dalej hmm

.embed-youtube jest zbyt wysoko, ponieważ już łapie treść posta (Test), a chcemy zrobić floata wyłącznie na playerze. Uderzamy w iframe

Yeah! Mały sukces. Iframe drgnął i osadził się w stałym miejscu (position: fixed)

Jeszcze zmienimy aby player był na pierwszym planie przez dodanie z-index

Świetnie, pierwszy krok zrobiony. Trzeba pomyśleć jak sam feature ma działać.

🐱‍👤 Działanie

  • Jako użytkownik klikam w playera
  • Filmik sie uruchamia
  • Przewijam stronę w dół
  • Player znika z ekranu
  • Player przenosi się w prawy dolny róg ekranu

🐱‍🏍 Piszemy kod!

Co na ten moment mamy? Potrafimy ustawić odpowiedni element (iframe) jako position: fixed. Najważniejszą teraz rzeczą jest wykrycie czy element zniknął nam z ekranu (viewport). Szybka grzebanina po necie, wiadomo SDD (StackOverflow Driven Development)

  function isElementInViewport($element) {
    var rect = $element.getBoundingClientRect();
    return (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <=
        (window.innerHeight || document.documentElement.clientHeight) &&
      rect.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
  }

Wykrywanie czy element jest widoczny działa. Trzeba to przyczepić do window.scrolla

   window.addEventListener("scroll", function () {
        getAllVideoPlayers().forEach(function ($videoPlayer) {
          if (isElementInViewport($videoPlayer)) {
            console.log("is visible")
          }
          else {
            console.log("is NOT visible")
          }
        });
      });

Szybki test… wygląda OK. Brniemy w to dalej!

            function makeFloatVideo($player) {
              $player.setAttribute(
                "style",
                "position: fixed; top: 75%; left: 81%; width: 320px; height: 190px; z-index: 100000"
              );
            }

Niby działa ale coś dziwnie. Przy każdym scrollu cały proces się uruchamia na nowo, iframe mruga, dzieją się rzeczy niestworzone.

Hmm nie ma sensu aby nawet najdelikatniejszy scroll odpalał całą procedurę od nowa. Przecież jak element już jest niewidoczny, to po co znów ustawiać style?

Idziemy w najprostsze rozwiązanie, Robimy flagę, która będzie czuwać nad uruchomieniem makeFloatVideo. Jeśli dojdzie do zmiany stanu (widoczny => niewidoczny) wtedy dopiero uruchomimy makeFloatVideo

 function onVisibilityChange($element, invisibleCallback) {
    var prevVisibility = $element.getAttribute(DATA_VISIBLE_ATTR);

    if (prevVisibility === null) {
      prevVisibility = false; // domyślna wartosc
    } else {
      prevVisibility = Boolean(prevVisibility); // musimy zrobic parsing dla boola, żeby nie operować na stringu: true/false
    }

    var currentVisibity = isElementInViewport($element);

    if (currentVisibity != prevVisibility) { // jeśli poprzednia wartość flagi z obecną się nie zgadzają, znaczy że zaszła zmiana
      $element.setAttribute(DATA_VISIBLE_ATTR, currentVisibity);

      if (!currentVisibity) {
        invisibleCallback();
      }
    }
  }

Działa i to jeszcze jak! 😉 Reszta rzeczy to już kosmetyka i drobnica, o której nie będę pisać, bo nie ma sensu. Nic ciekawego się w dalszej części prac nie dzieje.

👉 Dla zainteresowanych odsyłam do githuba.

🏍️ Światła! Kamera! Akcja!

👉 Klikamy w VIDEO

👉 SCROLL w doł

🔥Ale piękny floating video wyszedł🔥
Później trochę odsunę od krawędzi, bo jest zbyt blisko. Generalnie jestem zadowolony z (d)efektu.

Zapraszam do pobrania rozszerzenia Chrome Web Store i uczestnictwa w projekcie!

to be continued ヘ( ^o^)ノ\(^_^ )

✨Feature: Button wstaw link w edytorze ma wyświetlić prompt na wprowadzenie linka

Zrobienie tego feature’a nie było aż takie proste jak mi się wydawało. Problem w tym, że button w edytorze ma swoje własne zdarzenie onclick.

Fajnie byłoby event wywalić i napisać swój totalnie od zera.

Niestety nie da się tego tak prosto zrobić.
Dlaczego? Przecież jest metoda:

target.removeEventListener(type, listener);

Tak, jest. Ale by pozbyć się eventa musimy w parametrze podać event listender. Nie znamy go. Nie mamy jak go pobrać ani odwołać się do niego.

Jest jeszcze sposób w jQuery: unbind () czy też off(), lecz tutaj mamy kolejny problem. Kod js’a uruchomiony czy też wstrzyknięty rozszerzeniem działa w zupełnie innym kontekscie. Nie mamy możliwości odwołania do jQuery podpiętego na wykopie. Tak na prawdę, to nie możemy się odwołać do żadnych obiektów, funkcji jsowych na stronie, bo to nie nasz kontekst. Jedynie możemy się komunikować via DOM.

Wstrzyknięcie żywego kodu jsa też nie zadziała. Manifest v3 nie pozwala na to:

var script = document.createElement('script');
script.textContent = 'alert("test")';
(document.head||document.documentElement).appendChild(script);
script.remove();

Gdybyśmy użyli manifest v2, to dałoby radę, lecz do końca 2022 chrome przestanie wspierać v2 i rozszerzenie przestałoby działać.

Jedynie co można zrobić w tym przypadku, to:

  • w pliku manifest.json podać w web_accessible_resources z jakich resourceów, będziemy korzystać. Tutaj resourcem będzie plik z jsem: remove-event.js. Jeśli nie podamy pliku w web_accessible_resources dostaniemy komunikat:
    Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.
  • teraz można podpiąć plik do strony
var s = document.createElement('script');
s.src = chrome.runtime.getURL('/common/remove-event.js');
s.onload = function() {
  this.remove();
};
(document.head || document.documentElement).appendChild(s);

Wszystko fajnie. Skrypt się podpina i wykonuje.

Tylko czy na prawdę warto coś takiego robić aby pozbyć się eventa? Na pewno nie.
Można pomyśleć nad loadm przeniesieniem obecnych funkcjonalności do podpinania jak wyżej, czyli przez script.

Zamiast ładowania pliku js przez `executeScript`

Dodalibyśmy go przez script

Jak dla mnie rozwiązanie spoko, lecz zrobimy obejście aby trzymać się obecnego mechanizmu w ramach ćwiczeń. Zobaczę ile będzie męki z innymi featureami. Niech sobie obecny event na buttonie żyje jak żył. Dodamy nowego eventa, który się uruchomi zaraz PO obecnym

Kluczem jest (g-ł-u-p-i-e) rozwiązanie z setTimeout(function (), 1);

Głupie a działa zadziwiająco dobrze!

  • po kliknieciu w button pozwalamy odpalić pierwony event:
  • natychmiast pojawia się prompt z miejscem na linka
  • klikamy OK i link wskakuje

Pięknie. Niezbyt to eleganckie rozwiązanie ale działa poprawnie. Trzeba wziąć poprawkę na to, że trochę hackujemy wykop i mniej eleganckie rozwiązania będą się lepiej sprawdzać.

Ostatnia rzecz jaka wymagała zmiany to sposób uruchomienia featurea. Do tej pory miałem założenie: jeden feature na URL. Czyli jak jesteśmy na stronie:
https://www.wykop.pl/wiadomosc-prywatna/konwersacja/…
to na podstawie prostego RegEx’a dopinamy feature:

Ha! Jakże szybko okazało się to błędnym założeniem. Obecnie mamy dwie funkcjonalności, które powinny zostać uruchomione na tej samej stronie. Szybka zmiana:

Dodanie foreach’a:

Działa!

to be continued ヘ( ^o^)ノ\(^_^ )

✨Feature: „Większe pole do wiadomości”

Zgodnie z założeniem, zrobiłem najprostszy feature, aby móc jak najszybciej wrzucić rozszerzenie do Chrome Web Store. Czyli na pierwszy ogień wziąłem: Wieksze pole do wiadomości:

Bez rozszerzenia
Z rozszerzeniem

Postanowiłem inaczej rozplanować strukturę projektu niż w poprzednim extensionie JustJoin.IT, gdzie miałem jeden plik core.js zawierający wszystko co potrzebne.

Tutaj każdy feature będzie jako osoby plik dołączany dynamicznie na podstawie urla.

W pliku background.js mamy prostą funkcję pobierającą nazwę feature’a

function getFeatureName(currentUrl) {
  if (/wykop\.pl\/wiadomosc-prywatna/i.test(currentUrl)) {
    return "bigger-field-messages";
  }
  return "";
}

Natępnie wczytujemy wybrany feature:

    const featureName = getFeatureName(tab.url); // pobieramy nazwe
    if (featureName == "") {
      return;
    }
    
    chrome.scripting
      .executeScript({
        target: {
          tabId: tabId,
          allFrames: false,
        },
        files: [`./features/${featureName}.js`], // dodajemy plik zawierający logikę feature'a
      })
      .catch((err) => console.log(err));
  }

Myślę, że struktura projektu, tutaj jest najciekawsza.

Co do samego feature’a, to nic się tam specjalnego nie dzieje. Szukamy odpowiednich elementów DOM i wyliczamy nową wysokość wiadomości (.pmStreamView), nuda.

Wrzuciłem do sklepu rozszerzenie i czekam aż zostanie opublikowane. Trwa to gdzieś z parę dni.
Dam znać jak już się pojawi do instalacji.

to be continued ヘ( ^o^)ノ\(^_^ )

🧰🔨 Zaczynamy projekt: #naprawmywykop 🔨🧰

O co chodzi w tym projekcie #naprawmywykop?

Tworzymy razem ze społecznością wykop.pl oraz 4programmers rozszerzenie do przegladarki chrome. Rozszerzenie będzie dodawać lub modyfikować istniejące funkcjonalności serwisu.

Chcesz się przyłączyć do zabawy? Masz jakiś pomysł? Napisz:

Jest już kilka propozycji zebrane z https://www.wykop.pl/wpis/61580485/hej-wam-ruszam-z-akcja-naprawmywykop-celem-akcji-j/

  • Większe pole do wiadomości 👈
  • W znaleziskach, jesli jest film youtube/streamable czy inne treści co sa embed, żeby ten div był float i na górże, żeby mógł jednocześnie oglądać filmik i czytać komentarze ( ͡° ͜ʖ ͡°) 
  • Możliwość rozszerzenia filmu, teraz ma stała wartość co jest bardzo głupie, coś jak tryb kinowy na YT
  • Szyfrowanie aby można było omijac punkty regulaminu? Dla użytkowników bez dodatku wyświetlałby się ciąg znaków, dla osób z dodatkiem wyświetlałaby się normalna wiadomość.
  •  Przy pomocy wykop API naprawić przycisk dodawania do ulubionych aby faktycznie dodawał.
  • Możliwość pobierania filmów ze streamable, ewentualnie aby po kliknięciu na przycisk otwierał w nowej karcie link streamable.com/xyz123 na streamabledl.com/xyz123, ewentualnie od razu pobierał.
  • Zwiększenie #czarnolisto
  • Blokowanie po tytule jak ktoś nie dał tagów

Zacznę zupełnie inaczej jak to było w przypadku mojego pierwszego rozszerzenia do JustJoin.IT.

  • Najpierw zrobię najprostszą funkcjonalność
  • Wrzuce od razu do sklepu
  • Stopniowo będę rozwijać rozszerzenie
  • Od razu będzie można zainstalować extensiona i zwracać uwagi/zmiany

Na pierwszy ogien leci: Większe pole do wiadomości 👈

To będzie pierwsza funkcjonalność, a potem polecimy z czymś zabawniejszym 🧰

to be continued ヘ( ^o^)ノ\(^_^ )