✨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^)ノ\(^_^ )