Files
SYNITYandDangTinh311 2a23ad0c42 feat(bitrix24): recognize reply/quote context across all message types [B24:2794] (#1443)
Bitrix24 ships the quoted message inline on reply events via REPLY_MESSAGE,
but the channel parsed a non-existent REPLY_TO_MESSAGE_ID field, so the agent
never saw what a user was replying to. Recognize replies and feed their
context to the agent (Hybrid design):

- events.go: parse data[PARAMS][REPLY_MESSAGE][ID|AUTHOR_ID|MESSAGE] (form+json)
  with PARAMS[REPLY_ID] fallback into EventParams.ReplyMessage; drop the dead
  ReplyToMID field + bitrix_reply_to_mid meta.
- reply_context.go (new): resolveReplyContext prepends a short note and, for
  media-only originals, re-injects the quoted file(s) through the same download
  pipeline as a direct upload.
    text quote  -> sanitize BBCode + truncate, no API call.
    media quote -> im.dialog.messages.get (works for TYPE=bot bots, unlike the
      blocked imbot.v2.Chat.Message.get) to recover the attachment, bounded by
      media_max_mb; note-only fallback on any fetch/download failure.
- handle.go: wire resolveReplyContext into message assembly (prefix + extra media).

Covers text, image, audio, file, video quotes. Non-reply events are a no-op.
Adds unit tests for parse (form+json), sanitize, file-id extraction, result
parsing, the messages.get fetch, and both resolve branches.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-16 10:56:32 +07:00

106 lines
3.2 KiB
Go

package bitrix24
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func parseForm(t *testing.T, v interface{ Encode() string }) *Event {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/bitrix24/events", strings.NewReader(v.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
evt, err := ParseEvent(req)
if err != nil {
t.Fatalf("ParseEvent: %v", err)
}
return evt
}
func TestParseEvent_Form_ReplyMessage_Text(t *testing.T) {
v := buildBitrixForm()
v.Set("data[PARAMS][REPLY_MESSAGE][ID]", "315900")
v.Set("data[PARAMS][REPLY_MESSAGE][AUTHOR_ID]", "62")
v.Set("data[PARAMS][REPLY_MESSAGE][MESSAGE]", "báo giá cho anh nhé")
rm := parseForm(t, v).Params.ReplyMessage
if rm == nil {
t.Fatal("ReplyMessage nil; want parsed")
}
if rm.ID != "315900" || rm.AuthorID != "62" || rm.Text != "báo giá cho anh nhé" {
t.Errorf("ReplyMessage = %+v", rm)
}
}
func TestParseEvent_Form_ReplyMessage_MediaEmptyText(t *testing.T) {
// Media-only original: ID + AUTHOR present, MESSAGE empty (no file id inline).
v := buildBitrixForm()
v.Set("data[PARAMS][REPLY_MESSAGE][ID]", "315968")
v.Set("data[PARAMS][REPLY_MESSAGE][AUTHOR_ID]", "62")
v.Set("data[PARAMS][REPLY_MESSAGE][MESSAGE]", "")
rm := parseForm(t, v).Params.ReplyMessage
if rm == nil || rm.ID != "315968" || rm.Text != "" {
t.Errorf("ReplyMessage = %+v; want ID 315968, empty text", rm)
}
}
func TestParseEvent_Form_ReplyID_Fallback(t *testing.T) {
// Only the nested PARAMS[REPLY_ID] present (no REPLY_MESSAGE[ID]).
v := buildBitrixForm()
v.Set("data[PARAMS][PARAMS][REPLY_ID]", "315901")
rm := parseForm(t, v).Params.ReplyMessage
if rm == nil || rm.ID != "315901" {
t.Errorf("ReplyMessage = %+v; want fallback ID 315901", rm)
}
}
func TestParseEvent_Form_NoReply_NilReplyMessage(t *testing.T) {
if rm := parseForm(t, buildBitrixForm()).Params.ReplyMessage; rm != nil {
t.Errorf("non-reply event should have nil ReplyMessage, got %+v", rm)
}
}
func TestParseEvent_JSON_ReplyMessage(t *testing.T) {
body := `{
"event": "ONIMBOTMESSAGEADD",
"data": {
"PARAMS": {
"MESSAGE_ID": "500",
"DIALOG_ID": "chat9",
"FROM_USER_ID": "7",
"MESSAGE": "còn hàng không",
"REPLY_MESSAGE": {"ID": 315900, "AUTHOR_ID": 62, "MESSAGE": "áo sơ mi trắng"}
}
}
}`
req := httptest.NewRequest(http.MethodPost, "/bitrix24/events", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
evt, err := ParseEvent(req)
if err != nil {
t.Fatalf("ParseEvent: %v", err)
}
rm := evt.Params.ReplyMessage
if rm == nil || rm.ID != "315900" || rm.AuthorID != "62" || rm.Text != "áo sơ mi trắng" {
t.Errorf("ReplyMessage = %+v", rm)
}
}
func TestParseEvent_JSON_ReplyID_Fallback(t *testing.T) {
body := `{
"event": "ONIMBOTMESSAGEADD",
"data": {"PARAMS": {"MESSAGE_ID": "500", "FROM_USER_ID": "7", "PARAMS": {"REPLY_ID": 315901}}}
}`
req := httptest.NewRequest(http.MethodPost, "/bitrix24/events", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
evt, err := ParseEvent(req)
if err != nil {
t.Fatalf("ParseEvent: %v", err)
}
if rm := evt.Params.ReplyMessage; rm == nil || rm.ID != "315901" {
t.Errorf("ReplyMessage = %+v; want fallback ID 315901", rm)
}
}