ultisuite-backend/internal/contacts/discovery/match.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

161 lines
4.0 KiB
Go

package discovery
import (
"context"
"strings"
"unicode"
)
func (s *Service) loadNCContacts(ctx context.Context, ncUserID, bookID string) []ncContact {
if s.nc == nil || ncUserID == "" {
return nil
}
if bookID == "" {
bookID = "contacts"
}
path := "/addressbooks/" + ncUserID + "/" + bookID + "/"
contacts, err := s.nc.ListContacts(ctx, ncUserID, path)
if err != nil {
s.logger.Warn("load nc contacts for enrichment", "error", err)
return nil
}
return contacts
}
func (s *Service) resolveDiscoveryNCContext(ctx context.Context, externalUserID string) (ncUserID, bookID string) {
bookID = "contacts"
_ = s.db.QueryRow(ctx, `
SELECT COALESCE(NULLIF(nc_user_id, ''), ''), COALESCE(NULLIF(book_id, ''), 'contacts')
FROM contact_discovery_scans
WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
ORDER BY started_at DESC
LIMIT 1
`, externalUserID).Scan(&ncUserID, &bookID)
return
}
func findExistingContact(contacts []ncContact, email string) *ncContact {
email = strings.ToLower(strings.TrimSpace(email))
for i := range contacts {
if strings.EqualFold(strings.TrimSpace(contacts[i].Email), email) {
return &contacts[i]
}
}
return nil
}
func normalizePhone(s string) string {
var b strings.Builder
for _, r := range s {
if unicode.IsDigit(r) {
b.WriteRune(r)
}
}
return b.String()
}
func phonesEqual(a, b string) bool {
na, nb := normalizePhone(a), normalizePhone(b)
if na == "" || nb == "" {
return false
}
if len(na) >= 9 && len(nb) >= 9 {
return na[len(na)-9:] == nb[len(nb)-9:]
}
return na == nb
}
func stringsEqualFoldTrim(a, b string) bool {
return strings.EqualFold(strings.TrimSpace(a), strings.TrimSpace(b))
}
func suggestionAlreadyOnContact(existing *ncContact, sug Suggestion) bool {
if existing == nil {
return false
}
val := strings.TrimSpace(sug.SuggestedValue)
if val == "" {
return true
}
switch sug.FieldPath {
case "full_name":
return stringsEqualFoldTrim(val, existing.FullName)
case "company":
return stringsEqualFoldTrim(val, existing.Org)
case "phones":
return phonesEqual(val, existing.Phone)
case "emails":
return strings.EqualFold(val, strings.TrimSpace(existing.Email))
default:
return false
}
}
func filterRedundantSuggestions(suggestions []Suggestion, contacts []ncContact) []Suggestion {
if len(suggestions) == 0 || len(contacts) == 0 {
return suggestions
}
byUID := make(map[string]*ncContact, len(contacts))
for i := range contacts {
byUID[contacts[i].UID] = &contacts[i]
}
filtered := suggestions[:0]
for _, sug := range suggestions {
if sug.SuggestionType != "enrich_contact" || sug.TargetContactUID == "" {
filtered = append(filtered, sug)
continue
}
if suggestionAlreadyOnContact(byUID[sug.TargetContactUID], sug) {
continue
}
filtered = append(filtered, sug)
}
return filtered
}
func enrichExistingContactSuggestions(profileID, contactUID string, data *EnrichedContactData, existing *ncContact) []Suggestion {
if data == nil || existing == nil {
return nil
}
var out []Suggestion
addIfMissing := func(fieldPath, value, label string) {
value = strings.TrimSpace(value)
if value == "" {
return
}
sug := Suggestion{
ProfileID: profileID,
TargetContactUID: contactUID,
SuggestionType: "enrich_contact",
FieldPath: fieldPath,
SuggestedValue: value,
SuggestedLabel: label,
Confidence: 0.7,
Status: SuggestionPending,
}
if suggestionAlreadyOnContact(existing, sug) {
return
}
out = append(out, sug)
}
fullName := strings.TrimSpace(data.FirstName + " " + data.LastName)
addIfMissing("full_name", fullName, "")
addIfMissing("company", data.Company, "")
addIfMissing("job_title", data.JobTitle, "")
for _, p := range data.Phones {
addIfMissing("phones", p.Value, p.Label)
}
for _, e := range data.Emails {
if !strings.EqualFold(e.Value, existing.Email) {
addIfMissing("emails", e.Value, e.Label)
}
}
for _, sp := range data.SocialProfiles {
addIfMissing("social_profiles", sp.Value, sp.Label)
}
return out
}