653 lines
21 KiB
Go
653 lines
21 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/search"
|
|
)
|
|
|
|
// RepoSearchFilter holds filter parameters for search queries at the repository level.
|
|
// This is a repository-level mirror of the search.SearchFilter struct to avoid
|
|
// an import cycle between internal/search and internal/repository.
|
|
type RepoSearchFilter struct {
|
|
SearchMode search.SearchMode // ilike or trigram
|
|
Status []string
|
|
Priority []string
|
|
AssigneeID *uint
|
|
TeamID *uint
|
|
InboxID *uint
|
|
Labels []string
|
|
ContactSource string
|
|
ContactResolvedScope bool
|
|
ContactCRMV2 bool
|
|
AdvancedSearchEnabled bool
|
|
MessageType string
|
|
SenderType string
|
|
SenderID *uint
|
|
ContentType string
|
|
Private *bool
|
|
EnforceInboxAccess bool
|
|
AccessibleInboxIDs []uint
|
|
MessageCreatedAfter *time.Time
|
|
DateFrom *time.Time
|
|
DateTo *time.Time
|
|
SortBy string
|
|
SortOrder string
|
|
Page int
|
|
PerPage int
|
|
|
|
// Article-specific filters (Knowledge Base)
|
|
PortalID *uint
|
|
ArticleStatus string
|
|
ArticleLocale string
|
|
}
|
|
|
|
// IsTrigram returns true if the search mode uses pg_trgm fuzzy matching.
|
|
func (f *RepoSearchFilter) IsTrigram() bool {
|
|
return f.SearchMode == search.SearchModeTrigram
|
|
}
|
|
|
|
// Offset returns the calculated offset from Page and PerPage.
|
|
func (f *RepoSearchFilter) Offset() int {
|
|
return (f.Page - 1) * f.PerPage
|
|
}
|
|
|
|
// OrderClause returns the SQL ORDER BY clause string based on SortBy and SortOrder.
|
|
func (f *RepoSearchFilter) OrderClause() string {
|
|
col := f.SortBy
|
|
if col == "" {
|
|
col = "created_at"
|
|
}
|
|
dir := f.SortOrder
|
|
if dir != "asc" {
|
|
dir = "desc"
|
|
}
|
|
// Map common aliases to actual column names
|
|
switch col {
|
|
case "last_activity":
|
|
col = "last_activity_at"
|
|
case "created":
|
|
col = "created_at"
|
|
case "updated":
|
|
col = "updated_at"
|
|
}
|
|
return col + " " + dir
|
|
}
|
|
|
|
// searchFilterToRepo converts a search.SearchFilter to a RepoSearchFilter.
|
|
// This is used by the adapter methods that satisfy search.SearchRepoInterface.
|
|
func searchFilterToRepo(f *search.SearchFilter) *RepoSearchFilter {
|
|
if f == nil {
|
|
return &RepoSearchFilter{SearchMode: search.SearchModeILike, Page: 1, PerPage: 25, SortBy: "created_at", SortOrder: "desc"}
|
|
}
|
|
// Default SearchMode to ILIKE if not explicitly set
|
|
mode := f.SearchMode
|
|
if mode == "" {
|
|
mode = search.SearchModeILike
|
|
}
|
|
return &RepoSearchFilter{
|
|
SearchMode: mode,
|
|
Status: f.Status,
|
|
Priority: f.Priority,
|
|
AssigneeID: f.AssigneeID,
|
|
TeamID: f.TeamID,
|
|
InboxID: f.InboxID,
|
|
Labels: f.Labels,
|
|
ContactSource: f.ContactSource,
|
|
ContactResolvedScope: f.ContactResolvedScope,
|
|
ContactCRMV2: f.ContactCRMV2,
|
|
AdvancedSearchEnabled: f.AdvancedSearchEnabled,
|
|
MessageType: f.MessageType,
|
|
SenderType: f.SenderType,
|
|
SenderID: f.SenderID,
|
|
ContentType: f.ContentType,
|
|
Private: f.Private,
|
|
EnforceInboxAccess: f.EnforceInboxAccess,
|
|
AccessibleInboxIDs: append([]uint(nil), f.AccessibleInboxIDs...),
|
|
MessageCreatedAfter: f.MessageCreatedAfter,
|
|
DateFrom: f.DateFrom,
|
|
DateTo: f.DateTo,
|
|
SortBy: f.SortBy,
|
|
SortOrder: f.SortOrder,
|
|
Page: f.Page,
|
|
PerPage: f.PerPage,
|
|
PortalID: f.PortalID,
|
|
ArticleStatus: f.ArticleStatus,
|
|
ArticleLocale: f.ArticleLocale,
|
|
}
|
|
}
|
|
|
|
// SearchRepo provides unified search across conversations, messages, and contacts
|
|
// with advanced filtering and sorting support.
|
|
// Reference: Chatwoot GlobalSearchService + ConversationFilterService
|
|
type SearchRepo struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewSearchRepo creates a new SearchRepo.
|
|
func NewSearchRepo(db *gorm.DB) *SearchRepo {
|
|
return &SearchRepo{db: db}
|
|
}
|
|
|
|
// SearchConversations searches conversations with advanced filters.
|
|
// This method satisfies search.SearchRepoInterface by accepting *search.SearchFilter
|
|
// and converting it to *RepoSearchFilter internally.
|
|
// Reference: Chatwoot SearchService#filter_conversations.
|
|
func (r *SearchRepo) SearchConversations(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Conversation, int64, error) {
|
|
repoFilter := searchFilterToRepo(filter)
|
|
return r.searchConversationsInternal(ctx, accountID, query, repoFilter)
|
|
}
|
|
|
|
// searchConversationsInternal is the internal implementation using *RepoSearchFilter.
|
|
func (r *SearchRepo) searchConversationsInternal(ctx context.Context, accountID uint, query string, filter *RepoSearchFilter) ([]model.Conversation, int64, error) {
|
|
var conversations []model.Conversation
|
|
var total int64
|
|
|
|
q := r.db.WithContext(ctx).Model(&model.Conversation{}).Where("conversations.account_id = ?", accountID)
|
|
|
|
// Chatwoot conversation search matches display_id and contact identity fields.
|
|
if query != "" {
|
|
if filter.IsTrigram() {
|
|
q = q.Joins("INNER JOIN contacts ON conversations.contact_id = contacts.id").Where(
|
|
"CAST(conversations.display_id AS TEXT) % ? OR contacts.name % ? OR contacts.email % ? OR contacts.phone_number % ? OR contacts.identifier % ?",
|
|
query, query, query, query, query,
|
|
)
|
|
} else {
|
|
likeQuery := "%" + query + "%"
|
|
if r.db.Dialector.Name() == "sqlite" {
|
|
likeQuery = "%" + strings.ToLower(query) + "%"
|
|
q = q.Where(
|
|
`EXISTS (
|
|
SELECT 1 FROM contacts
|
|
WHERE contacts.id = conversations.contact_id
|
|
AND contacts.account_id = conversations.account_id
|
|
AND (LOWER(CAST(conversations.display_id AS TEXT)) LIKE ? OR LOWER(contacts.name) LIKE ? OR LOWER(contacts.email) LIKE ? OR LOWER(contacts.phone_number) LIKE ? OR LOWER(contacts.identifier) LIKE ?)
|
|
)`,
|
|
likeQuery, likeQuery, likeQuery, likeQuery, likeQuery,
|
|
)
|
|
} else {
|
|
q = q.Where(
|
|
`EXISTS (
|
|
SELECT 1 FROM contacts
|
|
WHERE contacts.id = conversations.contact_id
|
|
AND contacts.account_id = conversations.account_id
|
|
AND (CAST(conversations.display_id AS TEXT) ILIKE ? OR contacts.name ILIKE ? OR contacts.email ILIKE ? OR contacts.phone_number ILIKE ? OR contacts.identifier ILIKE ?)
|
|
)`,
|
|
likeQuery, likeQuery, likeQuery, likeQuery, likeQuery,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply conversation-specific filters
|
|
q = applySearchInboxAccess(q, filter)
|
|
q = applyConversationFilters(q, filter)
|
|
|
|
// Apply date range filter
|
|
q = applyDateRangeFilter(q, filter, "conversations.last_activity_at", true)
|
|
|
|
// Count
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("search conversations count: %w", err)
|
|
}
|
|
|
|
// Apply sort + pagination
|
|
orderClause := filter.OrderClause()
|
|
// Chatwoot SearchService#filter_conversations orders by created_at DESC.
|
|
if filter.SortBy == "created_at" || filter.SortBy == "" {
|
|
orderClause = "conversations.created_at DESC, conversations.id DESC"
|
|
}
|
|
|
|
err := withConversationSearchPayloadPreloads(q).
|
|
Offset(filter.Offset()).Limit(filter.PerPage).
|
|
Order(orderClause).
|
|
Find(&conversations).Error
|
|
return conversations, total, err
|
|
}
|
|
|
|
func withConversationSearchPayloadPreloads(q *gorm.DB) *gorm.DB {
|
|
return q.
|
|
Preload("Contact").
|
|
Preload("Inbox").
|
|
Preload("Assignee").
|
|
Preload("Messages", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("created_at ASC, id ASC")
|
|
})
|
|
}
|
|
|
|
// SearchMessages searches messages with advanced filters.
|
|
// This method satisfies search.SearchRepoInterface by accepting *search.SearchFilter
|
|
// and converting it to *RepoSearchFilter internally.
|
|
// Reference: Chatwoot messages search — full text search on message content
|
|
func (r *SearchRepo) SearchMessages(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Message, int64, error) {
|
|
repoFilter := searchFilterToRepo(filter)
|
|
return r.searchMessagesInternal(ctx, accountID, query, repoFilter)
|
|
}
|
|
|
|
// searchMessagesInternal is the internal implementation using *RepoSearchFilter.
|
|
func (r *SearchRepo) searchMessagesInternal(ctx context.Context, accountID uint, query string, filter *RepoSearchFilter) ([]model.Message, int64, error) {
|
|
var messages []model.Message
|
|
var total int64
|
|
|
|
q := r.db.WithContext(ctx).Model(&model.Message{}).Where("account_id = ?", accountID)
|
|
q = applyMessageRecentBase(q, filter)
|
|
|
|
// Text search on message content — ILIKE or pg_trgm fuzzy match
|
|
if query != "" {
|
|
if filter.IsTrigram() {
|
|
// pg_trgm fuzzy match: uses % operator (similarity threshold default 0.3)
|
|
q = q.Where("content % ?", query)
|
|
} else {
|
|
// ILIKE substring match
|
|
likeQuery := "%" + query + "%"
|
|
q = q.Where("content ILIKE ?", likeQuery)
|
|
}
|
|
}
|
|
|
|
// Apply message-specific filters
|
|
q = applySearchInboxAccess(q, filter)
|
|
q = applyMessageFilters(q, filter)
|
|
|
|
// Apply date range filter
|
|
q = applyDateRangeFilter(q, filter, "created_at", false)
|
|
|
|
// Count
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("search messages count: %w", err)
|
|
}
|
|
|
|
// Apply sort + pagination
|
|
err := q.Offset(filter.Offset()).Limit(filter.PerPage).
|
|
Order(filter.OrderClause() + ", id DESC").
|
|
Find(&messages).Error
|
|
return messages, total, err
|
|
}
|
|
|
|
// SearchContacts searches contacts with advanced filters.
|
|
// This method satisfies search.SearchRepoInterface by accepting *search.SearchFilter
|
|
// and converting it to *RepoSearchFilter internally.
|
|
// Reference: Chatwoot contacts#search — name, email, phone, identifier.
|
|
func (r *SearchRepo) SearchContacts(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Contact, int64, error) {
|
|
repoFilter := searchFilterToRepo(filter)
|
|
return r.searchContactsInternal(ctx, accountID, query, repoFilter)
|
|
}
|
|
|
|
func (r *SearchRepo) AccountFeatureEnabled(ctx context.Context, accountID uint, flag string) bool {
|
|
var account model.Account
|
|
if err := r.db.WithContext(ctx).Select("feature_flags").Where("id = ?", accountID).First(&account).Error; err != nil {
|
|
return false
|
|
}
|
|
return contactFeatureFlagEnabled(account.FeatureFlags, flag)
|
|
}
|
|
|
|
func (r *SearchRepo) SearchAccessForAccount(ctx context.Context, accountID, userID uint) (search.SearchAccess, error) {
|
|
var accountUser model.AccountUser
|
|
if err := r.db.WithContext(ctx).
|
|
Where("account_id = ? AND user_id = ?", accountID, userID).
|
|
First(&accountUser).Error; err != nil {
|
|
return search.SearchAccess{}, err
|
|
}
|
|
if accountUser.IsAdministrator() {
|
|
return search.SearchAccess{SkipInboxFiltering: true}, nil
|
|
}
|
|
|
|
var totalInboxes int64
|
|
if err := r.db.WithContext(ctx).Model(&model.Inbox{}).Where("account_id = ?", accountID).Count(&totalInboxes).Error; err != nil {
|
|
return search.SearchAccess{}, err
|
|
}
|
|
|
|
var inboxIDs []uint
|
|
if err := r.db.WithContext(ctx).Model(&model.InboxMember{}).
|
|
Joins("JOIN inboxes ON inboxes.id = inbox_members.inbox_id").
|
|
Where("inboxes.account_id = ? AND inbox_members.user_id = ?", accountID, userID).
|
|
Order("inbox_members.inbox_id ASC").
|
|
Pluck("inbox_members.inbox_id", &inboxIDs).Error; err != nil {
|
|
return search.SearchAccess{}, err
|
|
}
|
|
if totalInboxes > 0 && int64(len(inboxIDs)) == totalInboxes {
|
|
return search.SearchAccess{SkipInboxFiltering: true, InboxIDs: inboxIDs}, nil
|
|
}
|
|
return search.SearchAccess{InboxIDs: inboxIDs}, nil
|
|
}
|
|
|
|
// searchContactsInternal is the internal implementation using *RepoSearchFilter.
|
|
func (r *SearchRepo) searchContactsInternal(ctx context.Context, accountID uint, query string, filter *RepoSearchFilter) ([]model.Contact, int64, error) {
|
|
var contacts []model.Contact
|
|
var total int64
|
|
|
|
q := r.db.WithContext(ctx).Model(&model.Contact{}).Where("account_id = ?", accountID)
|
|
if filter.ContactResolvedScope {
|
|
if filter.ContactCRMV2 {
|
|
q = q.Where("contacts.contact_type = ?", "lead")
|
|
} else {
|
|
q = q.Where("(contacts.email <> '' OR contacts.phone_number <> '' OR contacts.identifier <> '')")
|
|
}
|
|
}
|
|
|
|
// Text search on contact fields — ILIKE or pg_trgm fuzzy match
|
|
if query != "" {
|
|
if filter.IsTrigram() {
|
|
// pg_trgm fuzzy match: uses % operator on name, email, phone_number, identifier
|
|
q = q.Where("name % ? OR email % ? OR phone_number % ? OR identifier % ?",
|
|
query, query, query, query)
|
|
} else {
|
|
// ILIKE substring match
|
|
likeQuery := "%" + query + "%"
|
|
q = q.Where("name ILIKE ? OR email ILIKE ? OR phone_number ILIKE ? OR identifier ILIKE ?",
|
|
likeQuery, likeQuery, likeQuery, likeQuery)
|
|
}
|
|
}
|
|
|
|
// Apply contact-specific filters
|
|
q = applyContactFilters(q, filter)
|
|
|
|
// Apply date range filter
|
|
q = applyDateRangeFilter(q, filter, "last_activity_at", true)
|
|
|
|
// Count
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("search contacts count: %w", err)
|
|
}
|
|
|
|
// Apply sort + pagination
|
|
orderClause := filter.OrderClause()
|
|
if filter.SortBy == "created_at" || filter.SortBy == "" {
|
|
orderClause = "last_activity_at DESC NULLS LAST, id DESC"
|
|
}
|
|
|
|
err := q.Offset(filter.Offset()).Limit(filter.PerPage).
|
|
Order(orderClause).
|
|
Find(&contacts).Error
|
|
return contacts, total, err
|
|
}
|
|
|
|
// SearchCompanies searches companies with advanced filters.
|
|
func (r *SearchRepo) SearchCompanies(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Company, int64, error) {
|
|
repoFilter := searchFilterToRepo(filter)
|
|
return r.searchCompaniesInternal(ctx, accountID, query, repoFilter)
|
|
}
|
|
|
|
func (r *SearchRepo) searchCompaniesInternal(ctx context.Context, accountID uint, query string, filter *RepoSearchFilter) ([]model.Company, int64, error) {
|
|
var companies []model.Company
|
|
var total int64
|
|
|
|
q := r.db.WithContext(ctx).Model(&model.Company{}).Where("account_id = ?", accountID)
|
|
if query != "" {
|
|
if filter.IsTrigram() {
|
|
q = q.Where("name % ? OR description % ? OR domain % ? OR website_url % ?", query, query, query, query)
|
|
} else {
|
|
likeQuery := "%" + query + "%"
|
|
q = q.Where("LOWER(name) LIKE LOWER(?) OR LOWER(description) LIKE LOWER(?) OR LOWER(domain) LIKE LOWER(?) OR LOWER(website_url) LIKE LOWER(?)",
|
|
likeQuery, likeQuery, likeQuery, likeQuery)
|
|
}
|
|
}
|
|
|
|
q = applyDateRangeFilter(q, filter, "created_at", false)
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("search companies count: %w", err)
|
|
}
|
|
|
|
err := q.Offset(filter.Offset()).Limit(filter.PerPage).
|
|
Order(filter.OrderClause() + ", id DESC").
|
|
Find(&companies).Error
|
|
return companies, total, err
|
|
}
|
|
|
|
// SearchArticles searches knowledge base articles with advanced filters.
|
|
// This method satisfies search.SearchRepoInterface by accepting *search.SearchFilter
|
|
// and converting it to *RepoSearchFilter internally.
|
|
// Reference: Chatwoot ArticlesController#search — full text search on title, description, content.
|
|
func (r *SearchRepo) SearchArticles(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Article, int64, error) {
|
|
repoFilter := searchFilterToRepo(filter)
|
|
return r.searchArticlesInternal(ctx, accountID, query, repoFilter)
|
|
}
|
|
|
|
// searchArticlesInternal is the internal implementation using *RepoSearchFilter.
|
|
func (r *SearchRepo) searchArticlesInternal(ctx context.Context, accountID uint, query string, filter *RepoSearchFilter) ([]model.Article, int64, error) {
|
|
var articles []model.Article
|
|
var total int64
|
|
|
|
q := r.db.WithContext(ctx).Model(&model.Article{}).Where("account_id = ?", accountID)
|
|
|
|
// Text search on article title, description, and content — ILIKE or pg_trgm fuzzy match
|
|
if query != "" {
|
|
if filter.IsTrigram() {
|
|
// pg_trgm fuzzy match: uses % operator on title, description, content
|
|
q = q.Where("title % ? OR description % ? OR content % ?",
|
|
query, query, query)
|
|
} else {
|
|
// ILIKE substring match
|
|
likeQuery := "%" + query + "%"
|
|
q = q.Where("title ILIKE ? OR description ILIKE ? OR content ILIKE ?",
|
|
likeQuery, likeQuery, likeQuery)
|
|
}
|
|
}
|
|
|
|
// Apply article-specific filters
|
|
q = applyArticleFilters(q, filter)
|
|
|
|
// Apply date range filter
|
|
q = applyDateRangeFilter(q, filter, "updated_at", false)
|
|
|
|
// Count
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("search articles count: %w", err)
|
|
}
|
|
|
|
// Sorting: articles default to updated_at DESC
|
|
orderClause := filter.OrderClause()
|
|
if filter.SortBy == "created_at" || filter.SortBy == "" {
|
|
orderClause = "updated_at DESC"
|
|
}
|
|
|
|
err := q.Offset(filter.Offset()).Limit(filter.PerPage).
|
|
Order(orderClause + ", id DESC").
|
|
Find(&articles).Error
|
|
return articles, total, err
|
|
}
|
|
|
|
// --- Filter helper functions ---
|
|
|
|
// applyConversationFilters applies conversation-specific filter conditions.
|
|
func applyConversationFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
|
if filter == nil {
|
|
return q
|
|
}
|
|
|
|
// Status filter
|
|
if len(filter.Status) > 0 {
|
|
q = q.Where("status IN ?", filter.Status)
|
|
}
|
|
|
|
// Priority filter
|
|
if len(filter.Priority) > 0 {
|
|
q = q.Where("priority IN ?", filter.Priority)
|
|
}
|
|
|
|
// Assignee ID filter
|
|
if filter.AssigneeID != nil {
|
|
q = q.Where("assignee_id = ?", *filter.AssigneeID)
|
|
}
|
|
|
|
// Team ID filter
|
|
if filter.TeamID != nil {
|
|
q = q.Where("team_id = ?", *filter.TeamID)
|
|
}
|
|
|
|
// Inbox ID filter
|
|
if filter.InboxID != nil && searchInboxIDAllowed(filter, *filter.InboxID) {
|
|
q = q.Where("inbox_id = ?", *filter.InboxID)
|
|
}
|
|
|
|
// Labels filter — uses text containment for PostgreSQL
|
|
// Reference: Chatwoot filters by labels array overlap
|
|
if len(filter.Labels) > 0 {
|
|
for _, label := range filter.Labels {
|
|
// Labels column is text (JSON array stored as string)
|
|
// Use ILIKE to match label within the text representation
|
|
q = q.Where("labels ILIKE ?", fmt.Sprintf(`%%%s%%`, strings.ToLower(label)))
|
|
}
|
|
}
|
|
|
|
return q
|
|
}
|
|
|
|
// applyMessageFilters applies message-specific filter conditions.
|
|
func applyMessageFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
|
if filter == nil {
|
|
return q
|
|
}
|
|
|
|
// Message type filter
|
|
if filter.MessageType != "" {
|
|
q = q.Where("message_type = ?", filter.MessageType)
|
|
}
|
|
|
|
if filter.AdvancedSearchEnabled {
|
|
// Sender and explicit inbox filters are Chatwoot advanced-search filters.
|
|
if filter.SenderType != "" {
|
|
q = q.Where("sender_type IN ?", searchSenderTypeAliases(filter.SenderType))
|
|
}
|
|
if filter.SenderID != nil {
|
|
q = q.Where("sender_id = ?", *filter.SenderID)
|
|
}
|
|
|
|
if filter.InboxID != nil && searchInboxIDAllowed(filter, *filter.InboxID) {
|
|
q = q.Where("inbox_id = ?", *filter.InboxID)
|
|
}
|
|
}
|
|
|
|
// Content type filter
|
|
if filter.ContentType != "" {
|
|
q = q.Where("content_type = ?", filter.ContentType)
|
|
}
|
|
|
|
// Private flag filter
|
|
if filter.Private != nil {
|
|
q = q.Where("private = ?", *filter.Private)
|
|
}
|
|
|
|
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
|
|
}
|
|
if len(filter.AccessibleInboxIDs) == 0 {
|
|
return q.Where("1 = 0")
|
|
}
|
|
return q.Where("inbox_id IN ?", filter.AccessibleInboxIDs)
|
|
}
|
|
|
|
func applyMessageRecentBase(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
|
if filter == nil || filter.MessageCreatedAfter == nil {
|
|
return q
|
|
}
|
|
return q.Where("created_at >= ?", *filter.MessageCreatedAfter)
|
|
}
|
|
|
|
func searchInboxIDAllowed(filter *RepoSearchFilter, inboxID uint) bool {
|
|
if filter == nil || !filter.EnforceInboxAccess {
|
|
return true
|
|
}
|
|
for _, id := range filter.AccessibleInboxIDs {
|
|
if id == inboxID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// applyContactFilters applies contact-specific filter conditions.
|
|
func applyContactFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
|
if filter == nil {
|
|
return q
|
|
}
|
|
|
|
// Contact source filter
|
|
if filter.ContactSource != "" {
|
|
q = q.Where("source = ?", filter.ContactSource)
|
|
}
|
|
|
|
return q
|
|
}
|
|
|
|
// applyArticleFilters applies article-specific filter conditions.
|
|
func applyArticleFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
|
if filter == nil {
|
|
return q
|
|
}
|
|
|
|
// Portal ID filter
|
|
if filter.PortalID != nil {
|
|
q = q.Where("portal_id = ?", *filter.PortalID)
|
|
}
|
|
|
|
// Article status filter
|
|
if filter.ArticleStatus != "" {
|
|
q = q.Where("status = ?", filter.ArticleStatus)
|
|
}
|
|
|
|
// Locale filter
|
|
if filter.ArticleLocale != "" {
|
|
q = q.Where("locale = ?", filter.ArticleLocale)
|
|
}
|
|
|
|
// Author filter (reuse AssigneeID field for author_id in article context)
|
|
if filter.AssigneeID != nil {
|
|
q = q.Where("author_id = ?", *filter.AssigneeID)
|
|
}
|
|
|
|
return q
|
|
}
|
|
|
|
// applyDateRangeFilter applies Chatwoot SearchService time filters using the
|
|
// entity-specific column selected by the caller.
|
|
func applyDateRangeFilter(q *gorm.DB, filter *RepoSearchFilter, columnName string, unixSeconds bool) *gorm.DB {
|
|
if filter == nil || !filter.AdvancedSearchEnabled {
|
|
return q
|
|
}
|
|
if columnName == "" {
|
|
columnName = "created_at"
|
|
}
|
|
|
|
if filter.DateFrom != nil {
|
|
q = q.Where(columnName+" >= ?", dateRangeValue(filter.DateFrom, unixSeconds))
|
|
}
|
|
if filter.DateTo != nil {
|
|
q = q.Where(columnName+" <= ?", dateRangeValue(filter.DateTo, unixSeconds))
|
|
}
|
|
|
|
return q
|
|
}
|
|
|
|
func dateRangeValue(value *time.Time, unixSeconds bool) any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
if unixSeconds {
|
|
return value.Unix()
|
|
}
|
|
return value
|
|
}
|