diff --git a/ai_oca_bridge_chatter/models/ai_bridge_execution.py b/ai_oca_bridge_chatter/models/ai_bridge_execution.py index adb74ca2..183b4828 100644 --- a/ai_oca_bridge_chatter/models/ai_bridge_execution.py +++ b/ai_oca_bridge_chatter/models/ai_bridge_execution.py @@ -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 @@ -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>...</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): + 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) diff --git a/ai_oca_bridge_chatter/tests/test_chatter.py b/ai_oca_bridge_chatter/tests/test_chatter.py index 67b4eda1..f3287d4e 100644 --- a/ai_oca_bridge_chatter/tests/test_chatter.py +++ b/ai_oca_bridge_chatter/tests/test_chatter.py @@ -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": "Hello! How can I help?
"}, + ) + 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("Hello! How can I help?
", ai_message.body) + self.assertNotIn("<p>", 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 world"}, + ) + 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("world", ai_message.body) + self.assertNotIn("<b>", 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": "
Hello! How can I help?
", + "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("<p>", ai_message.body) def test_channel_not_called(self): """No AI bridge should be called when the user is not callend in the channel"""