• Деджаваскриптизиция (2)

    Сделал то, что намеревался: удалил с сайта все сторонние сервисы. Больше нет социальных кнопок, гугло-аналитики и, самое важное, комментариев Disqus. На страницах вообще нет аякса за тем исключением, когда встроен плеер Ютуба. Красота.

    Отдельно расскажу про комментарии. Теперь они встроены в блог, являются его частью, а не лежат на серверах Disqus. Для этого я написал импорт из XML. Ясное дело, на Кложе, исходники на Гитхабе.

    Если вкратце, код пробегает по XML, строит мапы и индексы по ID. Для каждого комментария он создает файл вроде _comments/2016-08-08-08-27-14.md с содержимым:

    ---
    id: 2826354744
    is_spam: false
    is_deleted: false
    post: /interview/
    date: 2016-08-08T08:27:14Z
    author_fullname: 'Александр'
    author_nickname: 'VinokurovAlexnader'
    author_is_anon: false
    ---
    
    <p>Вопрос "Есть кортеж из трех элементов. Назначить переменным a, b, c его значения".</p>
    <p>У Вас ответ:<br>a, b, c = [1, 2, 3]</p>
    <p>Но это список, а не кортеж. С кортежом будет вот так:<br>a, b, c = (1, 2, 3)</p>
    

    Всего таких файлов 993. В шаблон заметки я добавил код для вывода комментариев:

    {% assign comments = site.comments | where:'post', include.permalink %}
    {% if comments.size == 0 %}
    <center>Комментариев пока нет</center>
    {% else %}
    <center>Комментарии</center>
    <div class="comments-block">
      {% for comment in comments %}
        <div class="comment-block">
          <p class="comment-lead">
            <small>
              <em>{{ comment.author_fullname or comment.author_nickname }}, {{ comment.date | date_to_string: "ordinal", "RU" }}</em>
            </small>
          </p>
          <div>{{ comment.content | markdownify }}</div>
        </div>
      {% endfor %}
    </div>
    {% endif %}
    

    Получилось неоптимально, потому что на каждый пост происходит линейный поиск. В идеале нужно один раз построить словарь URL => список комментариев и затем брать оттуда. Но я не настолько хорошо знаю Jekyll и Ruby, чтобы это запилить.

    Ради интереса проверьте страницы, где много комментариев, например про сервис IVI или про интервью. Лично мне результат нравится: чистенький HTML, минимум CSS, все лаконично. На текущий момент не учитывается структура, но технически это возможно: каждый комментарий знает ID родителя, и однажды их вывод можно улучшить.

    Теперь главное: как сделать прием комментариев. Мне накидали советов, плюс я гуглил, и все сводится двум решениям: Гитхаб или self-hosted сервис.

    Напомню, как работает Гитхаб: люди пишут комменты к какому-либо issue. Зная номер issue, легко выгрести комментарии GET-запросом, сгенерить HTML и вставить под заметкой. Выглядит красиво:

    , но меня тревожат две вещи.

    1. Комментарии лежат в чужом сервисе, который может отвалиться из-за санкций или Роскомнадзора.
    2. Обязательна авторизация через Гитхаб — не у всех есть учетная запись.

    Self-hosted-решения вроде Remark42 интересны, но вынуждают держать виртуалку, платить за нее и делать бекапы. Придется возиться.

    Я придумал так. Пишем лямбду для Яндекс.Облака (на Кложе с компиляцией Граалем). Лямбда принимает POST-запросы с сайта. На сайте висит статичная форма с полем action=URL нашей лямбды. Отправить форму может любой желающий, даже тот, у кого нет учетки в Гитхабе, Твиттере, Инстаграме и так далее.

    Получив запрос, лямбда создает pull request в репозитории блога. Для этого не нужен git: все возможности Гитхаба доступны в REST API. Pull request содержит новый файл с комментарием в папке _comments. Мне приходит письмо. Если норм, я нажимаю Merge, блог собирается и выкатывается новая версия.

    Одновременно получается модерация: если кто-то написал дичь, я отклоняю PR, и делу конец.

    Открыв PR, лямбда перенаправляет пользователя обратно в блог со словами “спасибо, ваш комментарий скоро появится”.

    Вот такую штуку я хочу запилить. А пока что комментации работают только в Телеграме.

  • Деджаваскриптизиция

    Как вы думаете, сколько запросов делает браузер, пока вы читаете заметку в моем блоге? Я тоже не знал. Десять, двадцать? Что гадать, если можно проверить!

    Открываем консоль разработчика, вкладка Network. Виден всякий ajax/js-хлам. Тык правой кнопкой → “Save all as HAR with content”:

    Получаем жирный JSON со всеми запросами. Вот как выдрать из него урлы:

    cat grishaev.me.har | jq '.log.entries[].request.url' | sort > urls.txt
    

    Если открыть urls.txt и удалить адреса с доменом grishaev.me (коих 5 штук), получится… 69 запросов. Вот они:

    https://a.disquscdn.com/1662551939/images/noavatar92.png
    https://a.disquscdn.com/1662551939/images/noavatar92.png
    https://accounts.google.com/o/oauth2/iframe
    https://accounts.google.com/o/oauth2/iframerpc?action=checkOrigin&origin=https%3A%2F%2Fdisqus.com&client_id=508198334196-bgmagrg0a2rub674g0shidj8fnd50dji.apps.googleusercontent.com
    https://accounts.google.com/o/oauth2/iframerpc?action=issueToken&response_type=token%20id_token&login_hint=AJDLj6J0ayD55kiHpX6FEyK8AjchAx-IxO13rW-0myyrGzenEj7YZFquZJE4XTK6YDPt86Y7SJkVEXs3QpNMeay0bCjiNqDIwg&client_id=508198334196-bgmagrg0a2rub674g0shidj8fnd50dji.apps.googleusercontent.com&origin=https%3A%2F%2Fdisqus.com&scope=profile%20email&ss_domain=https%3A%2F%2Fdisqus.com&include_granted_scopes=true
    https://apis.google.com/_/scs/abc-static/_/js/k=gapi.lb.en.z9QjrzsHcOc.O/m=auth2/rt=j/sv=1/d=1/ed=1/rs=AHpOoo8359JQqZQ0dzCVJ5Ui3CZcERHEWA/cb=gapi.loaded_0?le=scs
    https://apis.google.com/js/api.js
    https://c.disquscdn.com/get?url=https%3A%2F%2Fgrishaev.me%2Fassets%2Fstatic%2Faws%2Fbw%2Fui.png&key=5su7aFyKRIY_tqfWlxdSzw&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fgrishaev.me%2Fassets%2Fstatic%2Faws%2Fmail%2F2.png&key=P-FCJdQOuptvsG613K8vJA&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Fexcel%2Fgood.jpg&key=8OltNCXozdgFxMHmgBTDtQ&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Ftele%2Fscan.jpg&key=3S4NMvR9Sa2dqAik9FSSBw&h=200
    https://c.disquscdn.com/get?url=https%3A%2F%2Fs3.amazonaws.com%2Figrishaev.public%2Fvlc%2Feat.png&key=_fOEu4w6cw03wCawGGrA_Q&h=200
    https://c.disquscdn.com/next/current/embed/lang/ru.js
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/funny-512x512.png
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/sad-512x512.png
    https://c.disquscdn.com/next/current/publisher-admin/assets/img/emoji/upvote-512x512.png
    https://c.disquscdn.com/next/current/recommendations/lang/ru.js
    https://c.disquscdn.com/next/embed/alfie_v4.63f1ab6d6b9d5807dc0c94ef3fe0b851.js
    https://c.disquscdn.com/next/embed/assets/font/icons.4cc7a703d2fdfe684151ff8ac24d45f1.woff2
    https://c.disquscdn.com/next/embed/assets/img/disqus-social-icon-dark.a621bea3e02c9fa04fd3965a3d6f424d.svg
    https://c.disquscdn.com/next/embed/assets/img/loader.ba7c86e8b4b6135bb668d05223f8f127.gif
    https://c.disquscdn.com/next/embed/assets/img/sprite.ad630a07080a45451f139a7487853ff8.png
    https://c.disquscdn.com/next/embed/assets/img/svg-sprite.4da5413f5086c5755b46094b813dbfcd.svg
    https://c.disquscdn.com/next/embed/common.bundle.33bc87b2c4f9324203cc85b7dd1d0492.js
    https://c.disquscdn.com/next/embed/common.bundle.33bc87b2c4f9324203cc85b7dd1d0492.js
    https://c.disquscdn.com/next/embed/lounge.bundle.8d28276e15f31af0eebfd934278922d1.js
    https://c.disquscdn.com/next/embed/lounge.bundle.8d28276e15f31af0eebfd934278922d1.js
    https://c.disquscdn.com/next/embed/lounge.load.0837a7fb2afa86b68e4ee5098ec9905b.js
    https://c.disquscdn.com/next/embed/styles/lounge.4ceaf0673822a0def820ebdc38d84415.css
    https://c.disquscdn.com/next/embed/styles/lounge.4ceaf0673822a0def820ebdc38d84415.css
    https://c.disquscdn.com/next/recommendations/assets/img/img-placeholder.df52e7638153b73862008d3d0556fdda.png
    https://c.disquscdn.com/next/recommendations/common.bundle.a59fbd11efae764ccd959d61e4925fee.js
    https://c.disquscdn.com/next/recommendations/common.bundle.a59fbd11efae764ccd959d61e4925fee.js
    https://c.disquscdn.com/next/recommendations/recommendations.bundle.926bc472e4859a48daa346b4ba2ab4f4.js
    https://c.disquscdn.com/next/recommendations/recommendations.bundle.926bc472e4859a48daa346b4ba2ab4f4.js
    https://c.disquscdn.com/next/recommendations/recommendations.load.9d352c9674ae8172f8669d3aa3a905e9.js
    https://c.disquscdn.com/next/recommendations/styles/recommendations.10022a97346f1c6e3798931bbd8e4bb5.css
    https://c.disquscdn.com/next/recommendations/styles/recommendations.10022a97346f1c6e3798931bbd8e4bb5.css
    https://cdn.viglink.com/images/pixel.gif?ch=1&rn=8.95348561821992
    https://cdn.viglink.com/images/pixel.gif?ch=2&rn=8.95348561821992
    https://connect.facebook.net/en_US/sdk.js
    https://disqus.com/api/3.0/discovery/listRecommendations.json?forum=igrishaev&thread=url%3Ahttps%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&limit=8&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/forums/details?forum=igrishaev&attach=forumFeatures&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/forums/details?forum=igrishaev&attach=forumFeatures&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/api/3.0/threadReactions/loadReactions?thread=9335567925&api_key=E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F
    https://disqus.com/embed/comments/?base=default&f=igrishaev&t_u=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&t_d=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&t_t=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&s_o=default
    https://disqus.com/next/config.js
    https://disqus.com/next/config.js
    https://disqus.com/next/config.js
    https://disqus.com/recommendations/?base=default&f=igrishaev&t_u=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&t_d=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.&t_t=Zippo%3A%20additions%20to%20the%20standard%20clojure.zip%20package.
    https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i&display=swap&subset=cyrillic,cyrillic-ext,latin-ext
    https://fonts.gstatic.com/s/ptserif/v17/EJRSQgYoZZY2vCFuvAnt66qSVyvVp8NA.woff2
    https://fonts.gstatic.com/s/ptserif/v17/EJRVQgYoZZY2vCFuvAFSzr-_dSb_nco.woff2
    https://fonts.gstatic.com/s/ptserif/v17/EJRVQgYoZZY2vCFuvAFWzr-_dSb_.woff2
    https://glitter.services.disqus.com/urls/?callback=dsqGlitterResponseHandler&forum_shortname=igrishaev&thread_id=9335567925&referer=https%3A%2F%2Fgrishaev.me%2F
    https://igrishaev.disqus.com/embed.js
    https://igrishaev.disqus.com/recommendations.js
    https://io.narrative.io/?companyId=19&id=disqus_id%3Ac6g0c4dk2irns7l&ret=img&ref=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F
    https://links.services.disqus.com/api/ping
    https://live.rezync.com/pixel.html?c=4656c20ee35215f78e9273796625d90b&cid=c6g0c4dk2irns7l&pctry=RU&referrer=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F
    https://mc.yandex.ru/metrika/tag.js
    https://pippio.com/api/sync?pid=1391&ref=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&it=1&iv=c6g0c4dk2irns7l
    https://referrer.disqus.com/juggler/event.gif?abe=1&embed_hidden=0&load_time=1311&event=init_embed&thread=9335567925&forum=igrishaev&forum_id=3964395&imp=2l7278t3kf6iha&prev_imp&thread_slug=zippo_additions_to_the_standard_clojurezip_package&user_type=anon&referrer=https%3A%2F%2Fgrishaev.me%2F&theme=next&dnt=0&tracking_enabled=1&experiment=network_default&variant=fallthrough&service=dynamic&promoted_enabled=true&max_enabled=true
    https://referrer.disqus.com/juggler/event.js?experiment=network_default&variant=fallthrough&page_referrer=https%3A%2F%2Fgrishaev.me%2F&product=embed&thread=9335567925&thread_id=9335567925&forum=igrishaev&forum_id=3964395&zone=thread&page_url=https%3A%2F%2Fgrishaev.me%2Fen%2Fzippo%2F&service=dynamic&verb=view&object_type=product&object_id=embed&extra_data=%7B%22color_scheme%22%3A%22light%22%2C%22anchor_color%22%3A%22rgb(42%2C122%2C226)%22%2C%22typeface%22%3A%22serif%22%2C%22width%22%3A740%7D&event=activity&imp=2l7278t3kf6iha&prev_imp=&section=default&area=n%2Fa
    https://referrer.disqus.com/juggler/stat.gif?event=lounge.loading.view
    https://www.google-analytics.com/analytics.js
    https://www.gstatic.com/_/mss/boq-identity/_/js/k=boq-identity.IdpIFrameHttp.en_US.z2a12LkW96g.es5.O/d=1/rs=AOaEmlHeSnuHrM44y1tAD9SSj44ODEuRFQ/m=base
    https://yastatic.net/es5-shims/0.0.2/es5-shims.min.js
    https://yastatic.net/share2/share.js
    

    Да, Карл: шестьдесят девять запросов на всякие аналитики, кнопки шаринга и комментарии Disqus. Не знаю, почему некоторые урлы повторяются, выяснять это нет никакого желания. Важное уточнение: 69 – это только в начале. Если промотать страницу и написать комментарий, подгрузится еще пачка скриптов.

    С Disqus вообще беда: с недавних пор они внедрили рекламу. Блокировщики справляются, но как-то раз я зашел на сайт из голого Сафари и о…уел от того, что творит Disqus на странице:

    На мобиле еще хуже: баннеры выстраиваются вертикально, и нужно мотать два экрана.

    Кое-что из этого можно отключить в настройках, но в целом от рекламы нельзя избавиться, не заплатив. Да и не в рекламе дело, а в адском количестве скриптов. Сомневаюсь, что купив подписку, можно убавить их число.

    Ясно одно: это не правильно, нужно исправлять.

    Социальные кнопки и гугло-аналитику банально удалю. За кнопки мне стыдно, потому что это рудимент нулевых. Сегодня они встречаются разве что на сайтах, сделанных в говно-CMS вроде Джумлы, Друпала и Вордпресса. Аналитика была б хороша, если б Гугл не плевал на все этические нормы. Выбирая между желанием посмотреть, кто и что читает и приватностью читателей, я предпочту второе.

    Самое интересное с комментариями. Не сказать, что мои заметки активно комментируют, но время от времени это случается. Особо приятно получить комментарий от бывших коллег, с которыми когда-то работал. Несколько раз в комментариях писали ценные вещи, и я был искренне благодарен.

    У меня возникла идея перенести комментарии Disqus в блог. Технически это возможно: в админке Disqus выгружаем все комментарии, получаем жирный XML-файл (про JSON ребята не слышали). Далее пишем скрипт, который обходит XML и дописывает в файлы с постами. Однажды я уже делал так, когда мигрировал с Эгеи (движка на PHP), только вместо XML был дамп MySQL.

    Остался последний вопрос: как сделать прием комментариев без Disqus? У меня есть одна мысль, но я опишу ее в другой раз, а пока что послушаю ваши советы.

  • REPL, Cider, Emacs (часть 3/4)

    Все части

    Оглавление

    Пространства имен

    При работе с REPL мы всегда находимся в каком-то пространстве. По умолчанию оно написано в приглашении:

    user=> (+ 1 2)
    

    Если перейти в другое пространство, изменится и приглашение:

    user=> (in-ns 'foobar)
    foobar=>
    

    Код, что мы вводим в REPL, вычисляется в текущем пространстве. Если объявить в модуле user переменную:

    (in-ns 'user)
    (def number 1)
    

    , а затем сослаться на нее в пространстве foobar, получим ошибку, что символ number не найден в текущем контексте:

    (in-ns 'foobar)
    (+ 1 number)
    
    Syntax error compiling at (repl-chapter:localhost:53495(clj)*:1:8440).
    Unable to resolve symbol: number in this context
    

    Другой пример: объявим в модулях user и foobar переменные number со значениями 1 и 2. Теперь одна и та же форма (inc number) даст разный результат в зависимости от того, какое пространство текущее. Поэтому перед вычислением мы должны убедиться, что находимся в нужном пространстве имен.

    Чтобы уберечь нас от подобных ошибок, nREPL учитывает параметр ns в сообщениях. Когда мы выполняем код при помощи cider-eval-..., в сообщении, помимо полей op и code, передается ns. Его значение Cider находит из формы (ns...) в начале файла. Вычисляя форму, сервер временно меняет пространство, и результат совпадает с тем, что ожидают.

    Read more →

  • Zippo: additions to the standard clojure.zip package.

    The clojure.zip package is a masterpiece yet misses some utility functions. For example, finding locations, bulk updates, lookups, breadth-first traversing and so on. Zippo, the library I’m introducing in this post, brings some bits of missing functionality.

    Installation

    Lein:

    [com.github.igrishaev/zippo "0.1.0"]
    

    Deps.edn

    {com.github.igrishaev/zippo {:mvn/version "0.1.0"}}
    

    Usage & examples

    First, import both Zippo and clojure.zip:

    (ns zippo.core-test
      (:require
       [clojure.zip :as zip]
       [zippo.core :as zippo]))
    

    Declare a zipper:

    (def z
      (zip/vector-zip [1 [2 3] [[4]]]))
    

    Now check out the following Zippo functions.

    A finite seq of locations

    The loc-seq funtion takes a location and returns a lazy seq of locations untill it reaches the end:

    (let [locs (zippo/loc-seq z)]
      (mapv zip/node locs))
    
    ;; get a vector of notes to reduce the output
    [[1 [2 3] [[4]]]
     1
     [2 3]
     2
     3
     [[4]]
     [4]
     4]
    

    This is quite useful to traverse a zipper without keeping in mind the ending condition (zip/end?).

    Finding locations

    The loc-find function looks for the first location that matches a predicate:

    (let [loc (zippo/loc-find
               z
               (fn [loc]
                 (-> loc zip/node (= 3))))]
    
      (is (= 3 (zip/node loc))))
    

    Above, we found a location which node equals 3.

    The loc-find-all function finds all the locatins that match the predicate:

    (let [locs (zippo/loc-find-all
                z
                (zippo/->loc-pred (every-pred int? even?)))]
    
      (is (= [2 4]
             (mapv zip/node locs))))
    

    Since the predicate accepts a location, you can check its children, siblings and so on. For example, check if a location belongs to a special kind of parent.

    However, most of the time you’re interested in a value (node) rather than a location. The ->loc-pred function converts a node predicate, which accepts a node, into a location predicate. In the example above, the line

    (zippo/->loc-pred (every-pred int? even?))
    

    makes a location predicate which node is an even integer.

    Updating a zipper

    Zippo offers some functions to update a zipper.

    The loc-update one takes a location predicate, an update function and the rest arguments. Here is how you douple all the even numbers in a nested vector:

    (let [loc
          (zippo/loc-update
           z
           (zippo/->loc-pred (every-pred int? even?))
           zip/edit * 2)]
    
      (is (= [1 [4 3] [[8]]]
             (zip/root loc))))
    

    For the updating function, one may use zip/append-child to append a child, zip/remove to drop the entire location and so on:

    (let [loc
          (zippo/loc-update
           z
           (fn [loc]
             (-> loc zip/node (= [2 3])))
           zip/append-child
           :A)]
    
      (is (= [1 [2 3 :A] [[4]]]
             (zip/root loc))))
    

    The node-update function is similar but acts on nodes. Instead of loc-pred and loc-fn, it accepts node-pred and node-fn what operate on nodes.

    (let [loc
        (zippo/node-update
         z
         int?
         inc)]
    (is (= [2 [3 4] [[5]]]
           (zip/root loc))))
    

    Slicing a zipper by layers

    Sometimes, you need to slice a zipper on layers. This is what is better seen on a chart:

         +---ROOT---+    ;; layer 1
         |          |
       +-A-+      +-B-+  ;; layer 2
       | | |      | | |
       X Y Z      J H K  ;; layer 3
    
    • Layer 1 is [Root];
    • Layer 1 is [A B];
    • Layer 3 is [X Y Z J H K]

    The loc-layers function takes a location and builds a lazy seq of layers. The first layer is the given location, then its children, the children of children and so on.

    (let [layers
          (zippo/loc-layers z)]
    
      (is (= '(([1 [2 3] [[4]]])
               (1 [2 3] [[4]])
               (2 3 [4])
               (4))
             (for [layer layers]
               (for [loc layer]
                 (zip/node loc))))))
    

    Breadth-first seq of locations

    The clojure.zip package uses depth-first method of traversing a tree. Let’s number the items:

           +-----ROOT[1]----+
           |                |
     +----A[2]---+     +---B[6]--+
     |     |     |     |    |    |
     X[3] Y[4] Z[5]   J[7] H[8] K[9]
    

    This sometimes may end up with an infinity loop when you generate children on the fly.

    The loc-seq-breadth functions offers the opposite way of traversing a zipper:

           +-----ROOT[1]----+
           |                |
     +----A[2]---+     +---B[3]--+
     |     |     |     |    |    |
     X[4] Y[5] Z[6]   J[7] H[8] K[9]
    

    This is useful to solve some special tasks related to zippers.

    Lookups

    When working with zippers, you often need such functionality as “go up/left/right until meet something”. For example, from a given location, go up until a parent has a special attribute. Zippo offers four functions for that, namely lookup-up, lookup-left, lookup-right, and lookup-down. All of them take a location and a predicate:

    (let [loc
          (zip/vector-zip [:a [:b [:c [:d]]] :e])
    
          loc-d
          (zippo/loc-find loc
                          (zippo/->loc-pred
                           (fn [node]
                             (= node :d))))
    
          loc-b
          (zippo/lookup-up loc-d
                           (zippo/->loc-pred
                            (fn [node]
                              (and (vector? node)
                                   (= :b (first node))))))]
    
      (is (= :d (zip/node loc-d)))
    
      (is (= [:b [:c [:d]]] (zip/node loc-b))))
    

    In the example above, first we find the :d location. From there, we go up until we meet [:b [:c [:d]]]. If there is no such a location, the result will be nil.

    Also See

    The code from this library was used for Clojure Zippers manual – the complete guide to zippers in Clojure from the very scratch.

    © 2022 Ivan Grishaev

  • Деформация

    Последний месяц я вожусь с ботами для Телеграма. Занятная вещь, и как-нибудь расскажу об этом подробней. А пока что мелкое наблюдение.

    Чтобы отсечь спамеров, мой бот выводит задачу уровня 3 + 5 и кнопки с ответами. Кнопок шесть: одна правильная, остальные неправильные. Писать текст нельзя, только жать на кнопку. Если ответил верно, ограничения снимаются. После трех ошибок бан.

    Код для генерации кнопок я писал впопыхах. Коротко, он такой: взять пять случайных чисел, добавить к ним ответ, перемешать. Но не учел, что в случайных числах может выпасть ответ, из-за чего будет две правильные кнопки. Это случается редко, но все же. Вот один из случаев:

    Так вот, наблюдение: когда айтишник видит два правильных ответа, у него рвется шаблон. Начинаются фразочки OMG какой ответ правильный!! и прочая истерика. Тут у меня происходит двоемыслие.

    С одной стороны, что значит “какой ответ правильный”? Если задача 3 + 5 и две кнопки 8, обе из них правильные. Не бывает двух разных цифр 8 — она одна. Поэтому вместо истерики надо жать ту кнопку, которая по душе — ближе, дальше, круглее, — главное, чтобы на ней была восьмерка.

    С другой стороны, айтишников легко понять. Мы привыкли, что за текстом на кнопке стоит айдишник, который уходит на сервер, и все рассчитывается по айдишнику. Вполне возможно, у первой кнопки “8” айди btn_4, а у второй — btn_9. И не докажешь, что не верблюд.

    Помню, меня страшно бесили тесты по английскому на компе. Нужно было перетащить слова в прочерки, при этом глагол is часто встречался дважды. И хотя я все расставлял правильно, получал ошибку. Оказалось, у каждого слова свой айдишник, а в базе данных задана их последовательность. Имеем два is с номерами 2 и 5. Не так расставил — ошибка.

    В итоге скрипт, написанный на коленке, разошелся по школам, сайтам, университетам. Где только я с ним не сталкивался! Даже недавно, во время ковидного дурдома, сын делал английский онлайн, и там была та же проблема.

    Происходит деформация: даже если на двух кнопках написано одно и то же, мы не верим. Мы ищем подвох, потому что слишком часто были обмануты. В результате две кнопки 8 кажутся такой же ошибкой, как 3 + 5 = 9.

    Сюда же следует отнести штучки JavaScript вроде [1, 2, 3] == [1, 2, 3]. Только что проверил в консоли — вернет ложь. На JavaScript работают миллионы сайтов и устройств, а конца этому не видно. Весь этот бред так и тянется от стандарта к стандарту.

    В результате разработчик настолько привык к граблям технологий, что всячески оправдывает их косяки и ищет злой умысел там, где его нет. Так быть не должно.

    Подобно тому, как нужно не курить, не бухать и заниматься спортом, нужно работать с правильными технологиями. Такими, где массив [1, 2, 3] все-таки равен другому такому же массиву; где нет пяти функций сравнения; где код вернет то, что ожидаешь, а не то, что прописано на сотой странице стандарта. И деформации в голове станет меньше.

  • REPL, Cider, Emacs (часть 2/4)

    Все части

    Оглавление

    REPL в редакторе

    До сих пор мы набирали код в терминале, что не совсем удобно. Терминал подходит для коротких команд, но плохо справляется с многострочным вводом. Будет правильно набрать код в редакторе, а затем скопировать в терминал. Код останется в файле, и не придется печатать его в следующий раз.

    Со временем вы заметите, что переключение между редактором и терминалом отнимает время. Было бы здорово связать редактор с REPL напрямую. Вы набираете код и с помощью комбинации клавиш выполняете в REPL. В отдельной области редактор показывает результат. С таким подходом нам доступна мощь обеих сред: REPL и редактора.

    Read more →

  • Веб-файлы

    Каждый раз, когда вижу файловый интерфейс в вебе, думаю: почему такой пиздец? Я не люблю материться и делаю это редко. Но когда вижу браузерные поделки, хочется закрыть глаза от отчаяния.

    Вот скриншоты с официального сайта Дропбокса. Первая картинка:

    Что мы видим? Всюду адская разреженность. Изображение с отступами, сверху и справа два жирных бара. Иконки в барах мелкие и висят в воздухе. Дикая несогласованность элементов: сперва кнопка с надписью, кнопка с многоточием, заголовок, синяя кнопка со стрелкой вниз.

    Что делает заголовок среди кнопок? Там не поместится ничего длиннее пары слов. Почему кнопка Share синяя, в чем ее особый статус? Почему выпадашка слева отмечена многоточием, а у Share стрелка вниз?

    Зачем колокольчик в окне просмотра файла? Это события, связанные с этим файлом или любые события? Зачем сайдбар справа? Нельзя было уместить жалкие три кнопки наверху?

    Другая картинка:

    То же самое: адская разреженность. На месте трех файлов было легко уместить пятнадцать без потери смысла и качества.

    Наверху значок лупы, но поля ввода нет. Полагаю, оно появится по клику на лупу. Зачем кликать, если можно сразу нарисовать поле?

    Зачем земной шар? Открыть какую-то ссылку? Если да, то какую, и зачем мне туда переходить? Почему бы не разместить гиперссылку с текстом, например, “на сайт”?

    Полоса с текстом “Syncing” срезает столько места, что хватило бы на пару файлов.

    Третья картинка, но там уже нечего комментировать: сочетание косяков в разных пропорциях.

    А вот картинка, открытая в Preview на маке:

    • заголовок отдельно от кнопок. Влезет даже предложение.
    • изображение занимает максимальную площадь
    • виджет поиска отрисован с полем ввода
    • все иконки одинаковы и пригнаны друг к другу
    • по желанию в тулбар можно загнать другие иконки и поменять их местами
    • можно сменить показ иконок: только картинка, только текст или вместе.

    Дело даже не в маке: если взять Total Commander или виндовый Explorer, там то же самое: информация показана компактно и удобно. Могут быть придирки к конкретной программе: скажем, Explorer в режиме значков такой себе, но со списком можно жить. Это уже дело вкуса. Важно, что всем файловым менеджерам свойственна компактность и гибкость в настройках.

    А с вебом беда. Мамкины дизайнеры каждый день видят нормальный интерфейс, но рисуют в Фигме говно. Хотя ничего придумывать не надо: повтори Finder или Explorer, и люди скажут спасибо. Но нет: дизайнер дизайнерит.

    Беда не только у Дропбокса. Гугл, Слак и другие тяжеловесы не могут показать файлы в браузере. Каждый раз выходит ужас: растрата места, жирные бары, несогласованные кнопки — тут многоточие, там выпадашка. Иконки либо мелкие, либо гротескно жирные, словно в азбуке для малышей. Ни у одной иконки нет подписи, нужно наводить курсор или жать наудачу.

    Невозможно. Считаю, в целях воспитания нужно отобрать у дизайнеров родной файловый менеджер и выдать то, что они производят. Тогда дело быстро пойдет на лад.

  • REPL, Cider, Emacs (часть 1/4)

    Все части

    Оглавление

    Эта глава расскажет о REPL — фундаментальном свойстве Clojure. Так называют интерактивную работу с языком, когда программу наращивают постепенно. Мы рассмотрим, что такое REPL-driven development и почему, однажды познав, от него трудно отказаться.

    Аббревиатура REPL происходит от четырех слов: Read, Eval, Print и Loop. Дословно они означают прочитать, выполнить, напечатать и повторить. REPL — устойчивый термин, под которым понимают интерактивный режим программы.

    Многие современные языки предлагают интерактивный режим. Как правило, он запускается, если вызвать интерпретатор без параметров. Например, команды python или node запустят интерактивные сеансы Python и Node.js. В Ruby для этого служит утилита irb (где i означает interactive). REPL поддерживают не только интерпретаторы, но и языки, которые компилируются в байт-код (Java, Scala) или машинный код (Haskell, SBCL).

    Несмотря на это разнообразие, именно в Лисп-системах REPL имеет решающее значение. Если в Python или Node.js его рассматривают как приятное дополнение, то в Лиспе он необходим. Разработка на любом Лиспе зависит от того, насколько хорошо вы взаимодействуете с REPL. На REPL так или иначе опираются все инструменты и практики, документация, видеоуроки и так далее.

    В мире Лиспа ходит понятие REPL-driven development. Это стиль разработки, когда код пишут малыми порциями и запускают в REPL. С таким подходом сразу видно поведение программы. Легче проверить неочевидные случаи, например вызвать функцию с nil или обратиться к ресурсу, которого не существует.

    Другой полезный сценарий для REPL — извлечь данные из сети и исследовать их. Эта задача идеально ложиться на интерактивный режим. Как правило, HTTP-запрос предполагает несколько этапов: его подготовку, отправку, чтение тела и поиск нужных полей. Эти шаги проходят интерактивно методом проб и ошибок. Позже мы рассмотрим пример HTTP-запроса в сеансе REPL.

    Read more →

  • Teleward: a CAPTCHA bot for Telegram in Clojure + GraalVM

    (This is a copy of the readme file from the repository.)

    Teleward is a Telegram captcha bot written in Clojure and compiled with GraalVM native image. Runs on bare Linux/MacOS with no requirements. Fast and robust.

    Why

    Telegram chats suffer from spammers who are pretty smart nowadays. They don’t use bots; instead, they register ordinary accounts and automate them with Selenium + web-version of Telegram. Personally, I found Shieldy and other bots useless when dealing with such kind of spammers. This project aims the goal to finish that mess.

    Another reason I opened Teleward for is to try my skills in developing Clojure applications with GraalVM. Binary applications are nice: they are fast, and they don’t need installing JDK. At the same time, they’re are still Clojure: REPL is here, and that’s amazing.

    Features

    • This is Clojure, so you have REPL! During development, you call Telegram API directly from REPL and see what’s going on.
    • The bot can be delivered either as a Jar file or a binary file (with Graal).
    • When Graal-compiled, needs no requirements (Java SDK, etc). The binary size is about 30 Mb.
    • At the moment, supports only long polling strategy to obtain messages. The webhook is to be done soon.
    • Keeps all the state in memory and thus doesn’t need any kind of a database. The only exception is the current offset value which is tracked in a file.
    • Supports English and Russian languages.
    • Two captcha styles: normal “1 + 2” and Lisp captcha “(+ 1 2)”.
    • The +, -, and * operators are corresponding Unicode characters that prevent captcha from naive evaluation.

    Algorithm

    The bot listens for all the messages in a group. Once a new pack of messages arrives, the bot applies the following procedure to each message:

    • Mark new members as locked.
    • Send a captcha message to all members.
    • Unless an author of a message is locked, delete that message.
    • If a message is short and matches the captcha’s solution, unlock a user and delete the catpcha message.
    • If a locked user has posted three messages with no solution, ban them.
    • If a locked user hasn’t solved captcha in time, ban them as well.

    Please note: the bot processes only messages no older than two minutes from now. In other words, the bot is interested in what is happening now (with a slight delay), but not in the far past. This is to prevent a sutuation what a bot had been inactive and then has started to consume messages. Without this condition, it will send captcha to chat members who have already joined and confuse them.

    Java version

    To make a Jar artefact, run:

    make uberjar
    

    The uberjar target calls lein uberjar and also injects the VERSION file into it. The output file is ./target/teleward.jar.

    Binary version, Linux

    Linux version is built inside a Docker image, namely the ghcr.io/graalvm/graalvm-ce one with native-image extension preinstalled. Run the following command:

    make docker-build
    

    The output binary file appears at ./target/teleward.

    Binary version, MacOS

    gu install native-image
    
    • Then make the project:
    make
    

    Setting Up Your Bot

    • To run the bot, first you need a token. Contact @BotFather in Telegram to create a new bot. Copy the token and don’t share it.

    • Add your new bot into a Telegram group. Promote it to admins. At least the bot must be able to 1) send messages, 2) delete messages, and 3) ban users.

    • Run the bot locally:

    teleward -t <telegram-token> -l debug
    

    If everything is fine, the bot will start consuming messages and print them in console.

    Configuration

    See the version with -v, and help with -h. The bot takes into account plenty of settings, yet not all of them are available for configuration for now. Below, we name the most important parameters you will need.

    • -t, --telegram.token: the Telegram token you obtain from BotFather. Required, can be set via an env variable TELEGRAM_TOKEN.

    • -l, --logging.level: the logging level. Can be debug, info, error. Default is info. In production, most likely you will set error.

    • --telegram.offset-file: where to store offset number for the next getUpdates call. Default is TELEGRAM_OFFSET in the current working directory.

    • --lang: the language for messages. Can be en, ru, default is ru.

    • --captcha.style: a type of captcha. When lisp, the captcha looks like a lisp expression (+ 4 3). Any other value type will produce 4 + 3. The operator is taken randomly.

    Example:

    ./target/teleward -t <...> -l debug \
      --lang=en --telegram.offset-file=mybot.offset \
      --captcha.style=normal
    

    For the rest of the config, see the src/teleward/config.clj file.

    Deploying on bare Ubuntu

    • Buy the cheapest VPS machine and SSH to it.

    • Create a user:

    sudo useradd -s /bin/bash -d /home/ivan/ -m -G sudo ivan
    sudo passwd ivan
    mkdir /home/ivan/teleward
    
    • Compile the file locally and copy it to the machine:
    scp ./target/teleward ivan@hostname:/home/ivan/teleward/
    
    • Create a new systemctl service:
    sudo mcedit /etc/systemd/system/teleward.service
    
    • Paste the following config:
    [Unit]
    Description = Teleward bot
    After = network.target
    
    [Service]
    Type = simple
    Restart = always
    RestartSec = 1
    User = ivan
    WorkingDirectory = /home/ivan/teleward/
    ExecStart = /home/ivan/teleward/teleward -l debug
    Environment = TELEGRAM_TOKEN=xxxxxxxxxxxxxx
    
    [Install]
    WantedBy = multi-user.target
    
    • Enable autoload:
    sudo systemctl enable teleward
    
    • Manage the service with commands:
    sudo systemctl stop teleward
    sudo systemctl start teleward
    sudo systemctl status teleward
    

    For Jar, the config file would be almost the same except the ExecStart section. There, you specify something like java -jar teleward.jar ....

    Health check

    The bot accepts the /health command which it replies to “OK”.

    Further work

    • Implement webhook.
    • Add tests.
    • Report uptime for /health.
    • More config parameters via CLI args.
    • Widnows build.

    © 2022 Ivan Grishaev

  • Невышедший подкаст про Лисп. Работа над ошибками

    История о том, как я участвовал в подкасте про Лисп и Кложу, который не вышел в свет. Это ни в коем случае не обвинения любой из сторон, а попытка понять, что пошло не так. Кроме того, я вынес кое-какие уроки, чтобы подобное не повторилось в будущем.

    Три месяца назад мне постучались одни Крутые Ребята с предложением выступить в подкасте. Тема — Лисп и Кложа. Назначили дату и время, записали подкаст. Еще до записи кое-что показалось мне странными, но я утешал себя тем, что ребята Крутые и знают, что делают. Поэтому оставил на их усмотрение.

    Итак, записали, обменялись любезностями, супер-супер, огонь-огонь. Проходит месяц, другой. Ребята выпускают эпизоды, но моего среди них нет. Я спокойно жду, потому что знаю: у опытных подкастеров эпизоды расписаны на месяц. Кроме того, интересный материал иногда оставляют “в консервах”, то есть на случай, когда ничего интересного нет.

    В какой-то момент не стерпел и вежливо спросил, как дела с выпуском. Оба собеседника получили сообщение и… тишина. Проходит семь часов, я аккуратно повторяю вопрос. Наконец отвечают: выпуска не будет. Слово за словом выясняется, что ведущий им недоволен, получилось очень сложно, слушатели не поймут.

    Что ж, не будет и бог с ним, бывает. Но удивляюсь, почему нельзя было сказать об этом раньше вместо того, чтобы прятаться? Ведь Ребята такие Крутые: открытые, позитивные, вот это все. Но как только пошло не так, началась тишина.

    Безо всяких претензий пишу им: понимаю, не в обиде. Можно выложу у себя в блоге? Для этого нужна дорожка ведущего: во время записи у меня осталась только моя дорожка. Сведу и смонтирую сам. Отвечают: нужно согласие Главного Человека, сейчас спросим его и ответим. Я аж поморщился от этой лжи, потому что у тех, с кем я разговаривал, было достаточно полномочий для принятия решения. Они всем управляют, и вдруг оказывается, нужно спросить. Стоит ли говорить, что через месяц мне никто не ответил.

    По итогам событий я сделал следующие выводы.

    (1) Перед выступлением нужно изучить аудиторию, понять ее уровень. Уже после согласия я понял, что это популярный подкаст по принципу “все обо всем”. Рассказывать такой аудитории про Кложу и Лисп трудно. Это специфичные технологии, и слушатель должен знать азы программирования. Если этого нет, рассказ про Лисп не принесет удовольствия ни одной из сторон, что и подтвердилось. Во время записи ведущий часто перебивал меня с просьбой объяснить тот или иной момент. Я, напротив, был недоволен, что мы топчемся на месте. К концу второго часа мы наконец выяснили, что код на Лиспе состоит из списков, и в нем есть REPL и макросы. Это десятая часть того, что я мог бы рассказать про Лисп подготовленной аудитории.

    (2) Стороны должны согласовать тезисы. Когда я выступал в Подлодке, ведущий потребовал с меня тезисы и прислал образец с прошлого выпуска. Это в высшей степени правильно, потому что делает выпуск согласованным: гость и ведущий на одной волне. В случае с Крутыми Ребятами тезисы не понадобились: ведущий сказал, что подготовится сам. Позже он “ради интереса” попросил выслать тезисы для Подлодки, что я и сделал. Однако ни один тезис мы не обсудили.

    (3) Если что-то идет не так, надо честно признаться и сказать правду, не дожидаясь разборок. Меня расстроило не то, что эпизод не вышел, а отношение по принципу “молчим, авось отъебется”. Не надо так, потому что со страниц блогов и проектов вы Крутые, и вдруг такое. Реально расстраиваешься.

    (4) В то же время, это урок мне. Вдруг я тоже произвожу хорошее впечатление в блоге, а на деле сливаюсь? Не обидел ли я кого нибудь таким же образом? Пишу одно, делаю другое? Есть над чем подумать.

    В завершение текста покажу одно видео. Выступает Илья Синельников, тема “Провал — это хорошо”. Одно из немногих видео, которое я однажды посмотрел и тепло вспоминаю. Если коротко — важно не то, как вы ведете себя, когда все хорошо. Истинно важно — когда дела идут не по плану; тут и проявляется самое важное. Полчаса удовольствия:

Страница 17 из 74