package dav import ( "context" "crussell/clock" "fmt" "log/slog" "strings" "time" "github.com/jackc/pgx/v5/pgxpool" ) // BaseService holds shared DB connection type BaseService struct { db *pgxpool.Pool } // newBaseService returns a new BaseService instance func newBaseService(db *pgxpool.Pool) *BaseService { return &BaseService{db: db} } // ============================================================================ // Calendar Functions // ============================================================================ func (s *BaseService) ListEventsForMonth(year int, month time.Month) ([]CalendarEvent, error) { start := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC) end := start.AddDate(0, 1, 0).Add(-time.Second) query := ` SELECT id, calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid FROM dav_calendarobjects WHERE firstoccurence >= $1 AND firstoccurence <= $2 ORDER BY firstoccurence ` return s.queryEventsWithContacts(query, start.Unix(), end.Unix()) } func (s *BaseService) ListEventsBetween(start, end time.Time) ([]CalendarEvent, error) { query := ` SELECT id, calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid FROM dav_calendarobjects WHERE firstoccurence >= $1 AND firstoccurence <= $2 ORDER BY firstoccurence ` return s.queryEventsWithContacts(query, start.Unix(), end.Unix()) } func (s *BaseService) ListEventsTomorrow() ([]CalendarEvent, error) { now := clock.Now() tomorrow := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) dayAfter := tomorrow.Add(24 * time.Hour) return s.ListEventsBetween(tomorrow, dayAfter) } func (s *BaseService) ListEventsThisWeek() ([]CalendarEvent, error) { now := clock.Now() weekday := int(now.Weekday()) if weekday == 0 { // Sunday weekday = 7 } monday := now.AddDate(0, 0, -weekday+1) monday = time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, time.UTC) sunday := monday.AddDate(0, 0, 7) return s.ListEventsBetween(monday, sunday) } // ============================================================================ // Contact Functions // ============================================================================ func (s *BaseService) GetContactByURI(addressBookID int, uri string) (*Contact, error) { query := ` SELECT id, addressbookid, uri, carddata, lastmodified, etag, size FROM dav_cards WHERE addressbookid = $1 AND uri = $2 ` var c Contact err := s.db.QueryRow(context.Background(), query, addressBookID, uri).Scan( &c.ID, &c.AddressBookID, &c.URI, &c.CardData, &c.LastModified, &c.Etag, &c.Size, ) if err != nil { return nil, fmt.Errorf("contact not found: %w", err) } return &c, nil } func (s *BaseService) ListAllContacts() ([]Contact, error) { query := `SELECT id, addressbookid, uri, carddata, lastmodified, etag, size FROM dav_cards ORDER BY lastmodified DESC` rows, err := s.db.Query(context.Background(), query) if err != nil { return nil, err } defer rows.Close() var contacts []Contact for rows.Next() { var c Contact if err := rows.Scan(&c.ID, &c.AddressBookID, &c.URI, &c.CardData, &c.LastModified, &c.Etag, &c.Size); err != nil { return nil, err } contacts = append(contacts, c) } return contacts, nil } func (s *BaseService) ListRecentContacts(days int) ([]Contact, error) { cutoff := clock.Now().AddDate(0, 0, -days).Unix() query := `SELECT id, addressbookid, uri, carddata, lastmodified, etag, size FROM dav_cards WHERE lastmodified >= $1 ORDER BY lastmodified DESC` rows, err := s.db.Query(context.Background(), query, cutoff) if err != nil { return nil, err } defer rows.Close() var contacts []Contact for rows.Next() { var c Contact if err := rows.Scan(&c.ID, &c.AddressBookID, &c.URI, &c.CardData, &c.LastModified, &c.Etag, &c.Size); err != nil { return nil, err } contacts = append(contacts, c) } return contacts, nil } // ============================================================================ // Helper Methods // ============================================================================ func (s *BaseService) queryEventsWithContacts(query string, args ...any) ([]CalendarEvent, error) { rows, err := s.db.Query(context.Background(), query, args...) if err != nil { return nil, err } defer rows.Close() var events []CalendarEvent for rows.Next() { var e CalendarEvent if err := rows.Scan( &e.ID, &e.CalendarID, &e.URI, &e.CalendarData, &e.LastModified, &e.Etag, &e.Size, &e.ComponentType, &e.FirstOccurence, &e.LastOccurence, &e.UID, ); err != nil { return nil, err } e.ContactURIs = extractContactURIsFromICalendar(e.CalendarData) events = append(events, e) } return events, nil } func extractContactURIsFromICalendar(icalData string) []string { var uris []string for line := range strings.SplitSeq(icalData, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "ATTENDEE") { parts := strings.Split(line, ":") if len(parts) >= 2 { uris = append(uris, strings.TrimSpace(parts[len(parts)-1])) } } } return uris } // CreateContact adds a new contact to an address book func (s *BaseService) CreateContact(addressBookID int, userID string, input ContactInput) error { if s.db == nil { return nil // No DB configured, skip CardDAV sync } now := clock.Now().Unix() uri := fmt.Sprintf("%s.vcf", userID) cardData := GenerateVCard(input) tx, err := s.db.Begin(context.Background()) if err != nil { return err } defer func() { if err := tx.Rollback(context.Background()); err != nil && err.Error() != "tx is closed" { slog.Error("failed to rollback transaction", "err", err) } }() query := ` INSERT INTO dav_cards (addressbookid, uri, carddata, lastmodified, etag, size) VALUES ($1, $2, $3, $4, $5, $6) ` _, err = tx.Exec(context.Background(), query, addressBookID, uri, cardData, now, fmt.Sprintf("%d", now), len(cardData), ) if err != nil { return err } return tx.Commit(context.Background()) } // UpdateContact updates an existing contact by URI func (s *BaseService) UpdateContact(addressBookID int, uri string, input ContactInput) error { now := clock.Now().Unix() cardData := GenerateVCard(input) tx, err := s.db.Begin(context.Background()) if err != nil { return err } defer func() { if err := tx.Rollback(context.Background()); err != nil && err.Error() != "tx is closed" { slog.Error("failed to rollback transaction", "err", err) } }() query := ` UPDATE dav_cards SET carddata = $1, lastmodified = $2, etag = $3, size = $4 WHERE addressbookid = $5 AND uri = $6 ` _, err = tx.Exec(context.Background(), query, cardData, now, fmt.Sprintf("%d", now), len(cardData), addressBookID, uri) if err != nil { return err } return tx.Commit(context.Background()) } // DeleteContact deletes a contact by URI func (s *BaseService) DeleteContact(addressBookID int, uri string) error { tx, err := s.db.Begin(context.Background()) if err != nil { return err } defer func() { if err := tx.Rollback(context.Background()); err != nil && err.Error() != "tx is closed" { slog.Error("failed to rollback transaction", "err", err) } }() query := `DELETE FROM dav_cards WHERE addressbookid = $1 AND uri = $2` _, err = tx.Exec(context.Background(), query, addressBookID, uri) if err != nil { return err } return tx.Commit(context.Background()) } // CreateEvent adds a new event to a calendar func (s *BaseService) CreateEvent(calendarID int, input EventInput) error { uid := fmt.Sprintf("%d@example.com", clock.Now().UnixNano()) now := clock.Now().Unix() calendarData := GenerateICalEvent(input) tx, err := s.db.Begin(context.Background()) if err != nil { return err } defer func() { if err := tx.Rollback(context.Background()); err != nil && err.Error() != "tx is closed" { slog.Error("failed to rollback transaction", "err", err) } }() query := ` INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9) ` _, err = tx.Exec(context.Background(), query, calendarID, uid+".ics", calendarData, now, fmt.Sprintf("%d", now), len(calendarData), input.Start.Unix(), input.End.Unix(), uid, ) if err != nil { return err } return tx.Commit(context.Background()) } // UpdateEvent updates an existing calendar event by UID func (s *BaseService) UpdateEvent(calendarID int, uid string, input EventInput) error { now := clock.Now().Unix() calendarData := GenerateICalEvent(input) tx, err := s.db.Begin(context.Background()) if err != nil { return err } defer func() { if err := tx.Rollback(context.Background()); err != nil && err.Error() != "tx is closed" { slog.Error("failed to rollback transaction", "err", err) } }() query := ` UPDATE dav_calendarobjects SET calendardata = $1, lastmodified = $2, etag = $3, size = $4, firstoccurence = $5, lastoccurence = $6 WHERE calendarid = $7 AND uid = $8 ` _, err = tx.Exec(context.Background(), query, calendarData, now, fmt.Sprintf("%d", now), len(calendarData), input.Start.Unix(), input.End.Unix(), calendarID, uid, ) if err != nil { return err } return tx.Commit(context.Background()) } // DeleteEvent deletes an event by UID func (s *BaseService) DeleteEvent(calendarID int, uid string) error { tx, err := s.db.Begin(context.Background()) if err != nil { return err } defer func() { if err := tx.Rollback(context.Background()); err != nil && err.Error() != "tx is closed" { slog.Error("failed to rollback transaction", "err", err) } }() query := `DELETE FROM dav_calendarobjects WHERE calendarid = $1 AND uid = $2` _, err = tx.Exec(context.Background(), query, calendarID, uid) if err != nil { return err } return tx.Commit(context.Background()) } // ListEventsForContactSQL returns all calendar events where the given contact URI is an attendee (SQL optimized) func (s *BaseService) ListEventsForContact(contactURI string) ([]CalendarEvent, error) { // Use pattern matching to find events containing the contact URI in ATTENDEE lines likePattern := "%" + contactURI + "%" query := ` SELECT id, calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid FROM dav_calendarobjects WHERE calendardata LIKE $1 ORDER BY firstoccurence ` return s.queryEventsWithContacts(query, likePattern) }