Computer Keyboard - Głównie JavaScript

Głównie JavaScript

rss feed icon

by

System komentarzy HashOver jako alternatywa dla Disqus

Napis 'Comments' po anielsku
Autor Tookapi źródło pexels.com licencja własna

W zeszły miesiącu usunąłem komentarze Disqus, zastępując je aplikacją HashOver (w wersji next, czyli to co kiedyś będzie HashOver 2.0), która jest dostępna na licencji GNU Affero GPL i napisana w PHP, dlatego można jej użyć, gdy masz swój blog na współdzielonym hostingu tak jak ja. Nawet gdy jest to blog plików statycznych, tak jak Głównie JavaScript.

Nie musicie się przejmować wersją licencji Affero, która wymusza udostępnienie kodu źródłowego, nawet jeśli mamy aplikacje gdzieś na serwerze i jej nie kopiujemy, ponieważ zawiera wbudowaną przeglądarkę kodu PHP.

Zmiana systemu komentarzy była spowodowana tym, że w konsoli dostawałem 404 z jakiejś dziwnej domeny i były jakieś dziwne ciasteczka. Disqus dodaje też reklamy, na szczęście tylko jeśli ma się odpowiednio duży ruch na stronie, którego jeszcze nie przekroczyłem. W tym wpisie przedstawie jak dodać HashOver do strony, który jest dość prosty, ale najważniejszą częścią będzie, jak zaimportować komentarze z Disqus.

Instalacja

Instalacja HashOver Next jest dość prosta. Musimy wgrać pliki na serwer i zmodyfikować plik hashover/backend/classes/secret.php. Miejsce pliku może się zmienić, we wcześniejszej wersji był w katalogu hashover/scripts. Plik powinien się raczej znajdować w katalogu backend, ponieważ nie jest to klasa.

Do pliku musimy wpisać hasło, email i klucz szyfrujący.

Jako klucz można użyć np. hasz MD5 jakiegoś słowa. Po wgraniu na serwer, wystarczy na każdej stronie wpisu swojego bloga dodać:

<script type="text/javascript"
        src="/hashover-next/hashover/comments.php"></script>
<noscript>You must have JavaScript enabled to use the comments.</noscript>

I to w zasadzie tyle, tylko jeśli wcześniej mieliśmy komentarze Disqus, to je stracimy. Wiec trzeba by je jakoś zaimportować.

Import komentarzy z Disqus

DL;DR cały kod jest na GitHubie.

Pierwszą rzeczą jaką sprawdziłem był eksport danych z ich panelu, niestety nie ma w nich informacji i awatarach, więc każdy musiałby mieć jakieś randomowe. Ja chciałem mieć ikonki użytkowników, więc postanowiłem napisać skrypt, który zassie ich dane z API. Skrypt napisałem w PHP, ponieważ pomyślałem, że może się przydać do projektu (który jest napisany w PHP). Mogli by dodać opcję importu do aplikacji.

Oficjalna biblioteka PHP

Disqus na GitHubie ma swoją oficjalną bibliotekę do PHP. Problem jest taki że nie obsługuje stronicowania i chyba już nie będzie, ponieważ nikt się tą biblioteką nie zajmuje.

Ale dzięki temu, że jest Open Source można kod poprawić, a wystarczy zmienić linijkę:

return $data->response;

na

return $data;

Dzięki temu będziemy mieli dostęp do kursora, po danych. Bazując na dokumentacji API napisałem taki kod:

require('disqus-php/disqusapi/disqusapi.php');
$disqus = new DisqusAPI('<API KEY>');

function fetch($options, $fn, $cursor = NULL) {
    if ($cursor != NULL) {
        $payload = array_merge($options, array('cursor' => $cursor));
    } else {
        $payload = $options;
    }
    $res = $fn($payload);
    $posts = $res->response;
    if ($res->cursor->hasNext) {
        $posts = array_merge($posts, fetch($options, $fn, $res->cursor->next));
    }
    return $posts;
}

$opts = array('forum' => 'gjavascript');
// use limit: 100 max option if you have lots of comments

$posts = fetch($opts, function($payload) use ($disqus) {
    return $disqus->posts->list($payload);
});
$treads = fetch($opts, function($payload) use ($disqus) {
    return $disqus->threads->list($payload);
});

żeby nie przekroczyć limitu przy pisaniu kodu, pobrałem najpierw wszystkie wątki i wpisy i zapisałem je w pliku JSON.

function save($fname, $obj) {
    $f = fopen($fname, 'w');
    fwrite($f, json_encode($obj, JSON_PRETTY_PRINT));
    fclose($f);
}
save('posts.json', $posts);
save('threads.json', $treads);

