Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions ai_oca_bridge_chatter/models/ai_bridge_execution.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright 2025 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from markupsafe import Markup

from odoo import _, fields, models


Expand Down Expand Up @@ -36,7 +38,22 @@ def _process_response_message(self, response):
)
)
recipient._notify_typing(is_typing=False)
response["author_id"] = self.chatter_user_id.partner_id.id
response["message_type"] = "comment"
# Existing 18.0 bridges already return HTML as a plain str.
# message_post escapes str bodies and the Html field then wraps
# them, producing <p>&lt;p&gt;...&lt;/p&gt;</p>. body_is_html=True
# also warns for internal users, so convert to Markup instead of
# rewriting. Assume HTML by default; body_is_html=False opts out.
body = response.get("body") or ""
body_is_html = bool(response.pop("body_is_html", True))
if body_is_html and not isinstance(body, Markup):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not body_is_html, isn't it?

body = Markup(body)
response.update(
{
"author_id": self.chatter_user_id.partner_id.id,
"body": body,
"message_type": "comment",
"subtype_xmlid": "mail.mt_comment",
}
)

return super()._process_response_message(response)
80 changes: 75 additions & 5 deletions ai_oca_bridge_chatter/tests/test_chatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,82 @@ def test_chat(self):
body="Test message",
)
mock_post.assert_called_once()
self.assertEqual(
2,
self.env["mail.message"].search_count(
[("res_id", "=", self.chat.id), ("model", "=", "discuss.channel")]
),
messages = self.env["mail.message"].search(
[("res_id", "=", self.chat.id), ("model", "=", "discuss.channel")],
order="id",
)
self.assertEqual(2, len(messages))
ai_message = messages[-1]
self.assertEqual(ai_message.author_id, self.ai_user.partner_id)
self.assertEqual(ai_message.message_type, "comment")
self.assertEqual(ai_message.subtype_id, self.env.ref("mail.mt_comment"))
self.assertIn("My message", ai_message.body)

def test_chat_html_body_is_comment(self):
"""HTML replies stay Discuss comments and are not escaped as plaintext."""
with mock.patch("requests.post") as mock_post:
mock_post.return_value = mock.Mock(
status_code=200,
json=lambda: {"body": "<p>Hello! How can I help?</p>"},
)
self.chat.with_user(self.user.id).message_post(body="ola")
ai_message = self.env["mail.message"].search(
[
("res_id", "=", self.chat.id),
("model", "=", "discuss.channel"),
("author_id", "=", self.ai_user.partner_id.id),
],
limit=1,
)
self.assertTrue(ai_message)
self.assertEqual(ai_message.subtype_id, self.env.ref("mail.mt_comment"))
self.assertEqual(ai_message.message_type, "comment")
self.assertIn("Hello! How can I help?", ai_message.body)
self.assertIn("<p>Hello! How can I help?</p>", ai_message.body)
self.assertNotIn("<p>&lt;p&gt;", ai_message.body)

def test_chat_inline_html_body(self):
"""Inline HTML that does not start with a tag is still posted as HTML."""
with mock.patch("requests.post") as mock_post:
mock_post.return_value = mock.Mock(
status_code=200,
json=lambda: {"body": "Hello <b>world</b>"},
)
self.chat.with_user(self.user.id).message_post(body="ola")
ai_message = self.env["mail.message"].search(
[
("res_id", "=", self.chat.id),
("model", "=", "discuss.channel"),
("author_id", "=", self.ai_user.partner_id.id),
],
limit=1,
)
self.assertTrue(ai_message)
self.assertIn("<b>world</b>", ai_message.body)
self.assertNotIn("&lt;b&gt;", ai_message.body)

def test_chat_body_is_html_false(self):
"""body_is_html=False opts out of the 18.0 HTML default."""
with mock.patch("requests.post") as mock_post:
mock_post.return_value = mock.Mock(
status_code=200,
json=lambda: {
"body": "<p>Hello! How can I help?</p>",
"body_is_html": False,
},
)
self.chat.with_user(self.user.id).message_post(body="ola")
ai_message = self.env["mail.message"].search(
[
("res_id", "=", self.chat.id),
("model", "=", "discuss.channel"),
("author_id", "=", self.ai_user.partner_id.id),
],
limit=1,
)
self.assertTrue(ai_message)
self.assertEqual(ai_message.subtype_id, self.env.ref("mail.mt_comment"))
self.assertIn("&lt;p&gt;", ai_message.body)

def test_channel_not_called(self):
"""No AI bridge should be called when the user is not callend in the channel"""
Expand Down
Loading