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