Aby ten kod zadziałał, będziesz musiał wygenerować klucz API, czyli stworzyć aplikacje Disqus. Możesz spróbować bez klucza ponieważ według konsoli do testowania API nie jest wymagany. Ale ja tego nie testowałem.

Generowanie komentarzy

Komentarze w HashOver znajdują się w katalogu hashover/comments/threads. Każdy wątek, czyli wpis na blogu, ma swój katalog, w którym znajdują się pliki xml z nazwami np. 1.xml czy 1-1.xml, oznaczający komentarz pierwszy i odpowiedź do komentarza pierwszego.

W każdym katalogu znajdował się też katalog z meta danymi tj. plikiem JSON, w którym była tablica z posortowanymi wpisami, ale HashOver dział bez tego katalogu i pliku więc go zignorowałem.

Kod który napisałem do wygenerowania komentarzy, wygląda tak:

function addChild($xml, $node, $name, $value) {
    $element = $xml->createElement($name);
    $element->appendChild($xml->createTextNode($value));
    $node->appendChild($element);
    return $element;
}

foreach ($threads as $thread) {
    $dir_name = getSafeThreadName(preg_replace("%^https?://[^/]+%", "", $thread['link']));
    $dir = 'threads/' . $dir_name;
    // można by zoptymalizować ten kod i sprawdać czy są jakieś komentarze zanim utworzymy katalog
    if (!is_dir($dir)) {
        mkdir($dir);
        chmod($dir, 0775);
    }
    // wszystkie wywołania echo są tylko do debugowania
    echo str_repeat('-', 80) . "\n";
    echo ":: " . $thread['clean_title'] . "\n";
    echo str_repeat('-', 80) . "\n";
    foreach ($posts as $post) {
        $post['children'] = array_filter($posts, function($p) use ($post) {
            return $p['parent'] = $post['id'];
        });
    }
    $root = array_filter($posts, function($post) {
        return $post['parent'] == NULL;
    });
    $i = 1;
    $thread_posts = array_filter($posts, function($post) use ($thread) {
        return $post['thread'] == $thread['id'];
    });
    $refs = array(
        'date' => 'createdAt',
        'name' => 'author.username',
        'avatar' => 'author.avatar.permalink',
        'website' => 'author.url'
    );
    foreach($thread_posts as $post) {
        $xml = new DomDocument('1.0');
        $xml->preserveWhiteSpace = false;
        $xml->formatOutput = true;

        $root = $xml->createElement('comment');
        $root = $xml->appendChild($root);
        $body = preg_replace_callback("%<(a[^>]+)>(.*?)</a>%", function($match) {
            return (preg_match("/data-dsq-mention/", $match[1]) ? "@" : "") . $match[2];
        }, $post['message']);
        addChild($xml, $root, 'body', $body);
        foreach ($refs as $key => $value) {
            $parts = explode('.', $value);
            $ref = $post;
            foreach ($parts as $part) {
                $ref = $ref[$part];
            }
            addChild($xml, $root, $key, $ref);
        }
        $name_arr = post_name($thread_posts, $post, $i);
        $fname = $dir . '/' . implode('-', $name_arr) . ".xml";
        $f = fopen($fname, 'w');
        echo $xml->saveXML() . "\n";
        fwrite($f, $xml->saveXML());
        fclose($f);
        chmod($fname, 0664);
        $n = count($name_arr);
        echo str_repeat(" ", 4*$n) . implode('-', $name_arr) . "\n";
        echo str_repeat(" ", 4*$n) . preg_replace("/\n/", "\n" . str_repeat(" ", 4*$n),
                                                  $post['message']) . "\n";
        if ($post['parent'] == null) {
            $i += 1;
        }
    }
}

Użyłem w tym kodzie funkcji getSafeThreadName, która wygeneruje nazwę katalogu, skopiowałem ją z kodu źródłowego HashOver, aby mieć pewność, że nazwa będzie taka sama (przerobiłem ją tylko z metody na funkcję, komentarze oryginalne):

function reduceDashes ($name)
{
        // Remove multiple dashes
        if (mb_strpos ($name, '--') !== false) {
                $name = preg_replace ('/-{2,}/', '-', $name);
        }

        // Remove leading and trailing dashes
        $name = trim ($name, '-');

        return $name;
}

function getSafeThreadName ($name)
{
    $dashFromThreads = array (
                '<', '>', ':', '"', '/', '\\', '|', '?',
                '&', '!', '*', '.', '=', '_', '+', ' '
        );
        // Replace reserved characters with dashes
        $name = str_replace ($dashFromThreads, '-', $name);

        // Remove multiple/leading/trailing dashes
        $name = reduceDashes ($name);

        return $name;
}

