A webmail client. Forked from https://git.sr.ht/~migadu/alps
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

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