A webmail client. Forked from https://git.sr.ht/~migadu/alps
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

261 行
6.5 KiB

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