W XML-u komentarzy dodałem nowy element avatar, HashOver korzysta z szyfrowania, aby zapisać emaile. Można by zapisać email w zaszyfrowanej formie jak je czyta HashOver, niestety nie ma do nich dostępu z API (pewnie ze względu na GDPR/RODO). Dostępne są tylko ścieżki do plików miniaturek, utrzymywane na serwerach Disqus.

Moja modyfikacja biblioteki HashOver wygląda tak (wynik komendy git diff):

diff --git a/hashover/backend/classes/commentparser.php b/hashover/backend/classes/commentparser.php
index 15b5a2b..bf13c06 100644
--- a/hashover/backend/classes/commentparser.php
+++ b/hashover/backend/classes/commentparser.php
@@ -106,10 +106,16 @@ class CommentParser

                // Get avatar icons
                if ($this->setup->iconMode !== 'none') {
+
                        if ($this->setup->iconMode === 'image') {
                                // Get MD5 hash for Gravatar
                                $hash = Misc::getArrayItem ($comment, 'email_hash') ?: '';
-                               $output['avatar'] = $this->avatars->getGravatar ($hash);
+                               $avatar = Misc::getArrayItem ($comment, 'avatar');
+                               if (!empty($avatar)) {
+                                       $output['avatar'] = $avatar;
+                               } else {
+                                       $output['avatar'] = $this->avatars->getGravatar ($hash);
+                               }
                        } else {
                                $output['avatar'] = end ($key_parts);

Jak już mamy komentarze, to wystarczy je skopiować na serwer do katalogu hashover/comments/threads/. I możemy się cieszyć komentarzami bez reklam, które nikogo nie szpiegują.

Jeśli chciałbyś skasować swoje konto Disqus, może dobrym pomysłem byłoby, także pobranie wszystkich awatarów.

Pod GNU/Linux-em można to zrobić takim poleceniem:

gron posts.json | grep author.avatar.permalink | grep -oE 'https?[^"]+' | wget -i -

Podpięcie pobranych plików pozostawiam jako ćwiczenie dla czytelnika.

Narzędzie gron można znaleźć na GitHub-ie, opisywałem je we wpisie 5 Bibliotek do przetwarzania obiektów JavaScript i JSON.

Aktualizacja

Jeśli korzystacie z Google Webmaster Tools, warto także uniemożliwić indeksowanie przez Google (Google Indeksuje linki wygenerowane w JavaScript już od dość dawna), aby to uzyskać należy zmodyfikować dwa dodatkowe pliki i dodać tag nofollow.

diff --git a/hashover/backend/classes/sourcecode.php b/hashover/backend/classes/sourcecode.php
index 7fa3bf7..f87245c 100644
--- a/hashover/backend/classes/sourcecode.php
+++ b/hashover/backend/classes/sourcecode.php
@@ -468,6 +468,7 @@ class SourceCode
                                        '<html lang="en" dir="ltr">',
                                        "\t" . '<head>',
                                        "\t\t" . '<title>' . $name . '</title>',
+                    "\t\t" . '<meta name="robots" content="nofollow" />',
                                        "\t" . '</head>',
                                        "\t" . '<body>',
                                        "\t\t" . '<pre>' . $source . '</pre>',
diff --git a/hashover/backend/source-viewer.html b/hashover/backend/source-viewer.html
index a1c0001..758eb14 100644
--- a/hashover/backend/source-viewer.html
+++ b/hashover/backend/source-viewer.html
@@ -9,6 +9,7 @@

                <meta http-equiv="Content-type" content="text/html; charset=utf-8">
                <meta http-equiv="Content-Language" content="EN">
+        <meta name="robots" content="nofollow" />

                <link type="image/x-icon" href="../images/favicon.png" rel="shortcut icon">
                <link type="image/x-icon" href="../images/favicon.png" rel="icon">
diff --git a/hashover/comments/threads/.gitkeep b/hashover/comments/threads/.gitkeep
deleted file mode 100644
index e69de29..0000000

Jeśli nie chce Ci się modyfikować kodu samego, napisz komentarz, a zrobię forka HashOver i dodam wszystkie swoje modyfikacje. Np. poprawka do zmiennej $http_directory, aby działało z linkiem symbolicznym (tak mam lokalnie).

źródło strony (aby zobaczyć kod na githubie musisz kliknąć przycisk raw)
Komentarze

Hasło, które podasz umożliwi ponowne zalogowanie się i np. usunięcie komentarza, jest dobrowolne. Email jest szyfrowany i używany do wysyłania powiadomień o odpowiedziach do komentarzy oraz do pobierania awatara dzięki usłudze Gravatar.com.