Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iCalendar endpoints #53

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 78 additions & 35 deletions src/co/gaiwan/compass/routes/sessions.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@
organized by participants.
"
(:require
[clojure.string :as str]
[co.gaiwan.compass.config :as config]
[co.gaiwan.compass.db :as db]
[co.gaiwan.compass.db.queries :as q]
[co.gaiwan.compass.html.sessions :as session-html]
[co.gaiwan.compass.http.response :as response]
[co.gaiwan.compass.model.assets :as assets]
[co.gaiwan.compass.model.session :as session]
[co.gaiwan.compass.model.user :as user]
[java-time.api :as time]
[co.gaiwan.compass.services.discord :as discord]
[co.gaiwan.compass.util :as util]
[clojure.string :as str]))
[co.gaiwan.compass.util :as util :refer [now]]
[java-time.api :as time]))

(defn GET-session-new [req]
(if-not (:identity req)
Expand Down Expand Up @@ -210,42 +211,81 @@
:sessions (session/apply-filters sessions user filters)}]}))

(defn format-datetime [time]
(time/format "yyyyMMdd'T'HHmmss" time))
(->> (.withZoneSameInstant time java.time.ZoneOffset/UTC)
(time/format "yyyyMMdd'T'HHmmss'Z'")))

(defn generate-icalendar [event]
(let [{:keys [uid title description location start-time end-time]} event]
(str/join "\r\n"
["BEGIN:VCALENDAR"
"VERSION:2.0"
"BEGIN:VEVENT"
(str "UID:" uid)
(str "SUMMARY:" title)
(str "DESCRIPTION:" description)
(str "LOCATION:" location)
(str "DTSTART:" start-time)
(str "DTEND:" end-time)
"END:VEVENT"
"END:VCALENDAR"])))
(defn fold-content [content]
(->> (partition-all 73 content) ; 73 chars + CRLF = 75
(map #(apply str %))
(str/join "\r\n ")))

(defn create-icalendar-response [event]
(defn escape-text [text]
(-> text
(str/replace #"\r?\n" "\\\\n") ; Replace newlines with \n
(str/replace #"," "\\,") ; Escape commas
(str/replace #";" "\\;") ; Escape semicolons
(str/replace #"\\" "\\\\"))) ; Escape backslashes

(defn format-property [key value]
(let [content-line (str key ":" value)]
(fold-content content-line)))

(defn join-clrf [strs]
(str/join "\r\n" strs))

(defn session->ical-object
([session] (session->ical-object session (now)))
([now session]
(let [{:session/keys [title description
location time duration]
:as session} session]
(join-clrf
["BEGIN:VEVENT"
(format-property "UID" (str (:db/id session) "@heart_of_clojure_2024"))
(format-property "DTSTAMP" (format-datetime now))
(format-property "SUMMARY" (escape-text title))
(format-property "DESCRIPTION" (escape-text (or description "")))
(format-property "LOCATION" (escape-text (:location/name location)))
(format-property "DTSTART" (format-datetime time))
(format-property "DTEND" (format-datetime (time/+ time (time/duration duration))))
"END:VEVENT"]))))

(defn create-icalendar-response [icalendar]
{:headers {"content-type" "text/calendar"
"content-disposition" (str "attachment; filename=\"" (:title event) ".ics\"")}
:body (generate-icalendar event)})
"content-disposition" (str "attachment; filename=\"" (:title icalendar) ".ics\"")}
:body icalendar})

(defn GET-add-to-calendar-handler [req]
(let [session-eid (parse-long (get-in req [:path-params :id]))
{:session/keys [title description
location time duration]
:as session} (q/session session-eid)
event {:uid (str (:db/id session) "@heart_of_clojure_2024")
:title title
:description (when description (subs description 0 (min (count description) 50)))
:location (:location/name location)
:start-time (format-datetime time)
:end-time (-> time
(time/+ (time/duration duration))
format-datetime)}]
(create-icalendar-response event)))
(defn sessions->ical-objects [sessions now]
(->> sessions
(mapv (partial session->ical-object now))
join-clrf))

(defn sessions->icalendar [sessions now]
(let [prodid (str "-//Heart of Clojure_"
(config/value :compass/origin)
"//Compass//EN")]
(join-clrf
["BEGIN:VCALENDAR"
"VERSION:2.0"
(format-property "PRODID" prodid)
(sessions->ical-objects sessions now)
"END:VCALENDAR"])))

(defn GET-add-to-calendar-handler
"Get iCalendar response for a single session."
[req]
(let [session (q/session (parse-long (get-in req [:path-params :id])))]
(-> [session]
(sessions->icalendar (now))
create-icalendar-response)))

(defn GET-calendar-feed
"Get iCalendar feed for all sessions."
[_]
(let [sessions (q/all-sessions)]
(-> sessions
(sessions->icalendar (now))
create-icalendar-response)))

(defn routes []
[[""
Expand All @@ -261,6 +301,9 @@
{:name :session/new
:get {:middleware [[response/wrap-requires-auth]]
:handler GET-session-new}}]
["/calendar-feed"
{:name :session/calendar-feed
:get {:handler GET-calendar-feed}}]
["/:id"
{:name :session/get
:get {:handler GET-session}
Expand Down
2 changes: 2 additions & 0 deletions src/co/gaiwan/compass/util.clj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
[expires-in]
(.plusSeconds (Instant/now) (- expires-in 60)))

(defn now [] (ZonedDateTime/now))

(defn partition-with-limit
[limit parts]
(loop [result []
Expand Down
Loading