Преамбула

Будучи поклонником многопротокольных IM-клиентов, я довольно долго пользовался Miranda NG. Но кривоватая поддержка некоторых современных протоколов вроде Discord мешала пользоваться только ей, хотя возможности кастомизации у неё очень широкие. В конце-концов практичность взяла верх над перфекционизмом, и я установил Pidgin 2.14. Несмотря на некоторую страшноватость, программа оказалось весьма практичной. Однако была и ложка дёгтя.

Как выяснилось, трёхуровневая иерархия Discord (сервер - категория - канал) скверно ложится на двухуровневую иерархию списка контактов (группа - контакт), и соответствующил плагин решил эту коллизию просто - каждая категория была группой, чьё имя содержало префикс сервера. Разумеется, это привело к тому, что даже при моём скромном круге общения в контакт-листе образовалось 30+ групп - помимо групп из других протоколов. Ориентироваться в этом было не слишком удобно - хотелось иметь возможность устроить какую-то иерархию, например, сделать над-группы.

Однако быстрый поиск вывел на старый баг-трекер Pidgin, где такая возможность была упомянута... и отмечена как wont-fix. Упс. Ну что ж, где наша не пропадала - сделаю имитацию сам!

Полезные ресурсы

Концепция

Pidgin в основном поддерживает плагины, написанные на C. Есть загрузчики дла плагинов на Perl, а также механизм взаимодействия с DBus, но C является основным средством разработки. Кроме того, из всех затронутых языков он мне наименее незнаком.

Далее, ключевая идея плагина была сформулирована следующим образом: скрывать или комбинировать группы в списке контактов согласно некоторому набору правил. При этом наборы правил должны задаваться пользователем, иметь читаемое имя и, желательно, иконку. Поскольку плагин для Discord генерировал группы вида "Имя сервера: Категория", то напрашивалась идея использования wildcards в плагинах, чтобы не пришлось перечислять все категории сервера по отдельности. Также показалось стоящим предусмотреть возможность указания нескольких правил в наборе, а также создания отрицающих правил (исключение группы из показа).
Таким образом, задача была декомпозирована на составляющие:

  1. Понять, как устроен плагин Pidgin.

  2. Разобраться, какое средство использовать для wildcard-matching'а, и реализовать логику применения правил к группе.

  3. Понять, как устроен контакт-лист libpurple (бэкенд клиента Pidgin), и как получить сведения о группе.

  4. Понять, как устроено отображение списка контактов в Pidgin, и как скрыть или показать контакт в списке.

  5. Понять, как устроен GUI Pidgin в целом, и как в него добавить свою панель инструментов.

  6. Разобраться с хранением настроек, и созданием диалога настроек для своего плагина.

Простой плагин для Pidgin

К счастью, в репозитории нашёлся довольно понятный hello-world.c, описывающий базовую структуру плагина для Pidgin. Все плагины можно разделить на несколько категорий:

  • core - плагины, работающие только с libpurple и никаким образом не затрагивающие UI.

  • prpl - плагины, реализующие протоколы месседжеров.

  • lopl - загрузчики для плагинов на скриптовых языках.

  • gtk, gtk-x11 и gtk-win32 - плагины, работающие с GUI - универсальные, Linux-специфичные и Windows-специфичные.

  • gnt - плагин для консольной версии клиента (Finch).

Очевидно, нас интересует gtk-плагин, на худой конец gtk-win32.

И сам Pidgin, и нижележащая библиотека libpurple построены на базе GLib и графического тулкита GTK - правда, довольно старых версий. Из этого следует несколько выводов. Во-первых, для управления памятью часто используется механизм счётчика ссылок, что несколько упрощает решение вопросов, касающихся времени жизни объектов. Во-вторых, GLib предоставляет механизм сигналов, позволяющий достаточно прозрачно реализовать обработку событий, в том числе событий интерфейса. libpurple активно использует сигналы для оповещения клиентского кода о событиях, поступающих из IM-сетей, а также об операциях над хранимыми данными вроде списка контактов. Поэтому вопрос реакции на то или иное событие будет сводиться к поиску нужного сигнала и подписке на него.

Попутно решилась задача реализации windcard-matching'а - GLib представляет соответствующие средства, над которыми потребовалась лишь небольшая логическая надстройка.

BuddyList и все-все-все

По итогам вдумчивого чтения документации (и осторожных расспросов на полуофициальном Discord-сервере проекта) выяснилось следующее. Формально список контактов (buddy list) из libpurple представляет собой дерево из разнородных узлов, подобно известному паттерну "компоновщик". Это дерево обёрнуто в объект PurpleBuddyList.

