ultisuite-backend/internal/api/contacts/import.go
R3D347HR4Y f97988b51f
Some checks failed
CI / Go tests (push) Has been cancelled
CI / Integration tests (push) Has been cancelled
CI / DB migrations (push) Has been cancelled
feat(devices): implement mobile device token management and push notifications
- Added device token management API for mobile devices, including registration, unregistration, and listing of devices.
- Implemented push notification functionality using FCM for Android and APNS for iOS.
- Introduced new endpoints for device registration and management in the devices API.
- Enhanced the configuration to support mobile push notifications with optional credentials for FCM and APNS.
- Updated database schema to include a new table for storing device tokens.
- Added integration tests for device management and push notification features.
2026-06-17 00:11:25 +02:00

111 lines
3.4 KiB
Go

package contacts
import (
"encoding/json"
"sort"
"strings"
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
"github.com/ultisuite/ulti-backend/internal/nextcloud"
)
const (
maxImportBody = 8 << 20 // 8 MiB
maxContactsPerImport = 5000
)
// importRequest is the bulk import body. Each element of contacts is either a
// raw vCard string ("BEGIN:VCARD...") or a structured contact object matching
// the single-create shape (full_name/email/phone/org or raw_vcard).
type importRequest struct {
Contacts []json.RawMessage `json:"contacts"`
}
// parsedImport holds the valid contacts to send and a mapping back to the
// caller's original array index (so failures can be reported precisely).
type parsedImport struct {
contacts []nextcloud.Contact
originalIndex []int
}
func parseImportContacts(items []json.RawMessage) (parsedImport, []ImportFailure, *apivalidate.ValidationError) {
if len(items) == 0 {
return parsedImport{}, nil, apivalidate.NewValidationError(apivalidate.FieldDetail{
Field: "contacts", Message: "at least one contact is required",
})
}
if len(items) > maxContactsPerImport {
return parsedImport{}, nil, apivalidate.NewValidationError(apivalidate.FieldDetail{
Field: "contacts", Message: "too many contacts in a single import",
})
}
out := parsedImport{
contacts: make([]nextcloud.Contact, 0, len(items)),
originalIndex: make([]int, 0, len(items)),
}
preFailures := make([]ImportFailure, 0)
for i, raw := range items {
contact, err := decodeImportItem(raw)
if err != "" {
preFailures = append(preFailures, ImportFailure{Index: i, Error: err})
continue
}
out.contacts = append(out.contacts, contact)
out.originalIndex = append(out.originalIndex, i)
}
return out, preFailures, nil
}
// decodeImportItem turns a single array element into a Contact, returning a
// non-empty error string when the element is unusable.
func decodeImportItem(raw json.RawMessage) (nextcloud.Contact, string) {
trimmed := strings.TrimSpace(string(raw))
if trimmed == "" || trimmed == "null" {
return nextcloud.Contact{}, "empty contact"
}
// A JSON string element is treated as a raw vCard.
if trimmed[0] == '"' {
var vcard string
if err := json.Unmarshal(raw, &vcard); err != nil {
return nextcloud.Contact{}, "invalid vcard string"
}
vcard = strings.TrimSpace(vcard)
if vcard == "" {
return nextcloud.Contact{}, "empty vcard"
}
return nextcloud.Contact{RawVCard: vcard}, ""
}
var contact nextcloud.Contact
if err := json.Unmarshal(raw, &contact); err != nil {
return nextcloud.Contact{}, "invalid contact object"
}
if strings.TrimSpace(contact.RawVCard) == "" && strings.TrimSpace(contact.FullName) == "" {
return nextcloud.Contact{}, "full_name or raw_vcard required"
}
return contact, ""
}
// mergeImportFailures combines pre-send validation failures with service send
// failures, remapping service indices (relative to the valid slice) back to the
// caller's original array indices, sorted ascending.
func mergeImportFailures(pre []ImportFailure, svc []ImportFailure, originalIndex []int) []ImportFailure {
merged := make([]ImportFailure, 0, len(pre)+len(svc))
merged = append(merged, pre...)
for _, f := range svc {
idx := f.Index
if idx >= 0 && idx < len(originalIndex) {
idx = originalIndex[idx]
}
merged = append(merged, ImportFailure{Index: idx, Error: f.Error})
}
sort.SliceStable(merged, func(i, j int) bool {
return merged[i].Index < merged[j].Index
})
return merged
}