notion2ical/internal/server/http/routes/calendar.go

184 lines
5.5 KiB
Go

package routes
import (
"bytes"
"net/url"
"strings"
"github.com/emersion/go-ical"
"github.com/fmartingr/notion2ical/internal/config"
notionClient "github.com/fmartingr/notion2ical/internal/notion"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/utils"
"go.uber.org/zap"
)
type CalendarRoutes struct {
logger *zap.Logger
router *fiber.App
notion *notionClient.NotionClient
publicHostname string
limiterHandler fiber.Handler
cacheHandler fiber.Handler
}
func (r *CalendarRoutes) Setup() *CalendarRoutes {
r.router.
Use(r.limiterHandler).
Post("/wizard", r.wizardHandler).
Post("/download", r.downloadHandler).
Get("/", r.indexHandler).
Use(r.cacheHandler).
Get("/calendar.ics", r.calendarIcsHandler)
return r
}
func (r *CalendarRoutes) Router() *fiber.App {
return r.router
}
func (r *CalendarRoutes) indexHandler(c *fiber.Ctx) error {
return c.Render("index", fiber.Map{
"error": c.Query("error"),
})
}
func (r *CalendarRoutes) wizardHandler(c *fiber.Ctx) error {
var payload wizardPayload
if err := c.BodyParser(&payload); err != nil {
r.logger.Error("error parsing query", zap.String("query", c.Context().QueryArgs().String()))
return err
}
if err := payload.Validate(); err != nil {
return c.Redirect("/?error="+err.Error()+"#how-it-works", fiber.StatusTemporaryRedirect)
}
info, err := r.notion.GetDatabaseInfo(c.Context(), payload.GetDatabaseID())
if err != nil {
return c.Redirect("/?error=Error getting database information, have you set up the integration properly?#how-it-works", fiber.StatusTemporaryRedirect)
}
if len(info.DateProperties) == 0 {
return c.Redirect("/?error=Your database does not have any datetime properties, at least one is required#how-it-works", fiber.StatusTemporaryRedirect)
}
if len(info.TextProperties) == 0 {
return c.Redirect("/?error=Your database does not have any text properties, at least one is required#how-it-works", fiber.StatusTemporaryRedirect)
}
return c.Render("wizard", fiber.Map{
"textProperties": info.TextProperties,
"datetimeProperties": info.DateProperties,
"databaseName": info.Name,
"databaseID": info.ID,
})
}
func (r *CalendarRoutes) downloadHandler(c *fiber.Ctx) error {
var payload calendarDownloadPayload
if err := c.BodyParser(&payload); err != nil {
r.logger.Error("error parsing query", zap.String("query", c.Context().QueryArgs().String()))
return err
}
if err := payload.Validate(); err != nil {
return c.Redirect("/wizard?error="+err.Error(), fiber.StatusTemporaryRedirect)
}
return c.Render("download", fiber.Map{
"calendarSubscriptionUrl": r.publicHostname + "/calendar.ics?" + string(c.Request().Body()),
"calendarICSUrl": r.publicHostname + "/calendar.ics?" + string(c.Request().Body()),
})
}
func (r *CalendarRoutes) calendarIcsHandler(c *fiber.Ctx) error {
var payload calendarDownloadPayload
if err := c.QueryParser(&payload); err != nil {
r.logger.Error("error parsing query", zap.String("query", c.Context().QueryArgs().String()))
return err
}
if err := payload.Validate(); err != nil {
return c.Redirect("/?error="+err.Error(), fiber.StatusTemporaryRedirect)
}
results, err := r.notion.GetDatabaseItems(c.Context(), payload.DatabaseID, payload.NameProperty, payload.DateProperty)
if err != nil {
return err
}
cal := ical.NewCalendar()
cal.Props.SetText(ical.PropVersion, "2.0")
cal.Props.SetText(ical.PropProductID, "-//notion2ical//NONSGML PDA Calendar Version 1.0//EN")
uri, err := url.Parse(r.publicHostname + c.OriginalURL())
if err != nil {
r.logger.Error("error formatting calendar url", zap.Error(err))
return err
}
cal.Props.SetURI(ical.PropURL, uri)
for _, item := range results {
event := ical.NewEvent()
event.Props.SetText(ical.PropUID, item.ID)
if payload.AllDayEvents {
event.Props.SetDate(ical.PropDateTimeStamp, item.DateStart)
event.Props.SetDate(ical.PropDateTimeStart, item.DateEnd)
} else {
event.Props.SetDateTime(ical.PropDateTimeStamp, item.DateStart)
event.Props.SetDateTime(ical.PropDateTimeStart, item.DateEnd)
}
event.Props.SetText(ical.PropSummary, item.Name)
cal.Children = append(cal.Children, event.Component)
}
var buf bytes.Buffer
if err := ical.NewEncoder(&buf).Encode(cal); err != nil {
r.logger.Error("error encoding calendar", zap.Error(err))
return err
}
c.Set("Content-Type", "text/calendar")
return c.Send(buf.Bytes())
}
func NewCalendarRoutes(logger *zap.Logger, cfg *config.Config) *CalendarRoutes {
routes := CalendarRoutes{
logger: logger,
notion: cfg.Notion.Client,
router: fiber.New(),
publicHostname: cfg.Http.PublicHostname,
limiterHandler: limiter.New(limiter.Config{
Max: cfg.Routes.Calendar.LimiterMaxRequest,
Expiration: cfg.Routes.Calendar.LimiterExpiration,
}),
cacheHandler: cache.New(cache.Config{
Expiration: cfg.Routes.Calendar.CacheExpiration,
CacheControl: cfg.Routes.Calendar.CacheControl,
KeyGenerator: func(c *fiber.Ctx) string {
args := c.Context().QueryArgs()
params := []string{
"database_id",
string(args.Peek("database_id")),
"all_day_events",
string(args.Peek("all_day_events")),
"date_property",
string(args.Peek("date_property")),
"name_propety",
string(args.Peek("name_propety")),
}
return utils.CopyString(c.Path() + strings.Join(params, "-"))
},
}),
}
routes.Setup()
return &routes
}