A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

271 rivejä
6.7 KiB

  1. package alpscaldav
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "path"
  7. "strings"
  8. "time"
  9. "git.sr.ht/~emersion/alps"
  10. "github.com/emersion/go-ical"
  11. "github.com/emersion/go-webdav/caldav"
  12. "github.com/google/uuid"
  13. "github.com/labstack/echo/v4"
  14. )
  15. type CalendarRenderData struct {
  16. alps.BaseRenderData
  17. Time time.Time
  18. Calendar *caldav.Calendar
  19. Events []CalendarObject
  20. PrevPage, NextPage string
  21. }
  22. type EventRenderData struct {
  23. alps.BaseRenderData
  24. Calendar *caldav.Calendar
  25. Event CalendarObject
  26. }
  27. type UpdateEventRenderData struct {
  28. alps.BaseRenderData
  29. CalendarObject *caldav.CalendarObject // nil if creating a new contact
  30. Event *ical.Event
  31. }
  32. var monthPageLayout = "2006-01"
  33. func parseObjectPath(s string) (string, error) {
  34. p, err := url.PathUnescape(s)
  35. if err != nil {
  36. err = fmt.Errorf("failed to parse path: %v", err)
  37. return "", echo.NewHTTPError(http.StatusBadRequest, err)
  38. }
  39. return string(p), nil
  40. }
  41. func registerRoutes(p *alps.GoPlugin, u *url.URL) {
  42. p.GET("/calendar", func(ctx *alps.Context) error {
  43. var start time.Time
  44. if s := ctx.QueryParam("month"); s != "" {
  45. var err error
  46. start, err = time.Parse(monthPageLayout, s)
  47. if err != nil {
  48. return fmt.Errorf("failed to parse month: %v", err)
  49. }
  50. } else {
  51. now := time.Now()
  52. start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
  53. }
  54. end := start.AddDate(0, 1, 0)
  55. // TODO: multi-calendar support
  56. c, calendar, err := getCalendar(u, ctx.Session)
  57. if err != nil {
  58. return err
  59. }
  60. query := caldav.CalendarQuery{
  61. CompRequest: caldav.CalendarCompRequest{
  62. Name: "VCALENDAR",
  63. Props: []string{"VERSION"},
  64. Comps: []caldav.CalendarCompRequest{{
  65. Name: "VEVENT",
  66. Props: []string{
  67. "SUMMARY",
  68. "UID",
  69. "DTSTART",
  70. "DTEND",
  71. "DURATION",
  72. },
  73. }},
  74. },
  75. CompFilter: caldav.CompFilter{
  76. Name: "VCALENDAR",
  77. Comps: []caldav.CompFilter{{
  78. Name: "VEVENT",
  79. Start: start,
  80. End: end,
  81. }},
  82. },
  83. }
  84. events, err := c.QueryCalendar(calendar.Path, &query)
  85. if err != nil {
  86. return fmt.Errorf("failed to query calendar: %v", err)
  87. }
  88. return ctx.Render(http.StatusOK, "calendar.html", &CalendarRenderData{
  89. BaseRenderData: *alps.NewBaseRenderData(ctx),
  90. Time: start,
  91. Calendar: calendar,
  92. Events: newCalendarObjectList(events),
  93. PrevPage: start.AddDate(0, -1, 0).Format(monthPageLayout),
  94. NextPage: start.AddDate(0, 1, 0).Format(monthPageLayout),
  95. })
  96. })
  97. p.GET("/calendar/:path", func(ctx *alps.Context) error {
  98. path, err := parseObjectPath(ctx.Param("path"))
  99. if err != nil {
  100. return err
  101. }
  102. c, calendar, err := getCalendar(u, ctx.Session)
  103. if err != nil {
  104. return err
  105. }
  106. multiGet := caldav.CalendarMultiGet{
  107. CompRequest: caldav.CalendarCompRequest{
  108. Name: "VCALENDAR",
  109. Props: []string{"VERSION"},
  110. Comps: []caldav.CalendarCompRequest{{
  111. Name: "VEVENT",
  112. Props: []string{
  113. "SUMMARY",
  114. "DESCRIPTION",
  115. "UID",
  116. "DTSTART",
  117. "DTEND",
  118. "DURATION",
  119. },
  120. }},
  121. },
  122. }
  123. events, err := c.MultiGetCalendar(path, &multiGet)
  124. if err != nil {
  125. return fmt.Errorf("failed to multi-get calendar: %v", err)
  126. }
  127. if len(events) != 1 {
  128. return fmt.Errorf("expected exactly one calendar object with path %q, got %v", path, len(events))
  129. }
  130. event := &events[0]
  131. return ctx.Render(http.StatusOK, "event.html", &EventRenderData{
  132. BaseRenderData: *alps.NewBaseRenderData(ctx),
  133. Calendar: calendar,
  134. Event: CalendarObject{event},
  135. })
  136. })
  137. updateEvent := func(ctx *alps.Context) error {
  138. calendarObjectPath, err := parseObjectPath(ctx.Param("path"))
  139. if err != nil {
  140. return err
  141. }
  142. c, calendar, err := getCalendar(u, ctx.Session)
  143. if err != nil {
  144. return err
  145. }
  146. var co *caldav.CalendarObject
  147. var event *ical.Event
  148. if calendarObjectPath != "" {
  149. co, err := c.GetCalendarObject(calendarObjectPath)
  150. if err != nil {
  151. return fmt.Errorf("failed to get CalDAV event: %v", err)
  152. }
  153. events := co.Data.Events()
  154. if len(events) != 1 {
  155. return fmt.Errorf("expected exactly one event, got %d", len(events))
  156. }
  157. event = &events[0]
  158. } else {
  159. event = ical.NewEvent()
  160. }
  161. if ctx.Request().Method == "POST" {
  162. summary := ctx.FormValue("summary")
  163. description := ctx.FormValue("description")
  164. start, err := time.Parse("2006-01-02", ctx.FormValue("start"))
  165. if err != nil {
  166. err = fmt.Errorf("malformed start date: %v", err)
  167. return echo.NewHTTPError(http.StatusBadRequest, err)
  168. }
  169. end, err := time.Parse("2006-01-02", ctx.FormValue("end"))
  170. if err != nil {
  171. err = fmt.Errorf("malformed end date: %v", err)
  172. return echo.NewHTTPError(http.StatusBadRequest, err)
  173. }
  174. if start.After(end) {
  175. return echo.NewHTTPError(http.StatusBadRequest, "event start is after its end")
  176. }
  177. if start == end {
  178. end = start.Add(24 * time.Hour)
  179. }
  180. event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now())
  181. event.Props.SetText(ical.PropSummary, summary)
  182. event.Props.SetDateTime(ical.PropDateTimeStart, start)
  183. event.Props.SetDateTime(ical.PropDateTimeEnd, end)
  184. event.Props.Del(ical.PropDuration)
  185. if description != "" {
  186. description = strings.ReplaceAll(description, "\r", "")
  187. event.Props.SetText(ical.PropDescription, description)
  188. } else {
  189. event.Props.Del(ical.PropDescription)
  190. }
  191. newID := uuid.New()
  192. if prop := event.Props.Get(ical.PropUID); prop == nil {
  193. event.Props.SetText(ical.PropUID, newID.String())
  194. }
  195. cal := ical.NewCalendar()
  196. cal.Props.SetText(ical.PropProductID, "-//emersion.fr//alps//EN")
  197. cal.Props.SetText(ical.PropVersion, "2.0")
  198. cal.Children = append(cal.Children, event.Component)
  199. var p string
  200. if co != nil {
  201. p = co.Path
  202. } else {
  203. p = path.Join(calendar.Path, newID.String()+".ics")
  204. }
  205. co, err = c.PutCalendarObject(p, cal)
  206. if err != nil {
  207. return fmt.Errorf("failed to put calendar object: %v", err)
  208. }
  209. return ctx.Redirect(http.StatusFound, CalendarObject{co}.URL())
  210. }
  211. return ctx.Render(http.StatusOK, "update-event.html", &UpdateEventRenderData{
  212. BaseRenderData: *alps.NewBaseRenderData(ctx),
  213. CalendarObject: co,
  214. Event: event,
  215. })
  216. }
  217. p.GET("/calendar/create", updateEvent)
  218. p.POST("/calendar/create", updateEvent)
  219. p.GET("/calendar/:path/update", updateEvent)
  220. p.POST("/calendar/:path/update", updateEvent)
  221. p.POST("/calendar/:path/delete", func(ctx *alps.Context) error {
  222. path, err := parseObjectPath(ctx.Param("path"))
  223. if err != nil {
  224. return err
  225. }
  226. c, _, err := getCalendar(u, ctx.Session)
  227. if err != nil {
  228. return err
  229. }
  230. if err := c.RemoveAll(path); err != nil {
  231. return fmt.Errorf("failed to delete calendar object: %v", err)
  232. }
  233. return ctx.Redirect(http.StatusFound, "/calendar")
  234. })
  235. }