Using Org Mode, Emacs Lisp, and TRAMP to parse meetup calendar entries and generate a crontab
| org, emacsTimes and time zones trip me up. Even with calendar notifications, I still fumble scheduled events. Automation helps me avoid embarrassing hiccups.
We run BigBlueButton as a self-hosted web conferencing server for EmacsConf. It needs at least 8 GB of RAM when active. When it's dormant, it fits on a 1 GB RAM virtual private server. It's easy enough to scale the server up and down as needed. Using the server for Emacs meetups in between EmacsConfs gives people a way to get together, and it also means I can regularly test the infrastructure. That makes scaling it up for EmacsConf less nerve-wracking.
I have some code that processes various Emacs
meetup iCalendar files (often with repeating
entries) and combines them into one iCal file that
people can subscribe to calendar, as well as Org
files in different timezones that they can include
in their org-agenda-files
. The code I use to
parse the iCal seems to handle time zones and
daylight savings time just fine. I set it up so
that the Org files have simple non-repeating
entries, which makes them easy to parse. I can use
the Org file to determine the scheduled jobs to
run with cron on a home server (named xu4) that's
up all the time.
This code parses the Org file for schedule information, then generates pairs of crontab entries. The first entry scales the BigBlueButton server up 1 hour before the event using my bbb-testing script, and the second entry scales the server down 6 hours after the event using my bbb-dormant script (more info). That gives organizers time to test it before the event starts, and it gives people plenty of time to chat. A shared CPU 8 GB RAM Linode costs USD 0.072 per hour, so that's USD 0.50 per meetup hosted.
Using #+begin_src emacs-lisp :file "/ssh:xu4:~/bbb.crontab" :results file
as the header for my code block and using an
SSH agent for authentication lets me use TRAMP to
write the file directly to the server. (See
Results of Evaluation (The Org Manual))
(let* ((file "/home/sacha/sync/emacs-calendar/emacs-calendar-toronto.org")
(time-format "%M %H %d %m")
(bbb-meetups "OrgMeetup\\|Emacs Berlin\\|Emacs APAC")
(scale-up "/home/sacha/bin/bbb-testing")
(scale-down "/home/sacha/bin/bbb-dormant"))
(mapconcat
(lambda (o)
(let ((start-time (format-time-string time-format (- (car o) 3600 )))
(end-time (format-time-string time-format (+ (car o) (* 6 3600)))))
(format "# %s\n%s * %s\n%s * %s\n"
(cdr o)
start-time
scale-up
end-time
scale-down)))
(delq nil
(with-temp-buffer
(insert-file-contents file)
(org-mode)
(goto-char (point-min))
(org-map-entries
(lambda ()
(when (and
(string-match bbb-meetups (org-entry-get (point) "ITEM"))
(re-search-forward org-tr-regexp (save-excursion (org-end-of-subtree)) t))
(let ((time (match-string 0)))
(cons (org-time-string-to-seconds time)
(format "%s - %s" (org-entry-get (point) "ITEM") time)))))
"LEVEL=1")))
"\n"))
The code makes entries that look like this:
# OrgMeetup (virtual) - <2025-06-11 Wed 12:00>--<2025-06-11 Wed 14:00> 00 11 11 06 * /home/sacha/bin/bbb-testing 00 18 11 06 * /home/sacha/bin/bbb-dormant # Emacs Berlin (hybrid, in English) - <2025-06-25 Wed 12:30>--<2025-06-25 Wed 14:30> 30 11 25 06 * /home/sacha/bin/bbb-testing 30 18 25 06 * /home/sacha/bin/bbb-dormant # Emacs APAC: Emacs APAC meetup (virtual) - <2025-06-28 Sat 04:30>--<2025-06-28 Sat 06:00> 30 03 28 06 * /home/sacha/bin/bbb-testing 30 10 28 06 * /home/sacha/bin/bbb-dormant
This works because meetups don't currently overlap. If there were, I'll need to tweak the code so that the server isn't downscaled in the middle of a meetup. It'll be a good problem to have.
I need to load the crontab entries by using
crontab bbb.crontab
. Again, I can tell Org Mode
to run this on the xu4 home server. This time I
use the :dir argument to specify the default
directory, like this:
#+begin_src sh :dir "/ssh:xu4:~" :results silent
crontab bbb.crontab
#+end_src
Then cron can take care of things automatically, and I'll just get the e-mail notifications from Linode telling me that the server has been resized. This has already come in handy, like when I thought of Emacs APAC as being on Saturday, but it was actually on Friday my time.
I have another Emacs Lisp block that I use to
retrieve all the info and update the list of
meetups. I can add (goto-char (org-find-property
"CUSTOM_ID" "crontab"))
to find this section and
use org-babel-execute-subtree
to execute all the
code blocks. That makes it an automatic part of my
process for updating the Emacs Calendar and Emacs
News. Here's the code that does the calendar part
(Org source):
(defun my-prepare-calendar-for-export ()
(interactive)
(with-current-buffer (find-file-noselect "~/sync/emacs-calendar/README.org")
(save-restriction
(widen)
(goto-char (point-min))
(re-search-forward "#\\+NAME: event-summary")
(org-ctrl-c-ctrl-c)
(org-export-to-file 'html "README.html")
;; (unless my-laptop-p (my-schedule-announcements-for-upcoming-emacs-meetups))
;; update the crontab
(goto-char (org-find-property "CUSTOM_ID" "crontab"))
(org-babel-execute-subtree)
(when my-laptop-p
(org-babel-goto-named-result "event-summary")
(re-search-forward "^- ")
(goto-char (match-beginning 0))
(let ((events (org-babel-read-result)))
(oddmuse-edit "EmacsWiki" "Usergroups")
(goto-char (point-min))
(delete-region (progn (re-search-forward "== Upcoming events ==\n\n") (match-end 0))
(progn (re-search-forward "^$") (match-beginning 0)))
(save-excursion (insert (mapconcat (lambda (s) (concat "* " s "\n")) events ""))))))))
(my-prepare-calendar-for-export)
I used a similar technique to generate the
EmacsConf crontabs for automatically switching to
the next talk. For that one, I used Emacs Lisp to
write the files directly instead of using the
:file
header argument for Org Mode source
blocks. That made it easier to loop over multiple
files.
Hmm. Come to think of it, the technique of "go to
a specific subtree and then execute it" is pretty
powerful. In the past, I've found it handy to
execute source blocks by name. Executing a subtree
by custom ID is even more useful because I can
easily mix source blocks in different languages or
include other information. I think that's worth
adding a my-org-execute-subtree-by-custom-id
function to my Emacs configuration. Combined with
an elisp:
link, I can make links that execute
functional blocks that might even be in different
files. That could be a good starting point for a
dashboard.
I love the way Emacs can easily work with files and scripts in different languages on different computers, and how it can help me with times and time zones too. This code should help me avoid brain hiccups and calendar mixups so that people can just enjoy getting together. Now I don't have to worry about whether I remembered to set up cron entries and if I did the math right for the times. We'll see how it holds up!