ultisuite-backend/internal/contacts/discovery/grouping.go
R3D347HR4Y 556d5f416d Enhance API and configuration for contact discovery and public sharing
- Introduced new endpoints for contact discovery, including scanning, listing, and managing discovered contacts.
- Implemented retry logic for handling missing DAV credentials during contact operations.
- Added public share functionality for drive API, allowing users to manage public shares, including upload, delete, and rename operations.
- Updated Nextcloud configuration to support public share links and improved error handling for public share permissions.
- Enhanced logging and validation across contact and drive APIs for better error tracking and user feedback.
- Added tests for new contact matching and ranking functionalities to ensure accuracy and reliability.
2026-06-06 20:27:02 +02:00

179 lines
4.5 KiB
Go

package discovery
import (
"context"
"sort"
"strings"
"github.com/google/uuid"
)
var consumerMailDomains = map[string]struct{}{
"gmail.com": {}, "googlemail.com": {}, "outlook.com": {}, "hotmail.com": {},
"live.com": {}, "msn.com": {}, "yahoo.com": {}, "yahoo.fr": {},
"icloud.com": {}, "me.com": {}, "mac.com": {}, "proton.me": {}, "protonmail.com": {},
"aol.com": {}, "gmx.com": {}, "gmx.fr": {}, "mail.com": {}, "zoho.com": {},
"yandex.com": {}, "yandex.ru": {}, "fastmail.com": {},
}
func isConsumerMailDomain(domain string) bool {
domain = strings.ToLower(strings.TrimSpace(domain))
if domain == "" {
return true
}
if _, ok := consumerMailDomains[domain]; ok {
return true
}
return false
}
func personGroupKey(p Profile) string {
if p.EnrichedData != nil {
fn := strings.TrimSpace(p.EnrichedData.FirstName)
ln := strings.TrimSpace(p.EnrichedData.LastName)
if fn != "" || ln != "" {
return strings.ToLower(strings.TrimSpace(fn + " " + ln))
}
}
dn := strings.TrimSpace(p.DisplayName)
if dn != "" && !strings.Contains(dn, "@") {
return strings.ToLower(dn)
}
return ""
}
func groupProfiles(profiles []Profile) []ProfileGroup {
if len(profiles) == 0 {
return nil
}
buckets := map[string][]Profile{}
var solo []Profile
for _, p := range profiles {
key := personGroupKey(p)
if key == "" {
solo = append(solo, p)
continue
}
buckets[key] = append(buckets[key], p)
}
var groups []ProfileGroup
for key, list := range buckets {
if len(list) == 1 {
solo = append(solo, list[0])
continue
}
groups = append(groups, mergeProfileGroup(key, list))
}
for _, p := range solo {
groups = append(groups, mergeProfileGroup(p.ID, []Profile{p}))
}
sort.Slice(groups, func(i, j int) bool {
return compareProfileGroupsByInteraction(groups[i], groups[j])
})
return groups
}
func mergeProfileGroup(key string, profiles []Profile) ProfileGroup {
sort.Slice(profiles, func(i, j int) bool {
return compareProfilesByInteraction(profiles[i], profiles[j])
})
primary := profiles[0]
ids := make([]string, 0, len(profiles))
emailSeen := map[string]struct{}{}
var allEmails []EmailEntry
accountMap := map[string]*AccountDetection{}
totalMessages := 0
for _, p := range profiles {
ids = append(ids, p.ID)
totalMessages += p.MessageCount
for _, e := range p.AllEmails {
low := strings.ToLower(strings.TrimSpace(e.Email))
if low == "" {
continue
}
if _, ok := emailSeen[low]; ok {
continue
}
emailSeen[low] = struct{}{}
allEmails = append(allEmails, e)
}
if _, ok := emailSeen[strings.ToLower(primary.PrimaryEmail)]; !ok {
emailSeen[strings.ToLower(primary.PrimaryEmail)] = struct{}{}
}
for _, a := range p.DetectedInAccounts {
hit, ok := accountMap[a.AccountID]
if !ok {
cp := a
accountMap[a.AccountID] = &cp
continue
}
hit.MessageCount += a.MessageCount
}
}
var accounts []AccountDetection
for _, a := range accountMap {
accounts = append(accounts, *a)
}
sort.Slice(accounts, func(i, j int) bool {
return accounts[i].MessageCount > accounts[j].MessageCount
})
merged := primary
merged.AllEmails = allEmails
merged.DetectedInAccounts = accounts
merged.MessageCount = totalMessages
merged.AllEmails = allEmails
merged.DetectedInAccounts = accounts
merged.MessageCount = totalMessages
return ProfileGroup{
GroupKey: key,
ProfileIDs: ids,
Profile: merged,
Profiles: profiles,
DisplayName: profileDisplayName(merged),
PrimaryEmail: merged.PrimaryEmail,
MessageCount: totalMessages,
}
}
func profileDisplayName(p Profile) string {
if p.EnrichedData != nil {
fn := strings.TrimSpace(p.EnrichedData.FirstName)
ln := strings.TrimSpace(p.EnrichedData.LastName)
if fn != "" || ln != "" {
return strings.TrimSpace(fn + " " + ln)
}
}
if strings.TrimSpace(p.DisplayName) != "" {
return strings.TrimSpace(p.DisplayName)
}
return p.PrimaryEmail
}
func (s *Service) assignPersonGroups(ctx context.Context, externalUserID string) error {
profiles, err := s.ListProfilesByStatus(ctx, externalUserID, ProfileSuggested)
if err != nil {
return err
}
groups := groupProfiles(profiles)
for _, g := range groups {
if len(g.ProfileIDs) < 2 {
continue
}
groupID := uuid.NewString()
for _, id := range g.ProfileIDs {
_, _ = s.db.Exec(ctx, `
UPDATE contact_discovered_profiles
SET person_group_id = $3::uuid, updated_at = NOW()
WHERE id = $2::uuid AND user_id = (SELECT id FROM users WHERE external_id = $1)
`, externalUserID, id, groupID)
}
}
return nil
}