feat(search): align agent sender filters
This commit is contained in:
@@ -501,7 +501,7 @@ func applyMessageFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
if filter.AdvancedSearchEnabled {
|
||||
// Sender and explicit inbox filters are Chatwoot advanced-search filters.
|
||||
if filter.SenderType != "" {
|
||||
q = q.Where("sender_type = ?", filter.SenderType)
|
||||
q = q.Where("sender_type IN ?", searchSenderTypeAliases(filter.SenderType))
|
||||
}
|
||||
if filter.SenderID != nil {
|
||||
q = q.Where("sender_id = ?", *filter.SenderID)
|
||||
@@ -525,6 +525,19 @@ func applyMessageFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
return q
|
||||
}
|
||||
|
||||
func searchSenderTypeAliases(senderType string) []string {
|
||||
switch strings.ToLower(strings.TrimSpace(senderType)) {
|
||||
case "agent", "user":
|
||||
return []string{"agent", "user", "User"}
|
||||
case "contact":
|
||||
return []string{"contact", "Contact"}
|
||||
case "bot":
|
||||
return []string{"bot", "Bot"}
|
||||
default:
|
||||
return []string{senderType}
|
||||
}
|
||||
}
|
||||
|
||||
func applySearchInboxAccess(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
if filter == nil || !filter.EnforceInboxAccess {
|
||||
return q
|
||||
|
||||
@@ -294,6 +294,33 @@ func TestSearchRepo_SearchMessages_SenderFilterRequiresAdvancedSearch(t *testing
|
||||
assert.Equal(t, contactMsg.ID, results[0].ID)
|
||||
}
|
||||
|
||||
func TestSearchRepo_SearchMessages_AgentFromFilterMatchesStoredUserSenderTypes(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
conv := createTestConversationForSearch(t, db, account.ID, "open", "")
|
||||
userID := uint(77)
|
||||
|
||||
upperUserMsg := createTestMessageForSearch(t, db, account.ID, conv.ID, "upper user message", "outgoing")
|
||||
require.NoError(t, db.Model(upperUserMsg).Updates(map[string]any{"sender_type": "User", "sender_id": userID}).Error)
|
||||
lowerUserMsg := createTestMessageForSearch(t, db, account.ID, conv.ID, "lower user message", "outgoing")
|
||||
require.NoError(t, db.Model(lowerUserMsg).Updates(map[string]any{"sender_type": "user", "sender_id": userID}).Error)
|
||||
contactMsg := createTestMessageForSearch(t, db, account.ID, conv.ID, "contact message", "incoming")
|
||||
require.NoError(t, db.Model(contactMsg).Updates(map[string]any{"sender_type": "contact", "sender_id": userID}).Error)
|
||||
|
||||
filter := &search.SearchFilter{Page: 1, PerPage: 25, AdvancedSearchEnabled: true, SenderType: "agent", SenderID: &userID}
|
||||
results, total, err := repo.SearchMessages(context.Background(), account.ID, "", filter)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
require.Len(t, results, 2)
|
||||
ids := []uint{results[0].ID, results[1].ID}
|
||||
assert.Contains(t, ids, upperUserMsg.ID)
|
||||
assert.Contains(t, ids, lowerUserMsg.ID)
|
||||
assert.NotContains(t, ids, contactMsg.ID)
|
||||
}
|
||||
|
||||
// ========== SearchConversations DB tests (PG only for ILIKE) ==========
|
||||
|
||||
func TestSearchRepo_SearchConversations_EmptyQuery(t *testing.T) {
|
||||
|
||||
@@ -214,7 +214,7 @@ func (e *MeiliSearchEngine) filterExpression(accountID uint, docType SearchResul
|
||||
parts = append(parts, fmt.Sprintf("message_type = %q", filter.MessageType))
|
||||
}
|
||||
if filter.AdvancedSearchEnabled && filter.SenderType != "" {
|
||||
parts = append(parts, fmt.Sprintf("sender_type = %q", filter.SenderType))
|
||||
parts = append(parts, meiliSenderTypeFilter(filter.SenderType))
|
||||
}
|
||||
if filter.AdvancedSearchEnabled && filter.SenderID != nil {
|
||||
parts = append(parts, fmt.Sprintf("sender_id = %d", *filter.SenderID))
|
||||
@@ -274,6 +274,31 @@ func (e *MeiliSearchEngine) indexName(docType SearchResultType) string {
|
||||
return e.indexPrefix + strings.ReplaceAll(string(docType), "_", "_") + "s"
|
||||
}
|
||||
|
||||
func meiliSenderTypeFilter(senderType string) string {
|
||||
aliases := senderTypeAliases(senderType)
|
||||
if len(aliases) == 1 {
|
||||
return fmt.Sprintf("sender_type = %q", aliases[0])
|
||||
}
|
||||
parts := make([]string, 0, len(aliases))
|
||||
for _, alias := range aliases {
|
||||
parts = append(parts, fmt.Sprintf("sender_type = %q", alias))
|
||||
}
|
||||
return "(" + strings.Join(parts, " OR ") + ")"
|
||||
}
|
||||
|
||||
func senderTypeAliases(senderType string) []string {
|
||||
switch strings.ToLower(strings.TrimSpace(senderType)) {
|
||||
case "agent", "user":
|
||||
return []string{"agent", "user", "User"}
|
||||
case "contact":
|
||||
return []string{"contact", "Contact"}
|
||||
case "bot":
|
||||
return []string{"bot", "Bot"}
|
||||
default:
|
||||
return []string{senderType}
|
||||
}
|
||||
}
|
||||
|
||||
func appendListFilter(parts []string, field string, values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return parts
|
||||
|
||||
@@ -153,7 +153,25 @@ func TestMeiliSearchEngine_SearchSendsMessageSenderIDFilter(t *testing.T) {
|
||||
_, err := engine.Search(context.Background(), 42, "hello", filter)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "account_id = 42 AND sender_type = \"contact\" AND sender_id = 77", requestBody["filter"])
|
||||
assert.Equal(t, "account_id = 42 AND (sender_type = \"contact\" OR sender_type = \"Contact\") AND sender_id = 77", requestBody["filter"])
|
||||
}
|
||||
|
||||
func TestMeiliSearchEngine_SearchSendsAgentSenderAliases(t *testing.T) {
|
||||
var requestBody map[string]interface{}
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
require.Equal(t, "/indexes/gochat_messages/search", r.URL.Path)
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&requestBody))
|
||||
return jsonResponse(http.StatusOK, `{"hits":[],"estimatedTotalHits":0}`), nil
|
||||
})
|
||||
|
||||
engine := NewMeiliSearchEngine(EngineConfig{Host: "http://meili.test", IndexPrefix: "gochat_"})
|
||||
engine.client.SetTransport(transport)
|
||||
senderID := uint(77)
|
||||
filter := &SearchFilter{Page: 1, PerPage: 10, Types: []SearchResultType{ResultTypeMessage}, AdvancedSearchEnabled: true, SenderType: "agent", SenderID: &senderID}
|
||||
_, err := engine.Search(context.Background(), 42, "hello", filter)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "account_id = 42 AND (sender_type = \"agent\" OR sender_type = \"user\" OR sender_type = \"User\") AND sender_id = 77", requestBody["filter"])
|
||||
}
|
||||
|
||||
func TestMeiliSearchEngine_SearchSkipsAdvancedFiltersWhenFeatureDisabled(t *testing.T) {
|
||||
|
||||
@@ -189,3 +189,18 @@ func TestParseSearchFilter_ChatwootSearchParams(t *testing.T) {
|
||||
assert.Equal(t, uint(7), *sf.InboxID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchFilter_ChatwootAgentFromParam(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = &http.Request{
|
||||
URL: &url.URL{Path: "/search", RawQuery: "from=agent:77"},
|
||||
}
|
||||
|
||||
sf := ParseSearchFilter(c)
|
||||
|
||||
assert.Equal(t, "agent", sf.SenderType)
|
||||
if assert.NotNil(t, sf.SenderID) {
|
||||
assert.Equal(t, uint(77), *sf.SenderID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user