- 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.
179 lines
4.5 KiB
Go
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
|
|
}
|