A webmail client. Forked from https://git.sr.ht/~migadu/alps
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

331 rinda
8.3 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. Now time.Time
  19. Dates [7 * 6]time.Time
  20. Calendar *caldav.Calendar
  21. Events []CalendarObject
  22. PrevPage, NextPage string
  23. PrevTime, NextTime time.Time
  24. EventsForDate func(time.Time) []CalendarObject
  25. DaySuffix func(n int) string
  26. Sub func(a, b int) int
  27. }
  28. type EventRenderData struct {
  29. alps.BaseRenderData
  30. Calendar *caldav.Calendar
  31. Event CalendarObject
  32. }
  33. type UpdateEventRenderData struct {
  34. alps.BaseRenderData
  35. CalendarObject *caldav.CalendarObject // nil if creating a new contact
  36. Event *ical.Event
  37. }
  38. var monthPageLayout = "2006-01"
  39. func parseObjectPath(s string) (string, error) {
  40. p, err := url.PathUnescape(s)
  41. if err != nil {
  42. err = fmt.Errorf("failed to parse path: %v", err)
  43. return "", echo.NewHTTPError(http.StatusBadRequest, err)
  44. }
  45. return string(p), nil
  46. }
  47. func registerRoutes(p *alps.GoPlugin, u *url.URL) {
  48. p.GET("/calendar", func(ctx *alps.Context) error {
  49. var start time.Time
  50. if s := ctx.QueryParam("month"); s != "" {
  51. var err error
  52. start, err = time.Parse(monthPageLayout, s)
  53. if err != nil {
  54. return fmt.Errorf("failed to parse month: %v", err)
  55. }
  56. } else {
  57. now := time.Now()
  58. start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
  59. }
  60. end := start.AddDate(0, 1, 0)
  61. // TODO: multi-calendar support
  62. c, calendar, err := getCalendar(u, ctx.Session)
  63. if err != nil {
  64. return err
  65. }
  66. query := caldav.CalendarQuery{
  67. CompRequest: caldav.CalendarCompRequest{
  68. Name: "VCALENDAR",
  69. Props: []string{"VERSION"},
  70. Comps: []caldav.CalendarCompRequest{{
  71. Name: "VEVENT",
  72. Props: []string{
  73. "SUMMARY",
  74. "UID",
  75. "DTSTART",
  76. "DTEND",
  77. "DURATION",
  78. },
  79. }},
  80. },
  81. CompFilter: caldav.CompFilter{
  82. Name: "VCALENDAR",
  83. Comps: []caldav.CompFilter{{
  84. Name: "VEVENT",
  85. Start: start,
  86. End: end,
  87. }},
  88. },
  89. }
  90. events, err := c.QueryCalendar(calendar.Path, &query)
  91. if err != nil {
  92. return fmt.Errorf("failed to query calendar: %v", err)
  93. }
  94. // TODO: Time zones are hard
  95. var dates [7 * 6]time.Time
  96. initialDate := start.UTC()
  97. initialDate = initialDate.AddDate(0, 0, -int(initialDate.Weekday()))
  98. for i := 0; i < len(dates); i += 1 {
  99. dates[i] = initialDate
  100. initialDate = initialDate.AddDate(0, 0, 1)
  101. }
  102. eventMap := make(map[time.Time][]CalendarObject)
  103. for _, ev := range events {
  104. ev := ev // make a copy
  105. // TODO: include event on each date for which it is active
  106. co := ev.Data.Events()[0]
  107. startTime, _ := co.DateTimeStart(nil)
  108. startTime = startTime.UTC().Truncate(time.Hour * 24)
  109. eventMap[startTime] = append(eventMap[startTime], CalendarObject{&ev})
  110. }
  111. return ctx.Render(http.StatusOK, "calendar.html", &CalendarRenderData{
  112. BaseRenderData: *alps.NewBaseRenderData(ctx),
  113. Time: start,
  114. Now: time.Now(), // TODO: Use client time zone
  115. Calendar: calendar,
  116. Dates: dates,
  117. Events: newCalendarObjectList(events),
  118. PrevPage: start.AddDate(0, -1, 0).Format(monthPageLayout),
  119. NextPage: start.AddDate(0, 1, 0).Format(monthPageLayout),
  120. PrevTime: start.AddDate(0, -1, 0),
  121. NextTime: start.AddDate(0, 1, 0),
  122. EventsForDate: func(when time.Time) []CalendarObject {
  123. if events, ok := eventMap[when.Truncate(time.Hour * 24)]; ok {
  124. return events
  125. }
  126. return nil
  127. },
  128. DaySuffix: func(n int) string {
  129. if n % 100 >= 11 && n % 100 <= 13 {
  130. return "th"
  131. }
  132. return map[int]string{
  133. 0: "th",
  134. 1: "st",
  135. 2: "nd",
  136. 3: "rd",
  137. 4: "th",
  138. 5: "th",
  139. 6: "th",
  140. 7: "th",
  141. 8: "th",
  142. 9: "th",
  143. }[n % 10]
  144. },
  145. Sub: func (a, b int) int {
  146. // Why isn't this built-in, come on Go
  147. return a - b
  148. },
  149. })
  150. })
  151. p.GET("/calendar/:path", func(ctx *alps.Context) error {
  152. path, err := parseObjectPath(ctx.Param("path"))
  153. if err != nil {
  154. return err
  155. }
  156. c, calendar, err := getCalendar(u, ctx.Session)
  157. if err != nil {
  158. return err
  159. }
  160. multiGet := caldav.CalendarMultiGet{
  161. CompRequest: caldav.CalendarCompRequest{
  162. Name: "VCALENDAR",
  163. Props: []string{"VERSION"},
  164. Comps: []caldav.CalendarCompRequest{{
  165. Name: "VEVENT",
  166. Props: []string{
  167. "SUMMARY",
  168. "DESCRIPTION",
  169. "UID",
  170. "DTSTART",
  171. "DTEND",
  172. "DURATION",
  173. },
  174. }},
  175. },
  176. }
  177. events, err := c.MultiGetCalendar(path, &multiGet)
  178. if err != nil {
  179. return fmt.Errorf("failed to multi-get calendar: %v", err)
  180. }
  181. if len(events) != 1 {
  182. return fmt.Errorf("expected exactly one calendar object with path %q, got %v", path, len(events))
  183. }
  184. event := &events[0]
  185. return ctx.Render(http.StatusOK, "event.html", &EventRenderData{
  186. BaseRenderData: *alps.NewBaseRenderData(ctx),
  187. Calendar: calendar,
  188. Event: CalendarObject{event},
  189. })
  190. })
  191. updateEvent := func(ctx *alps.Context) error {
  192. calendarObjectPath, err := parseObjectPath(ctx.Param("path"))
  193. if err != nil {
  194. return err
  195. }
  196. c, calendar, err := getCalendar(u, ctx.Session)
  197. if err != nil {
  198. return err
  199. }
  200. var co *caldav.CalendarObject
  201. var event *ical.Event
  202. if calendarObjectPath != "" {
  203. co, err = c.GetCalendarObject(calendarObjectPath)
  204. if err != nil {
  205. return fmt.Errorf("failed to get CalDAV event: %v", err)
  206. }
  207. events := co.Data.Events()
  208. if len(events) != 1 {
  209. return fmt.Errorf("expected exactly one event, got %d", len(events))
  210. }
  211. event = &events[0]
  212. } else {
  213. event = ical.NewEvent()
  214. }
  215. if ctx.Request().Method == "POST" {
  216. summary := ctx.FormValue("summary")
  217. description := ctx.FormValue("description")
  218. start, err := time.Parse("2006-01-02", ctx.FormValue("start"))
  219. if err != nil {
  220. err = fmt.Errorf("malformed start date: %v", err)
  221. return echo.NewHTTPError(http.StatusBadRequest, err)
  222. }
  223. end, err := time.Parse("2006-01-02", ctx.FormValue("end"))
  224. if err != nil {
  225. err = fmt.Errorf("malformed end date: %v", err)
  226. return echo.NewHTTPError(http.StatusBadRequest, err)
  227. }
  228. if start.After(end) {
  229. return echo.NewHTTPError(http.StatusBadRequest, "event start is after its end")
  230. }
  231. if start == end {
  232. end = start.Add(24 * time.Hour)
  233. }
  234. event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now())
  235. event.Props.SetText(ical.PropSummary, summary)
  236. event.Props.SetDateTime(ical.PropDateTimeStart, start)
  237. event.Props.SetDateTime(ical.PropDateTimeEnd, end)
  238. event.Props.Del(ical.PropDuration)
  239. if description != "" {
  240. description = strings.ReplaceAll(description, "\r", "")
  241. event.Props.SetText(ical.PropDescription, description)
  242. } else {
  243. event.Props.Del(ical.PropDescription)
  244. }
  245. newID := uuid.New()
  246. if prop := event.Props.Get(ical.PropUID); prop == nil {
  247. event.Props.SetText(ical.PropUID, newID.String())
  248. }
  249. cal := ical.NewCalendar()
  250. cal.Props.SetText(ical.PropProductID, "-//emersion.fr//alps//EN")
  251. cal.Props.SetText(ical.PropVersion, "2.0")
  252. cal.Children = append(cal.Children, event.Component)
  253. var p string
  254. if co != nil {
  255. p = co.Path
  256. } else {
  257. p = path.Join(calendar.Path, newID.String()+".ics")
  258. }
  259. co, err = c.PutCalendarObject(p, cal)
  260. if err != nil {
  261. return fmt.Errorf("failed to put calendar object: %v", err)
  262. }
  263. return ctx.Redirect(http.StatusFound, CalendarObject{co}.URL())
  264. }
  265. return ctx.Render(http.StatusOK, "update-event.html", &UpdateEventRenderData{
  266. BaseRenderData: *alps.NewBaseRenderData(ctx),
  267. CalendarObject: co,
  268. Event: event,
  269. })
  270. }
  271. p.GET("/calendar/create", updateEvent)
  272. p.POST("/calendar/create", updateEvent)
  273. p.GET("/calendar/:path/update", updateEvent)
  274. p.POST("/calendar/:path/update", updateEvent)
  275. p.POST("/calendar/:path/delete", func(ctx *alps.Context) error {
  276. path, err := parseObjectPath(ctx.Param("path"))
  277. if err != nil {
  278. return err
  279. }
  280. c, _, err := getCalendar(u, ctx.Session)
  281. if err != nil {
  282. return err
  283. }
  284. if err := c.RemoveAll(path); err != nil {
  285. return fmt.Errorf("failed to delete calendar object: %v", err)
  286. }
  287. return ctx.Redirect(http.StatusFound, "/calendar")
  288. })
  289. }