release: 2026-04-23-191549

This commit is contained in:
He4eT 2026-04-23 19:15:50 +02:00
commit 461c2a920b
80 changed files with 6968 additions and 0 deletions

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta
name="description"
content="Redirect to '/posts/#2020'.">
<meta
http-equiv="Refresh"
content="0; URL=/posts/#2020">
<title>
Redirect | oddsquat
</title>
</head>
<body>
<main>
Redirect to
<a
style="color: inherit;"
href="/posts/#2020">
/posts/#2020
</a>
</main>
</body>
</html>

View file

@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<title>
initial post | oddsquat
</title>
<meta name="description" content="Первый пост в этом фэнзине, рассказывающий о его внутреннем устойстве.">
<link rel="preload" href="/fonts/open_sans_condensed-32.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans_condensed-27.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-25.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-24.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-17.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" type="text/css" href="/css/fonts.css">
<link rel="stylesheet" type="text/css" href="/css/typography.css">
<link rel="stylesheet" type="text/css" href="/css/main.css">
</head>
<body>
<div class="stripesContainer">
<div class="stripes">
</div>
</div>
<header>
<nav>
<ul>
<li><a href="/">oddsquat</a></li>
<li><a href="/posts/">
posts</a></li>
<li><a href="/posts/#2020">
2020</a></li>
<li>initial post</li>
</ul>
</nav>
</header>
<main>
<article>
<h1 id="initial-post">Initial Post</h1>
<p><strong>Oddsquat</strong>&nbsp;— мой маленький личный фэнзин про эксперименты, код и&nbsp;прочий киберпанк. Обычно такие сайты называются блогами, но&nbsp;в&nbsp;слове «фэнзин» есть приятный отголосок панк-культуры и&nbsp;её&nbsp;неотъемлемой DIY-составляющей.</p>
<p>Основные причины, побудившие меня на&nbsp;создание своего уютного куска интернета:</p>
<ul>
<li>Хочется писать буквы, а&nbsp;возвращаться в&nbsp;ЖЖ&nbsp;не&nbsp;хочется.</li>
<li>Фронтендер без портфолио&nbsp;— подозрительный фронтендер.</li>
<li>Современный веб&nbsp;— памятник оверинженирингу. Лендинг с&nbsp;двумя абзацами текста, тремя картинками и&nbsp;мегабайтным бандлом из&nbsp;модных библиотек&nbsp;— одновременно безумие и&nbsp;норма в&nbsp;текущих реалиях. Мне больно на&nbsp;это смотреть, поэтому пришлось строить себе персональный оазис.</li>
</ul>
<p>В&nbsp;первом посте хочется рассказать, какие инструменты были использованы для создания этого сайта, какие решения были приняты и&nbsp;почему.</p>
<h2 id="-">Название</h2>
<p>Я&nbsp;вырос в&nbsp;девяностые, поэтому квикимобиль, бэтпещера и&nbsp;пузиблинчики для меня не&nbsp;пустой звук, а&nbsp;образец системы нейминга. Вот, например, у&nbsp;меня есть <a href="https://github.com/He4eT/oddkb" target="_blank">oddkb</a>.</p>
<p>Свою квартиру с&nbsp;момента заселения я&nbsp;называю oddsquat (даже локация в&nbsp;Foursquare была), так пусть теперь и&nbsp;мой форпост в&nbsp;сети называется так же.</p>
<p>Назвал сайт в&nbsp;честь SSID домашнего Wi-Fi, потому что не&nbsp;смог придумать более достойного названия :)</p>
<h2 id="-">Платформа</h2>
<p>Для статического сайта не&nbsp;нужен серверный код, поэтому WordPress и&nbsp;прочие CMS в&nbsp;качестве платформы я&nbsp;даже не&nbsp;рассматривал.</p>
<p>Про генераторы статических сайтов раньше слышал, но&nbsp;подробно не&nbsp;изучал. Их&nbsp;оказалось так много (тут есть <a href="https://jamstack.org/generators/">длинный список</a>), что даже страшно.</p>
<p>Посмотрев на&nbsp;самые популярные решения, я&nbsp;понял, что как-то неправильно понимаю значение термина «генератор статических сайтов», потому что все хором предлагают мне сгенерировать Progressive Web Application из&nbsp;React-компонентов с&nbsp;импортом данных из&nbsp;GraphQL-сервера вместо россыпи HTML-документов.</p>
<p>Уверен, что уютный бложик&nbsp;— это точно не&nbsp;Web&nbsp;Application, а&nbsp;от&nbsp;Progressive&nbsp;в&nbsp;данном случае всем будет только хуже: искусственные трудности с&nbsp;роутингом, серверным рендерингом и&nbsp;сохранением позиции скрола успешно решены, когда каждая страница&nbsp;— это отдельный документ. Браузеры восхитительно умеют работать с&nbsp;HTML-документами!</p>
<p>После непродолжительных поисков был найден <a href="https://doug2k1.github.io/nanogen/">Nanogen</a>, который делает именно то, что нужно, и&nbsp;не&nbsp;похож на&nbsp;космический корабль:</p>
<ul>
<li>Умеет Markdown и&nbsp;HTML (ejs);</li>
<li>Имеет очень гибкую систему шаблонов;</li>
<li>Не&nbsp;выходит за&nbsp;рамки стека Node.js.</li>
</ul>
<p>Да, в&nbsp;репозитории проекта давно не&nbsp;было коммитов, но&nbsp;хочется думать, что это по&nbsp;причине надёжности и&nbsp;закончености :)</p>
<h2 id="-">Типографика</h2>
<p>Я&nbsp;люблю текст и&nbsp;не&nbsp;умею в&nbsp;дизайн. Oddsquat&nbsp;— проект про буквы, значит, буквы должны быть красивыми и&nbsp;удобными.</p>
<p>К&nbsp;теме типографики в&nbsp;вебе я&nbsp;подходил несколько раз, но&nbsp;безуспешно. Не&nbsp;получается структурировать и&nbsp;сохранить в&nbsp;голове все эти сакральные знания про кернинг и&nbsp;засечки, потому что не&nbsp;вижу никакой связи между теорией и&nbsp;практикой.</p>
<p>Очередное погружение системных знаний опять не&nbsp;принесло, зато принесло удобный инструмент: <a href="https://github.com/KyleAMathews/typography.js">Typography.js</a>.</p>
<p>Этот тулкит позволяет, покрутив ручки <a href="http://kyleamathews.github.io/typography.js/">интерактивного демо</a>, получить новое, но&nbsp;при этом гармоничное сочетание абзацев, заголовков и&nbsp;отступов между ними.
Идеальный инструмент для тех, кто хочет поиграть со&nbsp;шрифтами, но&nbsp;не&nbsp;до&nbsp;конца понимает, как.</p>
<p>Авторы настолько заботливые, что даже подключают <code>normalize.css</code> при генерации своих стилей, снимая с&nbsp;вас ещё одну головную боль. Благодаря им&nbsp;весь мой дополнительный css для стилизации сайта уложился в&nbsp;сто строк кода и&nbsp;хорошо работает в&nbsp;любых браузерах и&nbsp;на&nbsp;любых экранах.</p>
<p>Со&nbsp;шрифтами всё просто:</p>
<ul>
<li><strong>Open Sans</strong> для текста и&nbsp;<strong>Open Sans Condensed</strong> для заголовков, потому что мне нравится слово «Open»;</li>
<li><strong>Fira Code</strong> для кода, потому что лигатуры (<code>--&gt;</code>, <code>!==</code>)&nbsp;— это красиво.</li>
</ul>
<h2 id="-">Аналитика</h2>
<p>Я&nbsp;долго думал, можно&nbsp;ли собирать аналитику просмотров, или нужно играть в&nbsp;идеальный веб до&nbsp;конца и&nbsp;полностью отказаться от&nbsp;слежки за&nbsp;пользователями. С&nbsp;одной стороны высокая идея, с&nbsp;другой&nbsp;— любопытство.</p>
<p>После непродолжительного исследования пришёл к&nbsp;выводу, что Метрика и&nbsp;Аналитика от&nbsp;поисковых гигантов мне точно не&nbsp;подходят, а&nbsp;вот автор <a href="https://goatcounter.com/">GoatCounter</a> полностью понимает мою боль.</p>
<p><a href="https://www.goatcounter.com/why/">Why I&nbsp;made GoatCounter</a>&nbsp;— отличный пост, где создатель сервиса ответил на&nbsp;все вопросы, которые у&nbsp;меня к&nbsp;нему возникли. Вот, например, замечательный абзац про причины бесплатности начального тарифа:</p>
<blockquote>
<p>I&nbsp;think its important to&nbsp;make the barrier of&nbsp;entry for software like this low as&nbsp;feasible to&nbsp;make actual meaningful inroads to&nbsp;“de-Google-fi” the internet a&nbsp;bit, and make pervasive tracking less common. Making it&nbsp;freely available (for personal use) is&nbsp;part of&nbsp;that.</p>
</blockquote>
<h2 id="-">Содержание</h2>
<p>Самое главное. С&nbsp;формой и&nbsp;техническими вопросами всё ясно, но&nbsp;что&nbsp;же я&nbsp;буду складывать внутрь? Пока план такой:</p>
<ul>
<li>Я&nbsp;довольно часто изучаю штуки из&nbsp;разных областей и&nbsp;хочу описывать эти опыты. Вот, например, описал создание легковесного сайта.</li>
<li>Мне нравится настраивать инструменты и&nbsp;рассказывать об&nbsp;этом.</li>
<li>Есть несколько почтовых рассылок, которые я&nbsp;очень хочу начать переводить, чтобы начать читать.</li>
</ul>
<p>Признаюсь, что был очарован сайтом от&nbsp;ребят из&nbsp;команды <a href="https://100r.co/site/home.html">Hundred Rabbits</a> и&nbsp;захотел такое&nbsp;же, только своё.</p>
<p>Stay tuned!</p>
</article>
</main>
<footer>
2020-11-08
</footer>
<script async
data-goatcounter="https://he4et.goatcounter.com/count"
src="https://gc.zgo.at/count.js"></script>
</body>
</html>

