- 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.
111 lines
3.4 KiB
Go
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
|
|
}
|