;;; emacsconf-update.el --- Update an Org file with the current schedule -*- lexical-binding: t; -*- ;; Copyright (C) 2021 Sacha Chua ;; Author: Sacha Chua ;; Keywords: convenience, calendar ;; Version: 0.1 ;; URL: https://github.com/emacsconf/emacsconf-el ;; ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; The following code should retrieve the conference schedule and update ;; a file that people use for notes. It should keep the notes people ;; already have, and just update the scheduled times or create new notes ;; as needed. If the calendar file has UID properties, it is used to ;; match headings. If not, headings are used. ;; Make a file for your notes with Org, and then call ;; =M-x emacsconf-update-org-from-ical-url= . It should load the talks and ;; tentative schedules from the wiki. You can call it again and it should ;; update the titles and schedules while still keeping whatever notes ;; you've added. ;; ;; You may also like =M-x emacsconf-update-view-org-agenda=. ;; ;;; Code: (defgroup emacsconf-update nil "Convenience functions for downloading the schedule and updating an Org file." :group 'communication) (defcustom emacsconf-update-ical-url "https://emacsconf.org/current/emacsconf.ics" "URL to get schedule updates from." :type 'string :group 'emacsconf-update) (defcustom emacsconf-update-create-entries t "Non-nil means create missing entries." :type '(choice (const t "Create missing entries") (const nil "Don't create missing entries")) :group 'emacsconf-update) (require 'icalendar) (require 'ox-icalendar) (defun emacsconf-update-org-from-ical-url () "Update current notes from iCalendar file at `emacsconf-update-ical-url'." (interactive) (emacsconf-update-org-from-ical-string (with-temp-buffer (url-insert-file-contents emacsconf-update-ical-url) (buffer-string)))) (defun emacsconf-update-org-from-ical-file (filename) "Prompt for FILENAME and update current notes from schedule." (interactive "FFilename: ") (emacsconf-update-org-from-ical-string (with-temp-buffer (insert-file-contents filename) (buffer-string)))) (defun emacsconf-ical-get-calendar-from-string (string) "Read the calendar data from STRING." (with-temp-buffer (insert string) (goto-char (point-min)) (with-current-buffer (icalendar--get-unfolded-buffer (current-buffer)) (goto-char (point-min)) (when (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t) (beginning-of-line) (icalendar--read-element nil nil))))) (defun emacsconf-update--get-time-from-event (event property zone-map) "Return time of EVENT in PROPERTY with possible conversion from ZONE-MAP." (let* ((dt (icalendar--get-event-property event property)) (tz (icalendar--find-time-zone (icalendar--get-event-property-attributes event property) zone-map))) (apply 'encode-time (icalendar--decode-isodatetime dt nil tz)))) (defun emacsconf-update--format-new-entry (event zone-map) "Return string for new entry for EVENT." (let* ((dtstart-time (emacsconf-update--get-time-from-event event 'DTSTART zone-map)) (dtend-time (emacsconf-update--get-time-from-event event 'DTEND zone-map))) (format "\n* %s\n:PROPERTIES:\n:UUID: %s\n:URL: %s\n:END:\nTentative schedule: %s--%s\n\n" (icalendar--convert-string-for-import (icalendar--get-event-property event 'SUMMARY)) (icalendar--get-event-property event 'UID) (icalendar--get-event-property event 'URL) (org-format-time-string (cdr org-time-stamp-formats) dtstart-time) (org-format-time-string (cdr org-time-stamp-formats) dtend-time)))) (defun emacsconf-update--update-entry (event zone-map) "Update the current Org heading to match EVENT. Use ZONE-MAP for timestamp decoding." (let* ((dtstart-time (emacsconf-update--get-time-from-event event 'DTSTART zone-map)) (dtend-time (emacsconf-update--get-time-from-event event 'DTEND zone-map))) (org-back-to-heading) (when (looking-at org-complex-heading-regexp) (replace-match (icalendar--convert-string-for-import (icalendar--get-event-property event 'SUMMARY)) t t nil 4)) (org-end-of-meta-data) (when (re-search-forward (concat "Tentative schedule: " org-ts-regexp "--" org-ts-regexp " *\n") nil (save-excursion (org-end-of-subtree))) (replace-match "")) (insert "Tentative schedule: " (org-format-time-string (cdr org-time-stamp-formats) dtstart-time) "--" (org-format-time-string (cdr org-time-stamp-formats) dtend-time) "\n"))) (defun emacsconf-update-org-from-ical-string (string) "Update Org headings in current file by using STRING." (save-excursion (let* ((ical (emacsconf-ical-get-calendar-from-string string)) (zone-map (icalendar--convert-all-timezones ical))) (mapc (lambda (e) (let ((pos (org-find-property "UUID" (icalendar--get-event-property e 'UID)))) (cond (pos (goto-char pos) (emacsconf-update--update-entry e zone-map)) (emacsconf-update-create-entries (goto-char (point-max)) (insert (emacsconf-update--format-new-entry e zone-map)))))) (icalendar--all-events ical))))) ;;;###autoload (defun emacsconf-update-view-org-agenda () "Display the agenda." (interactive) (let (times min-time max-time (org-agenda-files (list (buffer-file-name)))) ;; Collect all the times (save-excursion (goto-char (point-min)) (while (re-search-forward org-ts-regexp nil t) (setq times (cons (time-to-days (encode-time (org-parse-time-string (match-string 0)))) times))) (setq min-time (apply 'min times) max-time (apply 'max times))) (org-agenda-list nil min-time (1+ (- max-time min-time))))) (provide 'emacsconf-update) ;;; emacsconf-update.el ends here