View file

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<title>
typographic linter | oddsquat
</title>
<meta name="description" content="Prettier для текста. Автоматизация рутинной типографики.">
<link rel="preload" href="/fonts/open_sans_condensed-32.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans_condensed-27.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-25.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-24.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-17.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" type="text/css" href="/css/fonts.css">
<link rel="stylesheet" type="text/css" href="/css/typography.css">
<link rel="stylesheet" type="text/css" href="/css/main.css">
</head>
<body>
<div class="stripesContainer">
<div class="stripes">
</div>
</div>
<header>
<nav>
<ul>
<li><a href="/">oddsquat</a></li>
<li><a href="/posts/">
posts</a></li>
<li><a href="/posts/#2020">
2020</a></li>
<li>typographic linter</li>
</ul>
</nav>
</header>
<main>
<article>
<h1 id="-code-style">Типографика как code style</h1>
<h2 id="-">Основы</h2>
<p>Для того, чтобы тексты было удобно читать и&nbsp;неудобно писать, человечество придумало разные правила. С&nbsp;орфографией и&nbsp;пунктуацией всё понятно, но&nbsp;про типографические правила говорят редко.</p>
<p>Типографика&nbsp;— набор правил вёрстки текста, призванных повысить эстетику и&nbsp;читаемость. Именно в&nbsp;таком порядке, потому что люди сначала замечают текст и&nbsp;только потом начинают читать.</p>
<p>Про шрифты, отступы и&nbsp;прочие кернинги в&nbsp;этот раз говорить не&nbsp;будем, потому что мне понравилось крутить ползунки в&nbsp;<a href="https://github.com/KyleAMathews/typography.js">Typography.js</a> и&nbsp;не&nbsp;переживать об&nbsp;эстетике.</p>
<p>Разговор пойдёт про висячие предлоги и&nbsp;прочие тире с&nbsp;кавычками.</p>
<h2 id="-">Проблематика</h2>
<p>Сайты в&nbsp;интернете делятся на&nbsp;два типа: те, кто запаривается за&nbsp;подготовку текстов, и&nbsp;те, кто забивает. <a href="https://meduza.io/">Meduza.io</a> расставляет неразрывные пробелы в&nbsp;текстах новостей, а&nbsp;<a href="https://www.rbc.ru/">rbc.ru</a>&nbsp;— нет. Это не&nbsp;единственное отличие этих новостных порталов, но&nbsp;очень знаковое, как мне кажется.</p>
<h3 id="-">Нужно забить!</h3>
<p>Я&nbsp;прекрасно понимаю тех, кто не&nbsp;запаривается. Часть не&nbsp;знает, часть забивает (<a href="https://vas3k.ru/">Вастрик</a>, например), а&nbsp;у&nbsp;остальных user-generated content, который довольно рискованно автоматически форматировать. Аргументы для каждой группы:</p>
<ul>
<li>И&nbsp;так всё понятно.</li>
<li>Условный Markdown со&nbsp;спецсимволами теряет половину обаяния.</li>
<li>Правильные автоматические кавычки-ёлочки не&nbsp;сделают из&nbsp;комментария в&nbsp;Facebook литературный памятник, зато случайный японский эмотикон может разрушить цивилизацию: <br>
<code>Прости ^_^&quot; Забыл купить &quot;Доширак&quot;</code> превращается в&nbsp;<br>
<code>Прости ^_^« Мы испортили »Твой текст«</code>.</li>
<li>У&nbsp;студентов должен быть отдельный курс по&nbsp;расшифровке смайликов и&nbsp;компилированию C++ из&nbsp;чатов. Бонусный аргумент.</li>
</ul>
<h3 id="-">Нужно запариться!</h3>
<p>Википедия утверждает, что <a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%B8%D0%BF%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D0%BA%D0%B0">типографика</a>&nbsp;— это «свод строгих правил», но&nbsp;список не&nbsp;показывает и&nbsp;ссылок не&nbsp;даёт. Стандартом де-факто в&nbsp;рунете являются правила из&nbsp;<a href="https://www.artlebedev.ru/kovodstvo/sections/62/">§&nbsp;62&nbsp;«Ководства»</a>. С&nbsp;ними или соглашаются, или <a href="https://medium.com/@kapanaga/62-6c664105dd30">громко спорят с&nbsp;некоторыми</a>.</p>
<p>Пусть мне и&nbsp;не&nbsp;нравится полное отсутствие у&nbsp;Лебедева обоснований или ссылок на&nbsp;источники, но&nbsp;самому погружаться в&nbsp;историю вопроса и&nbsp;читать какой-нибудь авторитетный <a href="https://www.artlebedev.ru/izdal/spravochnik-izdatelya-i-avtora-2017/">«Справочник издателя и&nbsp;автора»</a> страшно: в&nbsp;нём 1010&nbsp;страниц.</p>
<p>Авторы, которые хотят радовать читателей с&nbsp;помощью тире, обычно не&nbsp;пишут в&nbsp;блокноте (мне так кажется) или носят свои тексты из&nbsp;блокнота в&nbsp;<a href="https://www.artlebedev.ru/typograf/">Типограф</a> от&nbsp;Артемия Лебедева, а&nbsp;потом назад. Про первых я&nbsp;ничего не&nbsp;знаю, а&nbsp;ко&nbsp;вторым сам отношусь, когда нужно причесать один или два текста.</p>
<p>У&nbsp;Типографа и&nbsp;аналогичных веб-сервисов есть фундаментальные проблемы:</p>
<ul>
<li>Копировать текст из&nbsp;редактора в&nbsp;браузер, а&nbsp;потом назад&nbsp;— неудобно.</li>
<li>Сервис может быть офлайн в&nbsp;неподходящий момент, я&nbsp;тоже.</li>
<li>Сервис может читать мои тексты до&nbsp;публикации.</li>
<li>Некоторые тексты никому нельзя показывать (по&nbsp;разным причинам).</li>
</ul>
<p>Признаюсь, что раньше я&nbsp;ни&nbsp;разу не&nbsp;думал об&nbsp;использовании типографа вне браузера, но&nbsp;как только образовалась перспектива заниматься этим на&nbsp;постоянной основе, стало понятно, что копировать текст из&nbsp;редактора в&nbsp;браузер и&nbsp;назад&nbsp;— не&nbsp;самое оптимальное решение и&nbsp;нужно что-то с&nbsp;этим делать.</p>
<h2 id="-">Решение</h2>
<p>Очевидный вариант&nbsp;— научить текстовый редактор автоматически расставлять неразрывные пробелы, исправлять мелкие опечатки, приводить кавычки к&nbsp;правильному виду, заменять дефисы на&nbsp;тире в&nbsp;нужных местах и&nbsp;многое другое. <br>
Вот <a href="https://marketplace.visualstudio.com/items?itemName=rusnasonov.vscode-typograf">плагин для VS&nbsp;Code</a>.</p>
<p>Мне больше нравится другой подход. Мы&nbsp;уже давно используем линтеры для языков программирования, давайте использовать их&nbsp;и&nbsp;для контента.</p>
<p>Конечно, я&nbsp;не&nbsp;первый, кто так подумал. Плагин выше построен на&nbsp;основе замечательной библиотеки <a href="https://github.com/typograf/typograf">typograf</a> и&nbsp;прямо в&nbsp;README.md файле этого проекта можно найти ссылки на&nbsp;плагины для Babel, Gulp и&nbsp;Grunt.</p>
<p>Библиотека очень гибко настраивается, содержит <a href="https://github.com/typograf/typograf/blob/dev/docs/RULES.ru.md">сотню готовых правил</a> и&nbsp;позволяет легко добавлять кастомные.</p>
<p>В&nbsp;комплекте есть cli-утилита, которая умеет читать файлы, форматировать их&nbsp;и&nbsp;выводить результат в&nbsp;терминал, но&nbsp;почему-то не&nbsp;умеет файл перезаписывать. Поэтому для своих целей я&nbsp;написал небольшой скрипт, который, получив локаль и&nbsp;имя файла, изменяет его содержимое. С&nbsp;правилами тоже пришлось немного поиграть, потому что дефолтные принимают списки за&nbsp;диалоги и&nbsp;ломают markdown-разметку.</p>
<p>Теперь для расстановки всех хитрых значков не&nbsp;нужно ходить в&nbsp;браузер, достаточно просто запустить скрипт, посмотреть с&nbsp;помощью <code>git diff</code> на&nbsp;результат и&nbsp;закоммитить нужные изменения.</p>
<p>Красивые тексты лучше некрасивых.</p>
</article>
</main>
<footer>
2020-11-18
</footer>
<script async
data-goatcounter="https://he4et.goatcounter.com/count"
src="https://gc.zgo.at/count.js"></script>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta
name="description"
content="Redirect to '/posts/#2024'.">
<meta
http-equiv="Refresh"
content="0; URL=/posts/#2024">
<title>
Redirect | oddsquat
</title>
</head>
<body>
<main>
Redirect to
<a
style="color: inherit;"
href="/posts/#2024">
/posts/#2024
</a>
</main>
</body>
</html>

View file

