first commit
Made-with: Cursor
This commit is contained in:
189
internal/api/handlers/ics.go
Normal file
189
internal/api/handlers/ics.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/calendarapi/internal/middleware"
|
||||
"github.com/calendarapi/internal/models"
|
||||
"github.com/calendarapi/internal/repository"
|
||||
"github.com/calendarapi/internal/service"
|
||||
"github.com/calendarapi/internal/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ICSHandler struct {
|
||||
calSvc *service.CalendarService
|
||||
eventSvc *service.EventService
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
func NewICSHandler(calSvc *service.CalendarService, eventSvc *service.EventService, queries *repository.Queries) *ICSHandler {
|
||||
return &ICSHandler{calSvc: calSvc, eventSvc: eventSvc, queries: queries}
|
||||
}
|
||||
|
||||
func (h *ICSHandler) Export(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := middleware.GetUserID(r.Context())
|
||||
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.calSvc.GetRole(r.Context(), calID, userID); err != nil {
|
||||
utils.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
rangeStart := now.AddDate(-1, 0, 0)
|
||||
rangeEnd := now.AddDate(1, 0, 0)
|
||||
|
||||
events, err := h.queries.ListEventsByCalendarInRange(r.Context(), repository.ListEventsByCalendarInRangeParams{
|
||||
CalendarID: utils.ToPgUUID(calID),
|
||||
EndTime: utils.ToPgTimestamptz(rangeStart),
|
||||
StartTime: utils.ToPgTimestamptz(rangeEnd),
|
||||
})
|
||||
if err != nil {
|
||||
utils.WriteError(w, models.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//CalendarAPI//EN\r\n")
|
||||
|
||||
for _, ev := range events {
|
||||
b.WriteString("BEGIN:VEVENT\r\n")
|
||||
b.WriteString(fmt.Sprintf("UID:%s\r\n", utils.FromPgUUID(ev.ID).String()))
|
||||
b.WriteString(fmt.Sprintf("DTSTART:%s\r\n", utils.FromPgTimestamptz(ev.StartTime).Format("20060102T150405Z")))
|
||||
b.WriteString(fmt.Sprintf("DTEND:%s\r\n", utils.FromPgTimestamptz(ev.EndTime).Format("20060102T150405Z")))
|
||||
b.WriteString(fmt.Sprintf("SUMMARY:%s\r\n", ev.Title))
|
||||
if ev.Description.Valid {
|
||||
b.WriteString(fmt.Sprintf("DESCRIPTION:%s\r\n", ev.Description.String))
|
||||
}
|
||||
if ev.Location.Valid {
|
||||
b.WriteString(fmt.Sprintf("LOCATION:%s\r\n", ev.Location.String))
|
||||
}
|
||||
if ev.RecurrenceRule.Valid {
|
||||
b.WriteString(fmt.Sprintf("RRULE:%s\r\n", ev.RecurrenceRule.String))
|
||||
}
|
||||
b.WriteString("END:VEVENT\r\n")
|
||||
}
|
||||
|
||||
b.WriteString("END:VCALENDAR\r\n")
|
||||
|
||||
w.Header().Set("Content-Type", "text/calendar")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=calendar.ics")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(b.String()))
|
||||
}
|
||||
|
||||
func (h *ICSHandler) Import(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := middleware.GetUserID(r.Context())
|
||||
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||
utils.WriteError(w, models.NewValidationError("invalid multipart form"))
|
||||
return
|
||||
}
|
||||
|
||||
calIDStr := r.FormValue("calendar_id")
|
||||
calID, err := utils.ValidateUUID(calIDStr)
|
||||
if err != nil {
|
||||
utils.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
role, err := h.calSvc.GetRole(r.Context(), calID, userID)
|
||||
if err != nil {
|
||||
utils.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if role != "owner" && role != "editor" {
|
||||
utils.WriteError(w, models.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
utils.WriteError(w, models.NewValidationError("file required"))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
utils.WriteError(w, models.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
count := h.parseAndImportICS(r.Context(), string(data), calID, userID)
|
||||
|
||||
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"ok": true,
|
||||
"imported": map[string]int{"events": count},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ICSHandler) parseAndImportICS(ctx context.Context, data string, calID, userID uuid.UUID) int {
|
||||
count := 0
|
||||
lines := strings.Split(data, "\n")
|
||||
var inEvent bool
|
||||
var title, description, location, rruleStr string
|
||||
var dtstart, dtend time.Time
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "BEGIN:VEVENT" {
|
||||
inEvent = true
|
||||
title, description, location, rruleStr = "", "", "", ""
|
||||
dtstart, dtend = time.Time{}, time.Time{}
|
||||
continue
|
||||
}
|
||||
if line == "END:VEVENT" && inEvent {
|
||||
inEvent = false
|
||||
if title != "" && !dtstart.IsZero() && !dtend.IsZero() {
|
||||
_, err := h.queries.CreateEvent(ctx, repository.CreateEventParams{
|
||||
ID: utils.ToPgUUID(uuid.New()),
|
||||
CalendarID: utils.ToPgUUID(calID),
|
||||
Title: title,
|
||||
Description: utils.ToPgText(description),
|
||||
Location: utils.ToPgText(location),
|
||||
StartTime: utils.ToPgTimestamptz(dtstart),
|
||||
EndTime: utils.ToPgTimestamptz(dtend),
|
||||
Timezone: "UTC",
|
||||
RecurrenceRule: utils.ToPgText(rruleStr),
|
||||
Tags: []string{},
|
||||
CreatedBy: utils.ToPgUUID(userID),
|
||||
UpdatedBy: utils.ToPgUUID(userID),
|
||||
})
|
||||
if err == nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !inEvent {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(line, "SUMMARY:"):
|
||||
title = strings.TrimPrefix(line, "SUMMARY:")
|
||||
case strings.HasPrefix(line, "DESCRIPTION:"):
|
||||
description = strings.TrimPrefix(line, "DESCRIPTION:")
|
||||
case strings.HasPrefix(line, "LOCATION:"):
|
||||
location = strings.TrimPrefix(line, "LOCATION:")
|
||||
case strings.HasPrefix(line, "RRULE:"):
|
||||
rruleStr = strings.TrimPrefix(line, "RRULE:")
|
||||
case strings.HasPrefix(line, "DTSTART:"):
|
||||
dtstart, _ = time.Parse("20060102T150405Z", strings.TrimPrefix(line, "DTSTART:"))
|
||||
case strings.HasPrefix(line, "DTEND:"):
|
||||
dtend, _ = time.Parse("20060102T150405Z", strings.TrimPrefix(line, "DTEND:"))
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
Reference in New Issue
Block a user