- 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.
161 lines
4.0 KiB
Go
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
|
|
}
|