@ -0,0 +1,317 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<title>
selfhosted LLM | oddsquat
</title>
<meta name="description" content="Персональные LLM в docker-контейнере на твоём компьютере.">
<link rel="preload" href="/fonts/open_sans_condensed-32.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans_condensed-27.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-25.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-24.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-17.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" type="text/css" href="/css/fonts.css">
<link rel="stylesheet" type="text/css" href="/css/typography.css">
<link rel="stylesheet" type="text/css" href="/css/main.css">
</head>
<body>
<div class="stripesContainer">
<div class="stripes">
</div>
</div>
<header>
<nav>
<ul>
<li><a href="/">oddsquat</a></li>
<li><a href="/posts/">
posts</a></li>
<li><a href="/posts/#2024">
2024</a></li>
<li>selfhosted LLM</li>
</ul>
</nav>
</header>
<main>
<article>
<h1 id="your-own-private-large-language-models">Your Own Private Large Language Models</h1>
<p>С&nbsp;одной стороны, я&nbsp;сомневаюсь, что Большие Языковые Модели (LLM) смогут однажды эволюционировать в&nbsp;AGI. С&nbsp;другой&nbsp;— я&nbsp;по-настоящему впечатлён тем, что щепотка статистики справляется с&nbsp;написанием текстов лучше меня.</p>
<p>В&nbsp;любом случае, джинна обратно в&nbsp;бутылку уже не&nbsp;вернуть, и&nbsp;все те&nbsp;письма в&nbsp;различные организации, которые я&nbsp;не&nbsp;хочу писать сам, будут теперь написаны месивом из&nbsp;байтиков.</p>
<p>Действительно пугает меня в&nbsp;этой ситуации только то, что флагманом новой эры почему-то стала компания OpenAI, которая, вопреки названию, совершенно не&nbsp;Open.
Множество компаний и&nbsp;людей вписали их&nbsp;продукты в&nbsp;свою рутину и&nbsp;не&nbsp;страшатся такой неподконтрольной зависимости.</p>
<p>Я&nbsp;так не&nbsp;могу. К&nbsp;счастью, я&nbsp;не&nbsp;один такой, и&nbsp;на&nbsp;данный момент уже есть множество альтернативных моделей от&nbsp;разных вендоров. Они отличаются друг от&nbsp;друга качеством, размером, возможностями и&nbsp;лицензиями, так что при желании можно надолго занять себя знакомством с&nbsp;обширным ассортиментом. Например, на&nbsp;портале <a href="https://huggingface.co/">HuggingFace</a>, который можно описать как «GitHub для LLM и&nbsp;всего, что вокруг».</p>
<p>Должен признаться, что очень слабо разбираюсь в&nbsp;параметрах и&nbsp;характеристиках языковых моделей, но&nbsp;оказалось, что для того, чтобы начать, эти знания не&nbsp;так уж&nbsp;и&nbsp;необходимы.</p>
<p>Ниже инструкция, как запустить LLM на&nbsp;своём железе, как упаковать всё это в&nbsp;docker-контейнер, чтобы не&nbsp;размазать случайно по&nbsp;всей файловой системе, как получить совместимый с&nbsp;OpenAI API и&nbsp;как потом этим пользоваться.</p>
<hr>
<ul>
<li><a href="#setup">Установка и&nbsp;настройка</a><ul>
<li><a href="#setup-ollama">Установка Ollama</a></li>
<li><a href="#setup-model">Загрузка модели и&nbsp;диалог с&nbsp;ней</a></li>
<li><a href="#custom-model">Кастомные модели и&nbsp;их&nbsp;тонкая настройка</a></li>
</ul>
</li>
<li><a href="#usage">Использование</a><ul>
<li><a href="#fake-open-ai">Мимикрия под API от&nbsp;OpenAI</a></li>
<li><a href="#ollama-nvim">Интеграция с&nbsp;NeoVim</a></li>
</ul>
</li>
<li><a href="#update-delete">Обновление и&nbsp;удаление</a></li>
<li><a href="#performance">Производительность</a></li>
<li><a href="#why">Зачем всё это нужно?</a></li>
</ul>
<hr>
<h2 id='setup'>
Установка и&nbsp;настройка
</h2>
<p>Существует несколько продуктов, которые стараются избавить пользователя от&nbsp;головной боли и&nbsp;возни с&nbsp;инфраструктурой. Мне понятнее всего оказался проект <a href="https://ollama.ai/">Ollama</a>, с&nbsp;ним мы&nbsp;и&nbsp;будем экспериментировать.</p>
<p>Кроме бинарников для Linux и&nbsp;MacOS, они <a href="https://ollama.ai/blog/ollama-is-now-available-as-an-official-docker-image">предоставляют</a> официальный <a href="https://hub.docker.com/r/ollama/ollama">docker-образ</a>, работу с&nbsp;которым я&nbsp;и&nbsp;опишу.</p>
<p>Использование docker-контейнеров, к&nbsp;сожалению, слегка усложняет взаимодействие с&nbsp;Ollama, так что большая часть текста и&nbsp;кода в&nbsp;этом посте посвящены решению проблем, которые, по&nbsp;сути, я&nbsp;придумал себе сам.</p>
<h3 id='setup-ollama'>
Установка Ollama
</h3>
<p>Для создания и&nbsp;первого запуска контейнера нужно выполнить команду:</p>
<pre><code>docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama</code></pre><p>Счастливые владельцы видеокарт от&nbsp;Nvidia могут установить <a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installation">Nvidia container toolkit</a> и&nbsp;активировать поддержку GPU с&nbsp;помощью флага <code>--gpus=all</code>.</p>
<p>После создания запускать и&nbsp;останавливать контейнер <code>ollama</code> можно так:</p>
<pre><code>docker start ollama
docker stop ollama</code></pre><p>Контейнер предоставляет доступ к&nbsp;<a href="https://github.com/jmorganca/ollama/blob/main/docs/api.md">Ollama API</a> на&nbsp;11434&nbsp;порту, а&nbsp;также позволяет устанавливать и&nbsp;общаться с&nbsp;установленными LLM через терминал.</p>
<h3 id='setup-model'>
Загрузка модели и&nbsp;диалог с&nbsp;ней
</h3>
<p>Ollama позволяет запускать любые GGUF, PyTorch или Safetensors модели (что&nbsp;бы это ни&nbsp;значило), но&nbsp;самый простой путь&nbsp;— загрузка моделей из&nbsp;специальной <a href="https://ollama.ai/library">библиотеки</a>.</p>
<p>Для того, чтобы скачать модель и&nbsp;начать с&nbsp;ней диалог, нужно выполнить команду:</p>
<pre><code>docker exec -it ollama ollama run mistral</code></pre><p>Вместо <code>mistral</code> от&nbsp;одноимённой комании можно выбрать любую другую модель из&nbsp;библиотеки, например, легкую <code>phi</code> от&nbsp;Microsoft Research.</p>
<p>Кроме <code>run</code> доступны также <code>list</code>, <code>pull</code> и&nbsp;<code>rm</code> для просмотра списка, скачивания и&nbsp;удаления моделей соответственно.</p>
<p>Чтобы не&nbsp;писать такие длинные заклинания каждый раз, я&nbsp;добавил в&nbsp;<code>.zshrc</code> пару алиасов:</p>
<pre><code>alias summonable=&#39;docker exec -it ollama ollama list&#39;
alias summon=&#39;clear &amp;&amp; docker exec -it ollama ollama run&#39;</code></pre><p>Теперь можно смотреть на&nbsp;список установленных моделей и&nbsp;запускать диалог с&nbsp;выбранной:</p>
<pre><code>summonable
summon phi</code></pre><h3 id='custom-model'>
Кастомные модели и&nbsp;их&nbsp;тонкая настройка
</h3>
<p>Ollama позволяет на&nbsp;основе существующих создавать производные модели с&nbsp;заранее определёнными инструкциями или параметрами. Для этого нужно создать специальный файл, в&nbsp;котором указана родительская модель и&nbsp;определены желаемые значения параметров. Подробнее о&nbsp;формате этих файлов можно прочесть в&nbsp;документации: <a href="https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md">Modelfile</a>.</p>
<blockquote>
<p>В&nbsp;какой-то момент Ollama Web UI&nbsp;превратился в&nbsp;Open WebUI, а&nbsp;OllamaHub прекратил существовать. Все ссылки в&nbsp;следующем абзаце больше не&nbsp;представляют какой-либо ценности.</p>
</blockquote>
<p>Чтобы посмотреть, как должен выглядеть Modelfile, можно посетить <a href="https://ollamahub.com/">OllamaHub</a> от&nbsp;разработчиков стороннего <a href="https://github.com/ollama-webui/ollama-webui/">Ollama Web UI</a>. На&nbsp;сайте есть <a href="https://ollamahub.com/m/smoothbrainape/hu-tao:latest">примеры очень тонкой настройки множества параметров</a> модели для соответствия образу конкретного персонажа, но&nbsp;в&nbsp;качестве образца я&nbsp;буду использовать небольшой <a href="https://ollamahub.com/m/kamjin/english-teacher:latest">English Teacher Modelfile</a>:</p>
<h4 id="englishteacher-modelfile">EnglishTeacher.Modelfile</h4>
<pre><code>FROM llama2
SYSTEM &quot;&quot;&quot;
I want you to act as a English teacher.
Your main responsibility will be to instruct me in all aspects of English, including grammar, vocabulary, reading, writing and speaking.
You should always take the initiative to correct my mistakes in grammar and vocabulary, and you can give me two or three examples at any appropriate time to help me understand better.
&quot;&quot;&quot;</code></pre><p>Вообще, для загрузки кастомной модели достаточно выполнить команду <code>create</code>, но&nbsp;в&nbsp;случае использования Ollama внутри docker-контейнера возникает необходимость каким-то образом файл с&nbsp;моделью в&nbsp;этот контейнер передать.</p>
<p>Для решения этой проблемы я&nbsp;набросал небольшой bash-скрипт:</p>
<h4 id="applymodelfile-bash">applyModelfile.bash</h4>
<pre><code class="language-bash">#!/bin/bash
containerId=$(docker ps | grep ollama/ollama | cut -d&#39; &#39; -f1)
echo &quot;Container ID:&quot;
echo $containerId
echo &quot;&quot;
if [ -z &quot;$containerId&quot; ]; then
echo &quot;Container does not exist.&quot;
exit 1
fi
modelName=$1
echo &quot;Model name:&quot;
echo $modelName
echo
sourcePath=&quot;./models/${modelName}.Modefile&quot;
targetPath=&quot;/home/${modelName}.Modefile&quot;
if ! test -f $sourcePath; then
echo &quot;File does not exist.&quot;
echo $sourcePath
exit 1
fi
docker cp $sourcePath &quot;${containerId}:${targetPath}&quot;
clear
docker exec -it ollama ollama create $modelName -f $targetPath</code></pre>
<p>Этот скрипт нужно поместить по&nbsp;соседству с&nbsp;директорией <code>models</code> и&nbsp;сделать исполняемым с&nbsp;помощью <code>chmod +x applyModelfile.bash</code>.
Должна получиться примерно такая структура:</p>
<pre><code>├── models
│ └── EnglishTeacher.Modefile
└── applyModelfile.bash</code></pre><p>После этого модель можно загрузить в&nbsp;контейнер и&nbsp;начать с&nbsp;ней диалог:</p>
<pre><code>./applyModelfile.bash EnglishTeacher
summon EnglishTeacher</code></pre><h2 id='usage'>
Использование
</h2>
<p>Разговоры с&nbsp;галлюцинирующим искусственным интеллектом в&nbsp;терминале&nbsp;— это, конечно, волшебно, но&nbsp;потенциал больших языковых моделeй по-настоящему раскрывается, когда они начинают портить данные в&nbsp;соседних приложениях!</p>
<p>В&nbsp;<a href="https://github.com/jmorganca/ollama#community-integrations">GitHub-репозитории Ollama</a> можно найти ссылки на&nbsp;множество веб-интерфейсов, библиотек и&nbsp;плагинов для текстовых редакторов и&nbsp;прочих Obsidian&#39;ов.</p>
<p>Не&nbsp;ручаюсь за&nbsp;весь список, но&nbsp;расскажу про то, с&nbsp;чем экспериментировал сам.</p>
<h3 id='fake-open-ai'>
Мимикрия под API от&nbsp;OpenAI
</h3>
<blockquote>
<p>В&nbsp;какой-то момент <a href="https://ollama.com/blog/openai-compatibility">в&nbsp;Ollama появилась поддержка совместимости с&nbsp;форматом API от&nbsp;OpenAI</a> и&nbsp;этот раздел потерял актуальность.</p>
</blockquote>
<p>API Ollama используется в&nbsp;меньшем числе продуктов, чем API от&nbsp;OpenAI. К&nbsp;счастью, это не&nbsp;проблема: с&nbsp;помощью прокси-прослойки под названием <a href="https://github.com/BerriAI/litellm">LiteLLM</a> можно сделать их&nbsp;совместимыми. Инструкция по&nbsp;установке и&nbsp;использованию в&nbsp;общем случае есть в&nbsp;репозитории и&nbsp;довольно тривиальна, но&nbsp;мне опять потребовалось немного кода, чтобы заставить их&nbsp;работать вместе на&nbsp;моих условиях.</p>
<p>Я&nbsp;хотел, чтобы LiteLLM-прокси и&nbsp;Ollama работали на&nbsp;разных компьтерах, и&nbsp;не&nbsp;хотел ставить pip-пакеты в&nbsp;систему. В&nbsp;результате родилось решение из&nbsp;docker-файла с&nbsp;хаками и&nbsp;скрипта, который в&nbsp;нём запускается. Я&nbsp;не&nbsp;специалист в&nbsp;написании docker-файлов, так что уверен в&nbsp;неоптимальности финального решения. Точно можно и&nbsp;нужно обойтись без <code>run --net=host</code> и&nbsp;отдельного скрипта, например.</p>
<p>Несмотря на&nbsp;костыльность связки, она справляется со&nbsp;своей задачей:</p>
<h4 id="dockerfile">Dockerfile</h4>
<pre><code class="language-Dockerfile">FROM python:3.10
COPY startProxy.sh /usr/src/app/startProxy.sh
RUN chmod +x /usr/src/app/startProxy.sh
WORKDIR /usr/src/app
# Prevent ollama run attempt
RUN echo &#39;#!/bin/sh\necho &quot;$1&quot;&#39; &gt; /usr/bin/ollama &amp;&amp; \
chmod +x /usr/bin/ollama
CMD [&quot;./startProxy.sh&quot;]</code></pre>
<h4 id="startproxy-sh">startProxy.sh</h4>
<pre><code class="language-sh">#!/bin/bash
pip install litellm
litellm --model ollama/mistral --api_base http://ollama.internal:11434 --drop_params</code></pre>
<p>Собрать и&nbsp;запустить docker-контейнер можно с&nbsp;помощью этих двух команд:</p>
<pre><code class="language-sh">docker build -t diy-ollama-proxy .
docker run --net=host diy-ollama-proxy</code></pre>
<p>После запуска вы&nbsp;получите API, который совместим с&nbsp;API от&nbsp;OpenAI и&nbsp;доступен по&nbsp;адресу <code>http://localhost:8000/</code>.</p>
<h3 id='ollama-nvim'>
Интеграция с&nbsp;NeoVim
</h3>
<p>Языковые модели отлично умеют взаимодействовать с&nbsp;текстом, так что использование их&nbsp;в&nbsp;текстовом редакторе кажется разумной идеей.</p>
<p>Мне не&nbsp;очень нравится идея Copilot, который зачем-то постоянно подсовывает тебе странные куски кода. Я&nbsp;пробовал использовать <a href="https://codeium.com/">Codeium</a> в&nbsp;ручном режиме, но&nbsp;оказалось, что странные куски кода по&nbsp;запросу мне тоже не&nbsp;очень нужны. Гораздо более привлекательной мне кажется возможность выделить существующий фрагмент текста или кода и&nbsp;попросить бездушную машину что-нибудь с&nbsp;ним сделать: упростить, дополнить, изменить или даже перевести с&nbsp;одного языка на&nbsp;другой. Идеальным для такого подхода оказался <a href="https://github.com/nomnivore/ollama.nvim">плагин ollama.nvim</a>.</p>
<p>Кроме того, что он&nbsp;поддерживает кастомные промпты (в&nbsp;том числе интерактивные), он&nbsp;позволил мне обращаться к&nbsp;LLM, которая запущена на&nbsp;другом компьютере в&nbsp;локальной сети (для удобства я&nbsp;указал его адрес в&nbsp;<code>/etc/hosts/</code>).</p>
<p>Установка и&nbsp;настройка с&nbsp;использованием пакетного менеджера lazy.nvim выглядит примерно так:</p>
<pre><code>{
&#39;nomnivore/ollama.nvim&#39;,
dependencies = {
&#39;nvim-lua/plenary.nvim&#39;,
},
cmd = { &#39;Ollama&#39;, &#39;OllamaModel&#39; },
keys = {
{
&#39;&lt;leader&gt;j&#39;,
&#39;:Ollama&lt;CR&gt;&#39;,
desc = &#39;Ollama Menu&#39;,
mode = { &#39;v&#39; },
},
{
&#39;&lt;leader&gt;j&#39;,
&quot;:lua require(&#39;ollama&#39;).prompt(&#39;Generate_Code&#39;)&lt;cr&gt;&quot;,
desc = &#39;Ollama Code Generation&#39;,
mode = { &#39;n&#39; },
},
},
opts = {
model = &#39;mistral&#39;,
url = &#39;http://ollama.internal:11434&#39;, -- see /etc/hosts
prompts = {
Ask_About_Code = false,
Simplify_Code = false,
Improve_Text = {
prompt = &#39;Check the following sentence for grammar and clarity: &quot;$sel&quot;.\nRewrite it for better readability while maintaining its original meaning.&#39;,
extract = false,
action = &#39;replace&#39;,
},
Modify_Text = {
prompt = &#39;Modify this text in the following way: $input\n\n```$sel```&#39;,
extract = false,
action = &#39;replace&#39;,
},
Use_Selection_as_Prompt = {
prompt = &#39;$sel&#39;,
extract = false,
action = &#39;replace&#39;,
},
},
},
},</code></pre><p>В&nbsp;этом конфиге я&nbsp;выключил несколько дефолтных промптов и&nbsp;добавил несколько своих:</p>
<ul>
<li><code>Improve_Text</code> заменяет выделенный кусок текста «улучшенным».</li>
<li><code>Modify_Text</code> является аналогом встроенного <code>Modify_Code</code> и&nbsp;позволяет делать с&nbsp;выделенным текстом всякие глупости. Например, заменить все числа на&nbsp;слова.</li>
<li><code>Use_Selection_as_Prompt</code> просто заменяет выделенный текст на&nbsp;ответ от&nbsp;LLM.</li>
</ul>
<p>В&nbsp;итоге получается два сценария использования, оба доступны по&nbsp;<code>&lt;leader&gt; + j</code>:</p>
<ul>
<li>В&nbsp;<code>normal</code> mode плагин спрашивает меня, какой код мне нужен, и&nbsp;вставляет его.</li>
<li>В&nbsp;<code>visual</code> mode появляется меню действий над выделенным текстом.</li>
</ul>
<p>Как видно из&nbsp;конфига, я&nbsp;использую только <code>mistral</code>, но&nbsp;можно указать модель для кажого промпта и&nbsp;делегировать, например, манипуляции над кодом <code>codellama</code>, а&nbsp;операции над текстом&nbsp;<code>llama2</code>.</p>
<p>Возможность добавления кастомных промптов позволяет в&nbsp;будущем реализовать новые сценарии или вынести повторяющиеся действия в&nbsp;отдельный пункт меню или даже на&nbsp;отдельный шорткат.</p>
<h2 id='update-delete'>
Обновление и&nbsp;удаление
</h2>
<p>Для обновления и&nbsp;удаления моделей можно использовать команды <code>pull</code> и&nbsp;<code>rm</code>:</p>
<pre><code>docker exec -it ollama ollama pull mixtral
docker exec -it ollama ollama rm mistral</code></pre><p>Я&nbsp;знаю, что для обновления и&nbsp;удаления docker-образов и&nbsp;docker-контейнеров тоже есть специальные команды (это тоже <code>pull</code> и&nbsp;<code>rm</code>), но&nbsp;каждый раз ленюсь в&nbsp;этом разобраться, просто сношу всё с&nbsp;помощью утилиты <a href="https://github.com/TomasTomecek/sen">sen</a> и&nbsp;разворачиваю нужное заново.</p>
<h2 id='performance'>
Производительность
</h2>
<p>Для эксплуатации LLM требуется гораздо меньше ресурсов, чем для её&nbsp;обучения. Запустить 7b-модель средней тупости можно практически на&nbsp;любом CPU и&nbsp;8&nbsp;GB&nbsp;RAM, но&nbsp;нагрузка на&nbsp;систему и&nbsp;скорость генерации ответов часто будут далеки от&nbsp;комфортных значений.</p>
<p>Например, на&nbsp;моём немолодом Intel Core i7-10510U @ 8x 4.9GHz неаккуратный запрос к&nbsp;<code>llama2</code> может заставить систему шуршать вентиляторами пару-тройку минут. При этом <code>phi</code> на&nbsp;этом&nbsp;же процессоре способна отвечать на&nbsp;какие-нибудь не&nbsp;очень сложные вопросы практически мгновенно.</p>
<p>К&nbsp;счастью, у&nbsp;меня случайно завалялся MacBook на&nbsp;процессоре M1&nbsp;и&nbsp;он&nbsp;уже показывает куда более впечатляющие результаты. <code>Mistral</code> даже на&nbsp;непростые запросы отвечает за&nbsp;считанные секунды, а&nbsp;в&nbsp;режиме чата токены вылетают на&nbsp;экран заметно быстрее, чем в&nbsp;веб-интерфейсе ChatGPT.</p>
<p>Неприятным открытием стало то, что docker-версия Ollama на&nbsp;MacOS выполняется заметно медленнее (от&nbsp;3&nbsp;до&nbsp;5&nbsp;раз, если верить ощущениям), чем нативная. Возможно, всё дело в&nbsp;том, что я&nbsp;как-то неправильно настроил docker или приложение в&nbsp;контейнере нужно запускать с&nbsp;какими-нибудь специальными флагами для максимальной утилизации ресурсов. В&nbsp;любом случае, к&nbsp;порядку на&nbsp;этом ноутбуке я&nbsp;отношусь гораздо менее трепетно, поэтому просто установил и&nbsp;использую приложение с&nbsp;сайта Ollama.</p>
<h2 id='why'>
Зачем всё это нужно?
</h2>
<p>Конечно, ChatGPT умнее и&nbsp;умеет из&nbsp;коробки гораздо больше.<br>
Конечно, ChatGPT требует меньше телодвижений для использования.<br>
Конечно, самые умные модели требуют внушительных ресурсов, ведь для запуска нашумевшей <a href="https://ollama.ai/library/mixtral">mixtral</a> или аналогичной модели нужно иметь 48&nbsp;Gb&nbsp;оперативной памяти.<br>
Конечно, сидя в&nbsp;кафе задать вопрос Bard от&nbsp;Google гораздо проще, чем достучаться до&nbsp;модели в&nbsp;закрытом ноутбуке, который остался дома.<br></p>
<p>Я&nbsp;всё это прекрасно понимаю, но&nbsp;ничего из&nbsp;этого не&nbsp;стоит того, чтобы добровольно ставить себя в&nbsp;зависимость от&nbsp;монополистов с&nbsp;их&nbsp;закрытыми чёрными ящиками.</p>
<p>Даже если закрыть глаза на&nbsp;все идеологические вопросы, то&nbsp;любая локальная LLM отличается от&nbsp;любого облачного провайдера тем, что:</p>
<ul>
<li>Может работать в&nbsp;оффлайне.</li>
<li>Не&nbsp;хранит и&nbsp;не&nbsp;сливает вашу переписку.</li>
<li>Не&nbsp;станет завтра тупее, чем есть сегодня.</li>
<li>Не&nbsp;забанит тебя за&nbsp;возмутивший кого-то там запрос.</li>
<li>Обладает тем уровнем цензуры, который выбрал ты&nbsp;сам.</li>
</ul>
<p>Я&nbsp;искренне рад, что для доступа даже к&nbsp;передовым технологиям, всё ещё не&nbsp;обязательно поступаться своей приватностью и&nbsp;своими свободами.</p>
<hr>
<p>Этот пост написан без использования LLM =)</p>
</article>
</main>
<footer>
2024-01-15
</footer>
<script async
data-goatcounter="https://he4et.goatcounter.com/count"
src="https://gc.zgo.at/count.js"></script>
</body>
</html>

View file

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<title>
wrapped bw | oddsquat
</title>
<meta name="description" content="Превращаем fully-featured Bitwarden command-line interface в удобный.">
<link rel="preload" href="/fonts/open_sans_condensed-32.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans_condensed-27.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-25.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-24.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-17.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" type="text/css" href="/css/fonts.css">
<link rel="stylesheet" type="text/css" href="/css/typography.css">
<link rel="stylesheet" type="text/css" href="/css/main.css">
</head>
<body>
<div class="stripesContainer">
<div class="stripes">
</div>
</div>
<header>
<nav>
<ul>
<li><a href="/">oddsquat</a></li>
<li><a href="/posts/">
posts</a></li>
<li><a href="/posts/#2024">
2024</a></li>
<li>wrapped bw</li>
</ul>
</nav>
</header>
<main>
<article>
<h1 id="-bitwarden-cli-fzf-">Интеграция Bitwarden CLI с&nbsp;fzf и&nbsp;буфером обмена</h1>
<p>Менеджер паролей&nbsp;— это специальное приложение, которое помогает делать вид, что я&nbsp;помню разные пароли для разных аккаунтов, а&nbsp;не&nbsp;ввожу везде один и&nbsp;тот&nbsp;же. Мне нравится Bitwarden: открытый исходный код, возможность поднять собственный сервер, клиенты под разные устройства и&nbsp;расширения под разные браузеры.</p>
<p>Самым удобным, внезапно, оказался клиент для Android, который не&nbsp;заставляет меня каждый раз вводить 12+ знаков мастер-пароля (такую длину требуют Bitwarden и&nbsp;здравый смысл), а&nbsp;может быть разблокирован с&nbsp;помощью биометрии. Похожего удобства захотелось достичь и&nbsp;на&nbsp;Linux.</p>
<h2 id="-cli">Дикий CLI</h2>
<p>На&nbsp;ноутбуке я&nbsp;использую <a href="https://bitwarden.com/help/cli/">Bitwarden CLI</a>. Это powerful, fully-featured tool, которым на&nbsp;практике оказалось не&nbsp;слишком удобно пользоваться, если ты&nbsp;человек.</p>
<h3 id="-">Ключи от&nbsp;ключей</h3>
<blockquote>
<p>You are responsible for maintaining your session key.</p>
</blockquote>
<p>Bitwarden CLI поддерживает <a href="https://bitwarden.com/help/cli/#using-a-session-key">механизм сессий</a>, который призван избавить пользователя от&nbsp;бесконечного ввода мастер-пароля. Приложение позволяет разблокировать хранилище и&nbsp;получить временный сессионный ключ, который можно либо хранить в&nbsp;беззащитной переменной окружения, либо прикладывать к&nbsp;каждому запросу вручную.</p>
<p>По&nbsp;сути своей, сессионный ключ отличается от&nbsp;мастер-пароля тем, что его можно моментально деактивировать, но&nbsp;совершенно невозможно запомнить, а, значит, нужно где-то хранить. Хочется делать это удобно и&nbsp;безопасно, а&nbsp;не&nbsp;в&nbsp;общедоступной переменной окружения.</p>
<h3 id="-stdout">Пароли в&nbsp;stdout</h3>
<blockquote>
<p>The get command can only return one result, so&nbsp;you should use specific search terms. If&nbsp;multiple results are found, the CLI will return an&nbsp;error.</p>
</blockquote>
<p>Где-то в&nbsp;этот момент чтения документации я&nbsp;окончательно начал подозревать, что официальный CLI предназначен для скриптов: всё строго, никакого автодополнения, никакого интерактивного поиска, а&nbsp;пароли лаконично вываливаются в&nbsp;стандартный вывод терминала, откуда их&nbsp;ещё нужно как-то переправить в&nbsp;место назначения.</p>
<h2 id="-cli">Приручение CLI</h2>
<p>Может показаться, что я&nbsp;ругаюсь, но&nbsp;отсутствие удобств и&nbsp;излишеств в&nbsp;официальном CLI&nbsp;— это хорошо:</p>
<ul>
<li>Отсутствие фич всегда приятнее, чем кривые фичи.</li>
<li>Минимализм упрощает жизнь мейнтейнерам.</li>
<li>Минимализм повышает надёжность.</li>
<li>Меньше сторонних зависимостей.</li>
<li>Стандартные интерфейсы идеально подходят для автоматизации.</li>
</ul>
<p>Идея сделать Bitwarden CLI удобнее, разумеется, пришла в&nbsp;голову не&nbsp;только мне, так что на&nbsp;GitHub предсказуемо быстро нашёлся <a href="https://gist.github.com/loeschzwerg/c2b9d0b50f712a026aa6454af3b58598">скрипт-обёртка</a> от&nbsp;<a href="https://github.com/loeschzwerg">@loeschzwerg</a>. Этот ZSH-скрипт менее требователен к&nbsp;пользователю и&nbsp;позволяет в&nbsp;случае, когда под пользовательский поисковый запрос подходит несколько аккаунтов, выбрать нужный из&nbsp;списка с&nbsp;помощью fzf и&nbsp;автоматически скопировать логин, пароль и&nbsp;даже TOTP в&nbsp;буфер обмена.</p>
<p>К&nbsp;сожалению, найденный скрипт никак не&nbsp;решал проблему управления сессиями, так что я&nbsp;решил его немного доработать, избавив заодно от&nbsp;избытка многоточий в&nbsp;интерфейсе.</p>
<h3 id="-">«Безопасное» хранение сессионного ключа</h3>
<p>Как я&nbsp;писал выше, мне нравится подход Android-клиента: нужно один раз ввести свой невероятно длинный мастер-пароль, после чего можно разблокировать хранилище отпечатком пальца. В&nbsp;ходе непродолжительных размышлений я&nbsp;решил, что самое простое и&nbsp;надёжное подобие для приложения в&nbsp;терминале&nbsp;— один раз получить сессионный ключ и&nbsp;сохранить его в&nbsp;файл, который будет доступен для чтения только пользователю <code>root</code> и&nbsp;недоступен любым другим приложениям, запущенным от&nbsp;имени текущего пользователя.</p>
<p>Приятный бонус для владельцев биометрических сканеров: они отлично интегрируются с&nbsp;утилитой <code>sudo</code>.</p>
<p>В&nbsp;результате скрипт обогатился двумя функциями и&nbsp;одной проверкой:</p>
<pre><code class="language-zsh">local sessionfile=&quot;$HOME/.bitwarden_session&quot;
get_saved_sessionkey () {
sudo touch $sessionfile
echo $(sudo cat $sessionfile)
}
save_sessionkey () {
local sessionkey=$1
sudo chmod 600 $sessionfile
sudo sh -c &quot;echo $sessionkey &gt; $sessionfile&quot;
}</code></pre>
<pre><code>local sessionkey=$(get_saved_sessionkey)
if [[ -z $sessionkey ]] ; then
# Get and save a new session key
sessionkey=$(bw unlock --raw)
save_sessionkey $sessionkey
else
echo &quot;Using the existing session key from &#39;$sessionfile&#39;.&quot;
fi</code></pre><p>При первом запуске сессионный ключ, полученный после ввода мастер-пароля, записывается в&nbsp;файл, который после выполнения команды <code>chmod 600</code> становится недоступен для чтения никому, кроме суперпользователя:</p>
<pre><code>~ » ls -lah
...
-rw-------. 1 root root 89 Jul 24 22:15 .bitwarden_session
...
~ » less .bitwarden_session
.bitwarden_session: Permission denied</code></pre><p>Парольный менеджер и&nbsp;скрипт-обёртка запускаются от&nbsp;имени текущего пользователя, повышение привилегий требуется только в&nbsp;момент записи и&nbsp;чтения сессионного ключа.</p>
<p>Деактивировать сохранённый ключ можно с&nbsp;помощью команды <code>bw lock</code>.
К&nbsp;сожалению, я&nbsp;так и&nbsp;не&nbsp;понял, как с&nbsp;помощью утилиты <code>bw</code> можно проверить, валиден&nbsp;ли ключ, так что после деактивации придётся удалить файл <code>~/.bitwarden_session</code> вручную, иначе скрипт так и&nbsp;будет подставлять протухший сохранённый ключ, а&nbsp;<code>bw</code> будет каждый раз игнорировать его и&nbsp;настойчиво спрашивать мастер-пароль.</p>
<p><strong>Update [2026-03-29]</strong>:
Нормального способа проверить валидность сессионного ключа <a href="https://github.com/bitwarden/clients/issues/9254">всё ещё нет</a>,
но&nbsp;я&nbsp;научил утилиту удалять файл с&nbsp;протухшим ключом по&nbsp;косвенным признакам.</p>
<h2 id="-">Применять с&nbsp;осторожностью</h2>
<p>Взаимодействие с&nbsp;менеджером паролей выглядит для меня теперь примерно так:</p>
<pre><code>~ » bwc github
[sudo] password for $USER:
Using the existing session key from &#39;/home/$USER/.bitwarden_session&#39;.
Searching for &#39;github&#39;...
abcdefgh-ijkl-mnop-qrst-uvwxyz123456
github.com
Username &#39;username&#39; copied to clipboard.
[Press any key to copy the password]
Password copied to clipboard.</code></pre><p>Финальный вариант скрипта можно найти в&nbsp;репозитории <a href="https://github.com/He4eT/fuzzy-bitwarden-clipboard">He4eT/fuzzy-bitwarden-clipboard</a>.</p>
<p>Настоятельно рекомендую читать любой код перед тем, как запускать&nbsp;его. Особенно в&nbsp;тех случаях, когда речь идёт о&nbsp;настолько чувствительных данных.</p>
<p><strong>Важно!</strong> На&nbsp;системах без шифрования диска все эти танцы с&nbsp;правами на&nbsp;доступ к&nbsp;файлу не&nbsp;несут никакой пользы и&nbsp;превращают затею в&nbsp;увлекательный цирк.</p>
<p>Нельзя исключать, что я&nbsp;что-то совершенно неправильно понимаю в&nbsp;принципах работы системы прав доступа в&nbsp;Linux и&nbsp;совершил какие-нибудь грубейшие, с&nbsp;точки зрения настоящих специалистов по&nbsp;информационной безопасности, ошибки. Пожалуйста, сообщите, если я&nbsp;где-то неправ.</p>
<p>Нужно помнить, что такое упрощение жизни ведёт к&nbsp;новым рискам: теперь любой, кто знает ваш пароль для учётной записи системного пользователя и&nbsp;имеет доступ к&nbsp;компьютеру, будет также иметь доступ и&nbsp;ко&nbsp;всем паролям, сохранённым в&nbsp;Bitwarden.</p>
<p>Пользуйтесь с&nbsp;осторожностью и/или храните свои пароли в&nbsp;надёжных местах =)</p>
</article>
</main>
<footer>
2024-07-27
</footer>
<script async
data-goatcounter="https://he4et.goatcounter.com/count"
src="https://gc.zgo.at/count.js"></script>
</body>
</html>

View file

@ -0,0 +1,428 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<title>
encrypted XMPP | oddsquat
</title>
<meta name="description" content="Secure and private messaging with XMPP and OMEMO encryption.">
<link rel="preload" href="/fonts/open_sans_condensed-32.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans_condensed-27.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-25.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-24.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-17.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" type="text/css" href="/css/fonts.css">
<link rel="stylesheet" type="text/css" href="/css/typography.css">
<link rel="stylesheet" type="text/css" href="/css/main.css">
</head>
<body>
<div class="stripesContainer">
<div class="stripes">
</div>
</div>
<header>
<nav>
<ul>
<li><a href="/">oddsquat</a></li>
<li><a href="/posts/">
posts</a></li>
<li><a href="/posts/#2026">
2026</a></li>
<li>encrypted XMPP</li>
</ul>
</nav>
</header>
<main>
<article>
<h1 id="end-to-end-encryption-inxmpp-with-omemo">End-to-End Encryption in&nbsp;XMPP with OMEMO</h1>
<p>I&nbsp;find it&nbsp;funny that twenty years ago I&nbsp;was already trying
to&nbsp;get people to&nbsp;switch to&nbsp;XMPP.</p>
<p>For a&nbsp;long time, ICQ was extremely popular around me,
but the proprietary messenger kept breaking things for people
using alternative clients, which was quite annoying.
After yet another round of&nbsp;this pointless battle
I&nbsp;realized clearly that I&nbsp;prefer protocols over services.</p>
<p>I&nbsp;didnt have much success back then,
but fortunately, XMPP (and I&nbsp;hope I&nbsp;have too)
has continued moving forward over the past two decades.
It&nbsp;has developed slowly, sometimes awkwardly, but steadily.</p>
<p>Here, I&nbsp;wont talk about why XMPP is&nbsp;great or&nbsp;how it&nbsp;works.
You can check
<a
href='https://contrapunctus.codeberg.page/the-quick-and-easy-guide-to-xmpp.html'
target='_blank'>
this guide</a>
(one of&nbsp;many) and Id rather not write another one.
In&nbsp;this post,
I&nbsp;want to&nbsp;focus specifically on&nbsp;end-to-end encryption
and the practical aspects of&nbsp;using it.</p>
<h2 id="short-glossary">Short Glossary</h2>
<p><strong>End-to-end encryption</strong> is&nbsp;a&nbsp;way
to&nbsp;keep your chats truly private.<br>
Only you and the person youre messaging can read the messages.
Not even the server owner has the keys
needed to&nbsp;decrypt or&nbsp;modify them.</p>
<p><strong>XMPP</strong> is&nbsp;an&nbsp;extensible protocol for instant messaging.
Its open, decentralized, and mature.</p>
<p><strong>OMEMO</strong> is&nbsp;a&nbsp;<a
href='https://omemo.top/'
target='_blank'>widely supported</a>
XMPP Extension Protocol (XEP)
for secure multi-client end-to-end encryption.
You can read more about
it&nbsp;on&nbsp;a&nbsp;<a
href='https://conversations.im/omemo/'
target='_blank'>dedicated page by&nbsp;Daniel Gultsch</a>.</p>
<p><strong>Client</strong>, in&nbsp;this post,
means a&nbsp;specific instance
of&nbsp;an&nbsp;XMPP application on&nbsp;a&nbsp;user device.
<br>OMEMO-related documentation uses the term Device,
but I&nbsp;find it&nbsp;potentially confusing:
in&nbsp;practice, a&nbsp;single physical device
can run multiple independent clients.</p>
<h2 id="basic-concepts">Basic Concepts</h2>
<p>This section introduces some basics of&nbsp;end-to-end encryption.</p>
<p>If&nbsp;youre already familiar with the concepts and terminology,
you can skip ahead to&nbsp;<a
href='#practical-aspects-of-omemo-and-xmpp'>how end-to-end encryption
affects the XMPP user experience</a>,
or&nbsp;jump straight to&nbsp;the <a
href='#step-by-step-guide'>step-by-step workflow</a> I&nbsp;personally use.</p>
<h3 id="trade-offs-between-safety-and-convenience">Trade-offs Between Safety and Convenience</h3>
<p>Unfortunately, things that are truly secure are rarely convenient.
They often require some initial efforts
and a&nbsp;bit of&nbsp;ongoing attention.</p>
<p>Telegram, which used to&nbsp;be&nbsp;a&nbsp;benchmark for messenger usability
before its long dive into enshitification,
really draws the line between convenience and security.
Regular chats are easy and flexible,
but “secret” chats come with a&nbsp;full set of&nbsp;limitations:
theyre one-on-one only,
cant be&nbsp;synced to&nbsp;another device,
arent available on&nbsp;desktop at&nbsp;all,
and so&nbsp;on.</p>
<p>All commercial so-called “secure” messengers, like Signal or&nbsp;WhatsApp,
end up&nbsp;with pretty similar limitations,
because its tricky to&nbsp;make end-to-end encrypted chats
work the way users expect.</p>
<p>Luckily, protocols and cryptography dont care about
convenience or&nbsp;user expectations.
Many XMPP clients let you do&nbsp;almost anything youre trying to&nbsp;do.
Sometimes its clunky and unintuitive,
sometimes its the kind of&nbsp;freedom
that lets you shoot yourself in&nbsp;the foot.
At&nbsp;the end of&nbsp;the day, youd better understand what youre doing.</p>
<p>It&nbsp;might sound messy, but for that price, XMPP actually
gives you a&nbsp;lot of&nbsp;handy features:
your chats are secured with Signal-grade end-to-end encryption,
and you can use as&nbsp;many devices as&nbsp;you want,
all at&nbsp;the same time,
without being tied to&nbsp;any proprietary service.</p>
<p>In&nbsp;general, the XMPP experience today
could be&nbsp;described as&nbsp;a&nbsp;“WhatsApp with benefits and frictions”.
Its kinda ironic, considering that WhatsApps protocol
is&nbsp;actually based on&nbsp;XMPP, but incompatibly altered and defederated.</p>
<h3 id="keys-fingerprints-and-trust">Keys, Fingerprints and Trust</h3>
<p>OMEMO is&nbsp;based on&nbsp;the <a
href='https://en.wikipedia.org/wiki/Double_Ratchet_Algorithm'
target='_blank'>
Double Ratchet Algorithm</a>.
While the internal details are quite interesting,
for practical purposes its enough to&nbsp;know that
each client stores some cryptographic keys
and can derive a&nbsp;human-readable hash from them,
commonly called a&nbsp;fingerprint.</p>
<p>Keys are usually managed automatically by&nbsp;the XMPP client,
and in&nbsp;normal use you should never need to&nbsp;handle them manually.
In&nbsp;fact, you probably dont even need to&nbsp;know what they look like.</p>
<p>A&nbsp;fingerprint lets you identify
a&nbsp;specific client of&nbsp;your contact
and verify that it&nbsp;hasnt been spoofed.
Fingerprints for an&nbsp;account are not secret:
clients publish their own fingerprints to&nbsp;the XMPP server
and automatically receive the fingerprints of&nbsp;others.
Only fingerprints you explicitly mark as&nbsp;trusted are relevant.</p>
<p>In&nbsp;an&nbsp;typical scenario, the contact should confirm in&nbsp;person
or&nbsp;through an&nbsp;already trusted and secure communication channel
that the fingerprint belongs to&nbsp;their device,
and only then you mark it&nbsp;as&nbsp;trusted.</p>
<p>The list of&nbsp;trusted fingerprints
is&nbsp;used at&nbsp;the moment a&nbsp;message is&nbsp;sent.
Behind the scenes,
OMEMO performs a&nbsp;certain amount of&nbsp;key management,
and only the clients that are present in&nbsp;the trusted list
at&nbsp;the time of&nbsp;encryption
will be&nbsp;able to&nbsp;decrypt the message later.</p>
<p>Its important to&nbsp;understand
that trust cannot be&nbsp;applied retroactively:
its not possible to&nbsp;“extend” trust to&nbsp;new clients
after a&nbsp;message has already been encrypted and sent.</p>
<h2 id='practical-aspects-of-omemo-and-xmpp'>
Practical Aspects of&nbsp;OMEMO and XMPP
</h2>
<h3 id="chat-history">Chat History</h3>
<p>In&nbsp;theory, XMPP supports server-side message history storage via
<strong>XEP-0313: Message Archive Management</strong>.</p>
<p>In&nbsp;practice, support for this XEP,
as&nbsp;well as&nbsp;retention policies and message lifetime,
depends on&nbsp;the specific server.
You should never assume that all conversations are stored
indefinitely by&nbsp;default.
From a&nbsp;practical standpoint,
the server-side MAM archive is&nbsp;better considered a&nbsp;cache:
it&nbsp;can help you handle recent messages after a&nbsp;short period offline
or&nbsp;synchronize conversations across multiple devices.</p>
<p>At&nbsp;the end of&nbsp;the day,
keeping your chat history is&nbsp;your responsibility,
and this is&nbsp;a&nbsp;good place to&nbsp;apply a&nbsp;local-first approach.</p>
<h3 id="synchronisation">Synchronisation</h3>
<p>Seamless switching between clients is&nbsp;handled by
<strong>XEP-0280: Message Carbons</strong>.
Before its introduction, only incoming messages were synced between devices,
while your own outgoing messages were not.
Protocol-level mirroring of&nbsp;your own messages
is&nbsp;a&nbsp;rather non-obvious feature :D</p>
<p>Its important to&nbsp;note that with end-to-end encryption,
the concept of&nbsp;trusted fingerprints also applies to&nbsp;your own clients.
For seamless synchronisation of&nbsp;outgoing messages,
all your clients must trust each others fingerprints.
A&nbsp;new client,
or&nbsp;an&nbsp;old one that was not trusted
at&nbsp;the time messages were sent,
will receive the full history from MAM
but will not be&nbsp;able to&nbsp;decrypt it.
<br>Yes, even your own messages.</p>
<p>In&nbsp;theory, re-encrypting messages on&nbsp;already trusted clients
could solve this issue, but no&nbsp;XMPP client implements it&nbsp;yet.
So&nbsp;in&nbsp;practice you may need to&nbsp;manually resend
some data to&nbsp;a&nbsp;new device.</p>
<h3 id="message-correction">Message Correction</h3>
<p>Its worth keeping in&nbsp;mind that
features that seem simple and straightforward at&nbsp;first glance,
such as&nbsp;message editing and deletion,
actually rely on&nbsp;client-side implementation
and may not behave for your recipient the way you expect.</p>
<p>Theyre fine to&nbsp;use and are well supported in&nbsp;some clients,
but you shouldnt rely on&nbsp;them to&nbsp;hide anything.</p>
<h3 id="maintenance">Maintenance</h3>
<p>OMEMO was designed as&nbsp;a&nbsp;set-it-and-forget-it solution
and mostly succeeds in&nbsp;that goal.
If&nbsp;you have a&nbsp;basic understanding of&nbsp;how the protocol works
and check in&nbsp;online from time to&nbsp;time,
there shouldnt be&nbsp;any surprises.</p>
<p>All maintenance comes down to&nbsp;making regular backups
and notifying your contacts
when fingerprints are added or&nbsp;no&nbsp;longer valid
so&nbsp;they can keep their trust list up&nbsp;to&nbsp;date.</p>
<h2 id="step-by-step-guide">Step-by-Step Guide</h2>
<p>Lets say I&nbsp;have a&nbsp;XMPP account, <code>me@some.server</code>,
and a&nbsp;few devices:
a&nbsp;phone, a&nbsp;laptop, and a&nbsp;desktop computer.
First Ill describe my&nbsp;mindset at&nbsp;a&nbsp;high level,
then Ill add some notes about specific clients.</p>
<h3 id="client-roles">Client Roles</h3>
<p>On&nbsp;the one hand, I&nbsp;have my&nbsp;phone.
Its almost always with me&nbsp;and almost always online.
Thats where I&nbsp;keep the full chat history
and get real-time notifications.</p>
<p>On&nbsp;the other hand, I&nbsp;have a&nbsp;couple of&nbsp;desktop applications.
I&nbsp;only open them
when I&nbsp;need to&nbsp;discuss something using my&nbsp;keyboard
or&nbsp;share some text between devices.
I&nbsp;like to&nbsp;think of&nbsp;them as&nbsp;satellite clients.</p>
<h3 id="before-the-start">Before the Start</h3>
<p>First, enable OMEMO encryption
on&nbsp;every client if&nbsp;it&nbsp;isnt enabled by&nbsp;default.</p>
<p>The next step is&nbsp;to&nbsp;add
all clients to&nbsp;the trust list on&nbsp;each device:
my&nbsp;phone should trust all my&nbsp;computers,
and my&nbsp;computers should trust each other
as&nbsp;well as&nbsp;my&nbsp;phone.</p>
<p>Fingerprints do&nbsp;not have to&nbsp;be&nbsp;secret,
so&nbsp;they can be&nbsp;published on
your website or&nbsp;even on&nbsp;social media profiles.
Here is&nbsp;my&nbsp;page with the fingerprints, for example:
<br><a href='https://oddsquat.org/about/keys/' target='_blank'>
https://oddsquat.org/about/keys/
</a></p>
<h3 id="start-the-conversation-inperson">Start the Conversation in&nbsp;Person</h3>
<p>Lets say I&nbsp;meet Alice,
we&nbsp;start talking,
and then decide to&nbsp;continue the conversation online.</p>
<p>I&nbsp;open a&nbsp;special QR&nbsp;code on&nbsp;my&nbsp;phone,
and Alice scans it&nbsp;with her client.
This QR&nbsp;code already contains
the fingerprints of&nbsp;all my&nbsp;devices,
so&nbsp;no&nbsp;extra steps are needed on&nbsp;her phone.
After that, I&nbsp;do&nbsp;the same
and scan her QR&nbsp;code as&nbsp;well.</p>
<p>Later at&nbsp;home,
I&nbsp;manually mark her devices as&nbsp;trusted on&nbsp;my&nbsp;computers
using the trusted list on&nbsp;my&nbsp;phone, and she does the same.</p>
<p>Now we&nbsp;are both sure
that it&nbsp;is&nbsp;really us&nbsp;in&nbsp;the conversation,
and that all messages will be&nbsp;available
on&nbsp;all our devices and only on&nbsp;them.</p>
<h3 id="start-the-conversation-online">Start the Conversation Online</h3>
<p>Lets say Bob and I&nbsp;start discussing something
on&nbsp;a&nbsp;forum or&nbsp;in&nbsp;the Fediverse,
and then decide to&nbsp;continue the discussion on&nbsp;XMPP.</p>
<p>Before starting the chat,
Bob can confirm its really me&nbsp;using my&nbsp;page with fingerprints.
I&nbsp;can confirm its really him
by&nbsp;asking him to&nbsp;send his fingerprints
in&nbsp;a&nbsp;private message on&nbsp;the same forum or&nbsp;via email.</p>
<p>Ideally, Bob also has a&nbsp;public page with his fingerprints.
That way, we&nbsp;can both independently verify
that we&nbsp;are who we&nbsp;say we&nbsp;are.</p>
<p>In&nbsp;an&nbsp;alternative scenario,
where there has been no&nbsp;prior communication or&nbsp;public pages
and only a&nbsp;single JID&nbsp;is known,
things play out a&nbsp;bit differently:
Bob starts the chat,
I&nbsp;trust the first device he&nbsp;messages me&nbsp;from,
and then we&nbsp;exchange fingerprints for our other devices,
if&nbsp;we&nbsp;have any.
This approach is&nbsp;called TOFU (Trust On&nbsp;First Use).</p>
<h3 id="new-orlost-devices">New or&nbsp;Lost Devices</h3>
<p>If&nbsp;I&nbsp;start using a&nbsp;new device
or&nbsp;install another client application,
the first thing I&nbsp;do&nbsp;is&nbsp;add it&nbsp;to&nbsp;the list
of&nbsp;trusted clients on&nbsp;my&nbsp;existing devices.</p>
<p>If&nbsp;I&nbsp;lose one of&nbsp;my&nbsp;devices
or&nbsp;delete any private keys,
the first thing I&nbsp;do&nbsp;is&nbsp;remove the corresponding client
from the trusted list on&nbsp;my&nbsp;other devices.</p>
<p>Once Ive updated all my&nbsp;personal lists,
I&nbsp;should inform my&nbsp;contacts about changes via trusted channels.</p>
<p>I&nbsp;can simply ask Alice to&nbsp;scan
my&nbsp;new QR&nbsp;code the next time we&nbsp;meet,
and send Bob a&nbsp;message introducing
my&nbsp;new client or&nbsp;letting him know
that the lost device is&nbsp;no&nbsp;longer trusted
and that no&nbsp;real messages will ever come from it&nbsp;again.</p>
<h2 id="client-applications">Client Applications</h2>
<p>This section describes
how OMEMO is&nbsp;used in&nbsp;specific client applications
that I&nbsp;personally use.</p>
<h3 id="conversations-and-forks">Conversations and Forks</h3>
<p><a
href='https://conversations.im/'
target='_blank'>
Conversations</a> is&nbsp;a&nbsp;modern,
fully featured chat application for Android.
It&nbsp;supports everything a&nbsp;messaging app should support:
chats, voice calls, video calls, and sharing files of&nbsp;any kind.</p>
<p>There are several forks of&nbsp;it&nbsp;where
the UI&nbsp;or&nbsp;UX&nbsp;may differ,
but the core features work exactly the same.
I&nbsp;personally use <a
href='https://codeberg.org/monocles/monocles_chat'
target='_blank'>
Monocles Chat</a>.</p>
<p>On&nbsp;the Contact Details screen (including your own account),
you can see a&nbsp;list of&nbsp;published fingerprints
and manually mark them as&nbsp;trusted or&nbsp;revoke trust.</p>
<p>To&nbsp;simplify all these routine operations,
a&nbsp;QR-code-based system is&nbsp;used:
you can show your own QR&nbsp;code or&nbsp;scan other peoples codes
directly from the main screen.
This makes device verification during in-person meetings
simple and effortless.</p>
<h3 id="dino">Dino</h3>
<p><a
href='https://dino.im/'
target='_blank'>
Dino</a> is&nbsp;a&nbsp;lightweight GTK-based GUI client.</p>
<p>It&nbsp;can be&nbsp;considered a&nbsp;fully functional one,
although some non-essential features are still not implemented.
For example,
it&nbsp;is&nbsp;not possible to&nbsp;clear local chat history
using built-in methods :D</p>
<p>Trust and untrust decisions can be&nbsp;easily managed
in&nbsp;the Encryption tab of&nbsp;the Conversation Details window.</p>
<p>It&nbsp;is&nbsp;important to&nbsp;note that,
by&nbsp;default, Dino is&nbsp;configured
to&nbsp;automatically trust new fingerprints.
I&nbsp;recommend disabling this feature.</p>
<h3 id="profanity">Profanity</h3>
<p><a
href='https://profanity-im.github.io/'
target='_blank'>
Profanity</a> is&nbsp;a&nbsp;powerful TUI client
where everything is&nbsp;controlled through a&nbsp;built-in command system.</p>
<p>If&nbsp;you somehow intend to&nbsp;use it,
you can find a&nbsp;small cheat sheet for the <code>omemo</code> command below.
However, I&nbsp;strongly recommend reading the full documentation.</p>
<ul>
<li><p>Generate a&nbsp;key and add your other clients:</p>
<pre><code class="language-text">/omemo gen
/omemo trust me@some.server some-cool-fingerprint-01
/omemo trust me@some.server another-cool-fingerprint
/omemo qrcode</code></pre>
</li>
<li><p>View the list of&nbsp;your own or&nbsp;someone elses fingerprints:</p>
<pre><code class="language-text">/omemo fingerprint me@some.server
/omemo fingerprint alice@another.server</code></pre>
<p>Trusted ones will be&nbsp;marked as&nbsp;<code>trusted</code>.</p>
</li>
<li><p>Start an&nbsp;encrypted conversation:</p>
<pre><code class="language-text">/omemo start alice@another.server</code></pre>
</li>
<li><p>Add fingerprints to&nbsp;the trusted list:</p>
<pre><code class="language-text">/omemo trust alice@another.server some-cool-fingerprint-02
/omemo trust alice@another.server some-cool-fingerprint-03
/omemo trust bob@another.server some-cool-fingerprint-04</code></pre>
</li>
<li><p>Revoke trust for a&nbsp;specific client:</p>
<pre><code class="language-text">/omemo untrust alice@another.server some-cool-fingerprint-02</code></pre>
</li>
</ul>
<h2 id="late-disclaimer">Late Disclaimer</h2>
<p>This post was originally intended
as&nbsp;a&nbsp;collection of&nbsp;answers to&nbsp;questions
I&nbsp;had when I&nbsp;first started using XMPP with OMEMO.</p>
<p>It&nbsp;isnt meant to&nbsp;be&nbsp;exhaustive or&nbsp;formal,
but rather to&nbsp;clarify the practical side of&nbsp;things
and reduce that initial feeling of&nbsp;being lost
when you keep running into
“The message was not encrypted for this device”
over and over again.</p>
<p>From now on, I&nbsp;hope you wont encounter such errors
or&nbsp;any other issues with end-to-end encryption,
and youll feel confident using it&nbsp;in&nbsp;XMPP.</p>
</article>
</main>
<footer>
2026-04-23
</footer>
<script async
data-goatcounter="https://he4et.goatcounter.com/count"
src="https://gc.zgo.at/count.js"></script>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta
name="description"
content="Redirect to '/posts/#2026'.">
<meta
http-equiv="Refresh"
content="0; URL=/posts/#2026">
<title>
Redirect | oddsquat
</title>
</head>
<body>
<main>
Redirect to
<a
style="color: inherit;"
href="/posts/#2026">
/posts/#2026
</a>
</main>
</body>
</html>

File diff suppressed because it is too large Load diff

151
docs/posts/index.html Normal file
View file

@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<title>
posts | oddsquat
</title>
<meta name="description" content="Сomplete list of posts.">
<link rel="preload" href="/fonts/open_sans_condensed-32.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans_condensed-27.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-25.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-24.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/open_sans-17.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" type="text/css" href="/css/fonts.css">
<link rel="stylesheet" type="text/css" href="/css/typography.css">
<link rel="stylesheet" type="text/css" href="/css/main.css">
</head>
<body>
<div class="stripesContainer">
<div class="stripes">
</div>
</div>
<header>
<nav>
<ul>
<li><a href="/">oddsquat</a></li>
<li>posts</li>
</ul>
</nav>
</header>
<main>
<article>
<h1 id="posts">Posts</h1>
<p>Announcements are available via <a href="/rss.xml">RSS</a>.</p>
<hr>
<h2 id="2026">2026</h2>
<ul>
<li><article class='entry'>
<header>
[en]
<strong>
<a href='/posts/2026/encrypted_XMPP/'>encrypted XMPP</a>
</strong>
</header>
<section class='description'>
Secure and private messaging with XMPP and OMEMO encryption.
</section>
</article>
</li>
<li><article class='entry'>
<header>
[ru]
<strong>
<a href='/posts/2026/ugly_keyboards_ru/'>ugly keyboards</a>
</strong>
</header>
<section class='description'>
Почему нас окружают уродливые клавиатуры и&nbsp;что с&nbsp;этим можно сделать.
</section>
</article>
</li>
</ul>
<h2 id="2024">2024</h2>
<ul>
<li><article class='entry'>
<header>
[ru]
<strong>
<a href='/posts/2024/wrapped_bw_ru/'>wrapped bw</a>
</strong>
</header>
<section class='description'>
Превращаем fully-featured Bitwarden command-line interface в&nbsp;удобный.
</section>
</article>
</li>
<li><article class='entry'>
<header>
[ru]
<strong>
<a href='/posts/2024/selfhosted_llm/'>selfhosted LLM</a>
</strong>
</header>
<section class='description'>
Персональные LLM в&nbsp;docker-контейнере на&nbsp;твоём компьютере.
</section>
</article>
</li>
</ul>
<h2 id="2020">2020</h2>
<ul>
<li><article class='entry'>
<header>
[ru]
<strong>
<a href='/posts/2020/typographic_linter/'>typographic linter</a>
</strong>
</header>
<section class='description'>
Prettier для текста. Автоматизация рутинной типографики.
</section>
</article>
</li>
<li><article class='entry'>
<header>
[ru]
<strong>
<a href='/posts/2020/initial_post/'>initial post</a>
</strong>
</header>
<section class='description'>
Какие инструменты были использованы для создания этого сайта, какие решения были приняты и&nbsp;почему.
</section>
</article>
</li>
</ul>
</article>
</main>
<footer>
</footer>
<script async
data-goatcounter="https://he4et.goatcounter.com/count"
src="https://gc.zgo.at/count.js"></script>
</body>
</html>