Однако на практике это дерево формирует фиксированную трёхуровневую иерархическую структуру. На верхнем уровне находятся группы контактов, представленные объектами PurpleGroup, на нижнем - индивидуальные участники (PurpleBuddy), а на промежуточном - чаты (PurpleChat) и контакты (PurpleContact) как таковые. Элементы на каждом уровне дерева связаны в двусвязный список - таким образом, родитель хранит только ссылку на своего первого потомка.
По сути, контакты в libpurple являются аналогами мета-контактов в других клиентов - они позволяют объединить несколько учётных записей одного человека в одну сущность, что довольно удобно. Однако это никак не помогало в решении задачи по группировке групп контактов. Пришлось отказаться от идеи о группировке, и остановиться на варианте со скрытием "ненужных" групп.

Ради этого пришлось прошерстить исходный код, отвечающий за отображение контакта в графическом интерфейсе Pidgin. И тут всплыли так называемые флаги контакта, среди которых был флаг INVISIBLE. Элементы с таким флагом не отображались в списке контактов независимо от их типа. Бинго?

Почти. Как быстро выяснилось, скрытие контакта в группе могло приводить к скрытию всей группы. Такое поведение являлось следствием логики в функции pidgin_blist_update_group(), которая скрывала всю группу, если обновлялся скрытый контакт в ней. Почему так, я до сих пор не понял. Тем не менее, это ещё не означало непригодности данного механизма - только ограничивало плагин скрытием групп целиком.

Графический интерфейс

Сам же графический элемент списка контактов оказался устроен и проще, и сложнее чем я предполагал. Проще, потому что это оказался всего один компонент - GtkTreeView. Сложнее, потому что этот компонент имеет дело с отдельным объектом-моделью (скрытом за интерфейсом GtkTreeModel), и обладает заковыристым механизмом для кастомизации рендеринга элементов модели на экране. Как следствие, чтобы вмешаться в его работу в достаточной степени, мне пришлось бы повторить реализацию солидного куска списка контактов Pidgin - что было бы долго, сложно и плохо совместимо с другими плагинами. Как итог, для реализации скрытия пришлось остановиться на описанном выше флаге INVISIBLE.
Тем не менее, GtkTreeView мне пригодился в ходе реализации диалога редактирования правил.

Что касается остального графического интерфейса, то там всё оказалось достаточно просто. TreeView завернут в несколько контейнеров, ссылки на которые хранятся в публично доступной структуре данных. Таким образом, создав свой собственный контейнер, можно добавить его в уже существующие. Но такой подход серьёзно ограничен в возможностях. Можно только добавить панель статического размера выше или ниже списка контактов, но не слева или справа. Плагин mystatusbox из purple plugin pack добавляет панель с изменяемым размером, но для этого ему приходится перелопачивать половину интерфейса. Я не настолько был уверен в своих силах, чтобы провернуть подобное и не сломать совместимость с mystatusbox (которым пользовался и сам), поэтому ограничился простой панелью.

Хранение настроек

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

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

Наконец, Pidgin предоставляет три механизма для изменения настроек плагина.

Первый, упрощённый, позволяет указать список из записей, содержащих имя параметра и его желаемое представление. На основании этого списка Pidgin автоматически сформирует диалог настроек, и выполнит их привязку к элементам управления.
Второй, чуть более сложный, позволяет задать кастомный диалог в качестве диалога настроек, и показывать его вместо стандартного.
Третий, наиболее универсальный - воспользоваться механизмом действий, т.е. дополнительных пунктов меню, добавляемых плагинами. В рамках действий можно выполнять практически любые операции, в том числе показ нужных диалоговых окон. Единственное ограничение - эти окна будут работать параллельно с остальной программой.

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

Результат

Интерфейс выбора показываемых групп выглядит примерно так:

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

Настройка правил осуществляется через диалог настроек, показанный ниже. Не слишком красиво, но свои функции выполняет.

Сам плагин можно найти на Github.

Итоги проекта

Считаю ли я плагин полезным? Мне он пригодился. Если сам плагин, или приведённая в статье информация пригодятся кому-то ещё, будет замечательно.

Насколько сложно было въехать в тематику? На удивление просто, несмотря на то, что я ранее не имел дела ни с разработкой на чистом C, ни с GTK. Кодовая база pidgin достаточно хорошо организована, хотя документация иногда оставляет желать лучшего.

Что было самым трудным в разработке? Как ни странно, настроить окружение для сборки проекта на Windows. Под Linux это оказалось намного проще, так что почти вся разработка велась на домашнем сервере под Debian. Инструкция по сборке есть, но она требует адаптации. В итоге пришлось просить помощи на Discord-сервере Pidgin.

Почему я счёл необходимым написать эту статью? Потому что мне пришлось повозиться, собирая крупицы документации и продираясь через мёртвые ссылки, и захотелось сохранить полученные знания.

И хотя сейчас Pidgin переживает переход на версию 3.0 с сопутствующей переработкой кодовой базы, версия 2.x.y ещё долго будет актуальна. Да и в целом ситуация выглядит так, словно мультипротокольные клиенты находятся в некотором упадке. Если это не так, отпишитесь в комментариях - буду рад ошибиться.

Комментарии (0)