- 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.
121 lines
2.8 KiB
Go
121 lines
2.8 KiB
Go
package contacts
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/nextcloud"
|
|
)
|
|
|
|
func normalizeContactSearchText(value string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(value))
|
|
for _, r := range strings.ToLower(strings.TrimSpace(value)) {
|
|
if unicode.IsMark(r) {
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func queryTokens(query string) []string {
|
|
n := normalizeContactSearchText(query)
|
|
if n == "" {
|
|
return nil
|
|
}
|
|
return strings.Fields(n)
|
|
}
|
|
|
|
func fieldMatchScore(haystack, needle string) float64 {
|
|
h := normalizeContactSearchText(haystack)
|
|
n := normalizeContactSearchText(needle)
|
|
if n == "" || !strings.Contains(h, n) {
|
|
return 0
|
|
}
|
|
if h == n {
|
|
return 1
|
|
}
|
|
if strings.HasPrefix(h, n) {
|
|
return 0.95 + 0.05*float64(len(n))/float64(max(len(h), 1))
|
|
}
|
|
for _, word := range strings.FieldsFunc(h, func(r rune) bool {
|
|
return r == ' ' || r == '@' || r == '.' || r == '_' || r == '+' || r == '-'
|
|
}) {
|
|
if word == "" {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(word, n) {
|
|
return 0.88 + 0.07*float64(len(n))/float64(max(len(word), 1))
|
|
}
|
|
}
|
|
idx := strings.Index(h, n)
|
|
positionBonus := 1 - float64(idx)/float64(max(len(h), 1))*0.35
|
|
lengthBonus := float64(len(n)) / float64(max(len(h), 1))
|
|
return 0.42 + 0.28*positionBonus + 0.22*lengthBonus
|
|
}
|
|
|
|
func contactSearchFields(c nextcloud.Contact) []string {
|
|
out := make([]string, 0, 4)
|
|
for _, v := range []string{c.FullName, c.Email, c.Phone, c.Org} {
|
|
if strings.TrimSpace(v) != "" {
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func contactQueryMatchScore(c nextcloud.Contact, query string) float64 {
|
|
fields := contactSearchFields(c)
|
|
tokens := queryTokens(query)
|
|
if len(tokens) == 0 {
|
|
return 0
|
|
}
|
|
|
|
var total float64
|
|
for _, token := range tokens {
|
|
best := 0.0
|
|
for _, field := range fields {
|
|
best = max(best, fieldMatchScore(field, token))
|
|
best = max(best, fieldMatchScore(field, query))
|
|
}
|
|
if best == 0 {
|
|
return 0
|
|
}
|
|
total += best
|
|
}
|
|
return total / float64(len(tokens))
|
|
}
|
|
|
|
func rankContactsByQuery(contacts []nextcloud.Contact, query string) []nextcloud.Contact {
|
|
query = strings.TrimSpace(query)
|
|
if query == "" {
|
|
return contacts
|
|
}
|
|
|
|
type scored struct {
|
|
contact nextcloud.Contact
|
|
score float64
|
|
}
|
|
matches := make([]scored, 0, len(contacts))
|
|
for _, c := range contacts {
|
|
if score := contactQueryMatchScore(c, query); score > 0 {
|
|
matches = append(matches, scored{contact: c, score: score})
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(matches, func(i, j int) bool {
|
|
if matches[i].score != matches[j].score {
|
|
return matches[i].score > matches[j].score
|
|
}
|
|
return strings.ToLower(matches[i].contact.FullName) < strings.ToLower(matches[j].contact.FullName)
|
|
})
|
|
|
|
out := make([]nextcloud.Contact, len(matches))
|
|
for i, m := range matches {
|
|
out[i] = m.contact
|
|
}
|
|
return out
|
|
}
|