diff --git a/docs/update-4.md b/docs/update-4.md
new file mode 100644
index 0000000..7c2ae05
--- /dev/null
+++ b/docs/update-4.md
@@ -0,0 +1,68 @@
+# Обновление 4 — Исправление отображения картинок
+
+## Контекст
+
+В update-2 и update-3 была реализована отправка новостей через XHTML-IM (XEP-0071) с картинкой через тег `
`.
+По результатам тестирования картинки не отображаются:
+- **Gajim** — показывает URL картинки вместо изображения (использует plain-text fallback)
+- **Monocle Chat (форк Conversations)** — показывает незагруженную заглушку
+
+Причины установлены в ходе анализа лога stanza.
+
+---
+
+## Исправление 1 — двойной `
` в XHTML-IM
+
+### Проблема
+
+slixmpp xep_0071 при присвоении `msg["html"]["body"] = xhtml_body` самостоятельно оборачивает
+содержимое в ``. Текущая реализация `_build_xhtml`
+также включает этот тег, в результате в stanza попадает вложенный ``:
+
+```xml
+ ← добавляет slixmpp
+ ← добавляет наш код
+ ...
+
+
+```
+
+Это невалидный XHTML-IM, что приводит к использованию plain-text fallback в строгих клиентах.
+
+### Решение
+
+В методе `_build_xhtml` убрать обёртку `...` — возвращать только
+внутренние элементы (``, `
`). slixmpp добавит `
` сам.
+
+---
+
+## Исправление 2 — OOB для отображения картинок в Conversations-клиентах
+
+### Проблема
+
+Клиенты семейства Conversations (включая Monocle Chat) блокируют загрузку внешних изображений
+из XHTML-IM `
` по умолчанию — это намеренная защита от трекинг-пикселей.
+Картинка парсится, но не загружается.
+
+### Решение
+
+Дополнительно к XHTML-IM прикреплять URL картинки как **Out-of-Band Data (XEP-0066)**.
+Conversations и его форки умеют рендерить OOB-ссылки как inline-превью.
+
+Для этого к сообщению добавляется элемент:
+
+```xml
+
+ https://example.com/image.jpg
+
+```
+
+### Техническая реализация
+
+1. Зарегистрировать плагин `xep_0066` в `JabberConnection`
+2. В методе `send_to_room_xhtml` добавить OOB-поле к сообщению, если передан `image_url`:
+ ```python
+ msg["oob"]["url"] = image_url
+ ```
+3. Сигнатуру `send_to_room_xhtml` расширить необязательным параметром `image_url: str | None = None`
+4. В `JabberNewsPublisher.publish` передавать `item.image_url` в `send_to_room_xhtml`
diff --git a/src/adapters/jabber/connection.py b/src/adapters/jabber/connection.py
index 7063ae2..a66244f 100644
--- a/src/adapters/jabber/connection.py
+++ b/src/adapters/jabber/connection.py
@@ -25,6 +25,7 @@ class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver, MucJidRes
self._rooms_on_start: List[str] = []
self.register_plugin("xep_0045") # MUC
+ self.register_plugin("xep_0066") # Out-of-Band Data
self.register_plugin("xep_0071") # XHTML-IM
self.register_plugin("xep_0199") # XMPP Ping
@@ -106,16 +107,25 @@ class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver, MucJidRes
logger.warning("Не удалось отправить сообщение в %s", room_jid)
return False
- async def send_to_room_xhtml(self, room_jid: str, plain_text: str, xhtml_body: str) -> bool:
+ async def send_to_room_xhtml(
+ self,
+ room_jid: str,
+ plain_text: str,
+ xhtml_body: str,
+ image_url: Optional[str] = None,
+ ) -> bool:
"""
Отправляет XHTML-IM сообщение в конференцию (XEP-0071).
plain_text — fallback для клиентов без поддержки XHTML.
+ image_url — если указан, добавляется OOB (XEP-0066) для клиентов семейства Conversations.
Возвращает False если возникла ошибка.
"""
try:
msg = self.make_message(mto=room_jid, mtype="groupchat")
msg["body"] = plain_text
msg["html"]["body"] = xhtml_body
+ if image_url:
+ msg["oob"]["url"] = image_url
msg.send()
return True
except Exception:
diff --git a/src/adapters/jabber/news_publisher.py b/src/adapters/jabber/news_publisher.py
index 4cbbd5d..4df9600 100644
--- a/src/adapters/jabber/news_publisher.py
+++ b/src/adapters/jabber/news_publisher.py
@@ -19,7 +19,7 @@ class JabberNewsPublisher(NewsPublisher):
"""
plain = self._build_plain(item)
xhtml = self._build_xhtml(item)
- return await self._connection.send_to_room_xhtml(room_jid, plain, xhtml)
+ return await self._connection.send_to_room_xhtml(room_jid, plain, xhtml, item.image_url)
def _build_plain(self, item: NewsItem) -> str:
blocks = []
@@ -33,12 +33,11 @@ class JabberNewsPublisher(NewsPublisher):
def _build_xhtml(self, item: NewsItem) -> str:
SEP = "
"
- parts = ['']
+ parts = []
if item.image_url:
parts.append(f'
{SEP}')
parts.append(f'{item.title}
{SEP}')
if item.summary:
parts.append(f'{item.summary}
{SEP}')
parts.append(f'{item.link}
')
- parts.append('')
return ''.join(parts)