- 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.
197 lines
5.5 KiB
Go
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
|
|
}
|