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

197 lines
5.5 KiB
Go

package discovery
import (
"context"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
)
func (s *Service) relatedProfileIDs(ctx context.Context, externalUserID, profileID string) ([]string, error) {
var groupID *string
err := s.db.QueryRow(ctx, `
SELECT person_group_id::text
FROM contact_discovered_profiles p
JOIN users u ON p.user_id = u.id
WHERE u.external_id = $1 AND p.id = $2::uuid
`, externalUserID, profileID).Scan(&groupID)
if err != nil {
if err == pgx.ErrNoRows {
return nil, fmt.Errorf("profile not found")
}
return nil, err
}
if groupID != nil && strings.TrimSpace(*groupID) != "" {
rows, err := s.db.Query(ctx, `
SELECT p.id::text
FROM contact_discovered_profiles p
JOIN users u ON p.user_id = u.id
WHERE u.external_id = $1 AND p.person_group_id = $2::uuid
`, externalUserID, *groupID)
if err != nil {
return nil, err
}
defer rows.Close()
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
if len(ids) > 0 {
return ids, rows.Err()
}
}
return []string{profileID}, nil
}
func (s *Service) profileEmails(ctx context.Context, externalUserID string, profileIDs []string) ([]string, error) {
rows, err := s.db.Query(ctx, `
SELECT primary_email
FROM contact_discovered_profiles p
JOIN users u ON p.user_id = u.id
WHERE u.external_id = $1 AND p.id = ANY($2::uuid[])
`, externalUserID, profileIDs)
if err != nil {
return nil, err
}
defer rows.Close()
seen := map[string]struct{}{}
var emails []string
for rows.Next() {
var email string
if err := rows.Scan(&email); err != nil {
return nil, err
}
low := strings.ToLower(strings.TrimSpace(email))
if low == "" {
continue
}
if _, ok := seen[low]; ok {
continue
}
seen[low] = struct{}{}
emails = append(emails, low)
}
return emails, rows.Err()
}
func (s *Service) persistEmailRejections(ctx context.Context, externalUserID string, emails []string, rejectionType string) {
for _, email := range emails {
_, _ = s.db.Exec(ctx, `
INSERT INTO contact_discovery_rejections (user_id, rejection_key, rejection_type)
VALUES ((SELECT id FROM users WHERE external_id = $1), $2, $3)
ON CONFLICT DO NOTHING
`, externalUserID, "email:"+email, rejectionType)
}
}
func (s *Service) rejectPendingSuggestions(ctx context.Context, externalUserID string, profileIDs []string) {
_, _ = s.db.Exec(ctx, `
UPDATE contact_enrichment_suggestions
SET status = 'rejected', rejected_at = NOW()
WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
AND profile_id = ANY($2::uuid[])
AND status = 'pending'
`, externalUserID, profileIDs)
}
func (s *Service) IgnoreProfile(ctx context.Context, externalUserID, profileID string) ([]string, error) {
ids, err := s.relatedProfileIDs(ctx, externalUserID, profileID)
if err != nil {
return nil, err
}
_, err = s.db.Exec(ctx, `
UPDATE contact_discovered_profiles
SET status = 'ignored', ignored_at = NOW(), updated_at = NOW()
WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
AND id = ANY($2::uuid[])
`, externalUserID, ids)
if err != nil {
return nil, err
}
emails, err := s.profileEmails(ctx, externalUserID, ids)
if err != nil {
return nil, err
}
s.persistEmailRejections(ctx, externalUserID, emails, "ignore")
s.rejectPendingSuggestions(ctx, externalUserID, ids)
return emails, nil
}
func (s *Service) BlockProfile(ctx context.Context, externalUserID, profileID string) ([]string, error) {
ids, err := s.relatedProfileIDs(ctx, externalUserID, profileID)
if err != nil {
return nil, err
}
_, err = s.db.Exec(ctx, `
UPDATE contact_discovered_profiles
SET status = 'blocked',
blocked_at = NOW(),
user_blocked = true,
is_spam_heavy = true,
updated_at = NOW()
WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
AND id = ANY($2::uuid[])
`, externalUserID, ids)
if err != nil {
return nil, err
}
emails, err := s.profileEmails(ctx, externalUserID, ids)
if err != nil {
return nil, err
}
s.persistEmailRejections(ctx, externalUserID, emails, "block")
s.rejectPendingSuggestions(ctx, externalUserID, ids)
return emails, nil
}
func (s *Service) ListProfilesByStatus(ctx context.Context, externalUserID string, status ProfileStatus) ([]Profile, error) {
rows, err := s.db.Query(ctx, `
SELECT `+profileSelectColumns+`
FROM contact_discovered_profiles p
JOIN users u ON p.user_id = u.id
WHERE u.external_id = $1 AND p.status = $2
ORDER BY `+profileInteractionOrderBy+`
`, externalUserID, status)
if err != nil {
return nil, err
}
defer rows.Close()
var profiles []Profile
for rows.Next() {
p, err := scanProfileRow(rows)
if err != nil {
return nil, err
}
profiles = append(profiles, p)
}
if err := rows.Err(); err != nil {
return nil, err
}
s.attachSignaturesToProfiles(ctx, profiles)
return profiles, nil
}
func (s *Service) ListOtherProfileGroups(ctx context.Context, externalUserID string) ([]ProfileGroup, error) {
page, err := s.ListOtherProfileGroupsPage(ctx, externalUserID, MaxOtherGroupsPageSize, 0, "")
if err != nil {
return nil, err
}
if page.Total <= page.Limit {
return page.Groups, nil
}
all := append([]ProfileGroup{}, page.Groups...)
for offset := page.Limit; offset < page.Total; offset += MaxOtherGroupsPageSize {
next, err := s.ListOtherProfileGroupsPage(ctx, externalUserID, MaxOtherGroupsPageSize, offset, "")
if err != nil {
return nil, err
}
all = append(all, next.Groups...)
}
return all, nil
}