Emacs calendar

Prerequisites

sudo apt-get install ical2html pandoc python3-icalendar python3-recurring-ical-events  python3-pypandoc

You'll also need https://src.alexschroeder.ch/oddmuse.el.git/

Upcoming events

  • M-x Research: TBA https://m-x-research.github.io/ Wed Nov 20 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Thu Nov 21 0000 Asia/Singapore
  • Emacs APAC (virtual) https://emacs-apac.gitlab.io/ Sat Nov 23 0030 America/Vancouver - 0230 America/Chicago - 0330 America/Toronto - 0830 Etc/GMT - 0930 Europe/Berlin - 1400 Asia/Kolkata - 1630 Asia/Singapore
  • Emacs Berlin (virtual, in English) https://emacs-berlin.org/ Wed Nov 27 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1730 Etc/GMT - 1830 Europe/Berlin - 2300 Asia/Kolkata – Thu Nov 28 0130 Asia/Singapore
  • Emacs Paris: S: Emacs workshop in Paris (online) https://emacs-doctor.com/ Thu Dec 5 0830 America/Vancouver - 1030 America/Chicago - 1130 America/Toronto - 1630 Etc/GMT - 1730 Europe/Berlin - 2200 Asia/Kolkata – Fri Dec 6 0030 Asia/Singapore
  • Emacs.si (in person): Emacs.si meetup #15 2024 (v #živo) https://dogodki.kompot.si/events/57815aa7-f253-4768-8059-9fbede8de0f9 Thu Dec 5 1900 CET
  • M-x Research: TBA https://m-x-research.github.io/ Fri Dec 6 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Sat Dec 7 0000 Asia/Singapore
  • Atelier Emacs Montpellier (in person) https://lebib.org/date/atelier-emacs Fri Dec 13 1800 Europe/Paris
  • M-x Research: TBA https://m-x-research.github.io/ Wed Dec 18 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Thu Dec 19 0000 Asia/Singapore
  • Emacs Berlin (virtual, in English) https://emacs-berlin.org/ Wed Dec 25 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1730 Etc/GMT - 1830 Europe/Berlin - 2300 Asia/Kolkata – Thu Dec 26 0130 Asia/Singapore
  • Emacs APAC (virtual) https://emacs-apac.gitlab.io/ Sat Dec 28 0030 America/Vancouver - 0230 America/Chicago - 0330 America/Toronto - 0830 Etc/GMT - 0930 Europe/Berlin - 1400 Asia/Kolkata - 1630 Asia/Singapore

Introduction

This calendar is maintained by sacha@sachachua.com. You can find it at https://emacslife.com/calendar/

You can find a list of upcoming events and other meet-ups at https://www.emacswiki.org/emacs/Usergroups.

Or you can add this iCal to your calendar program:

https://emacslife.com/calendar/emacs-calendar.ics

Or you view the following HTML calendars:

(America/Vancouver America/Chicago America/Toronto Etc/GMT Europe/Berlin Asia/Kolkata Asia/Singapore)

(Let me know if you want me to add yours! - mailto:sacha@sachachua.com)

Or you periodically download and include one of these files in your Org agenda files:

Enjoy!

Code I use to run it

Timezones

  • America/Vancouver
  • America/Chicago
  • America/Toronto
  • Etc/GMT
  • Europe/Berlin
  • Asia/Kolkata
  • Asia/Singapore

Download and parse the iCal file with Python

pip3 install icalevents recurring_ical_events pypandoc requests pytz
import requests
import http.cookiejar as cookielib
import os
cj = cookielib.MozillaCookieJar(os.path.expanduser('~/.cache/meetup-cookies.txt'))
cj.load()
data = requests.get('https://www.meetup.com/emacs-sf/events/ical', cookies=cj)
print(data)
import requests
import http.cookiejar as cookielib
import os
from urllib.request import urlopen
import icalendar
import datetime
from dateutil.relativedelta import *
import recurring_ical_events
import pytz
import re
import pypandoc
import subprocess
import sys
import csv
import requests

INCLUDE_MEETUPS = True
INCLUDE_OTHER_ICALS = True

cj = cookielib.MozillaCookieJar(os.path.expanduser('~/.cache/meetup-cookies.txt'))
cj.load()

#                 'Singapore': 'Emacs-SG',
other_meetups = {'EmacsNYC': 'New-York-Emacs-Meetup',
                 'EmacsSF (in person)': 'Emacs-SF',
                 'EmacsATX': 'EmacsATX',
                 'Pelotas, Brazil': 'Pelotas-Emacs-Meetup',
                 'Emacs FFM': 'emacs-ffm',
                 'London Emacs (in person)': 'London-Emacs-Hacking',
                 'London Emacs Lisp': 'London-Emacs-Lisp-Meetup',
                 'Finland': 'Finland-Emacs-User-Group',
                 'Delhi': 'Emacs-Delhi'}
other_icals = [ {'name': 'Atelier Emacs (in French)',
                 'source': 'https://mobilizon.fr/@communaute_emacs_francophone/feed/ics'},
                {'name': 'Emacs Paris',
                 'source': 'https://emacs-doctor.com/emacs-paris-meetups.ics',
                 'url':'https://emacs-doctor.com/'},
                {'name': 'M-x Research',
                'url': 'https://m-x-research.github.io/',
                'source': 'https://calendar.google.com/calendar/ical/o0tiadljp5dq7lkb51mnvnrh04%40group.calendar.google.com/public/basic.ics',
                'summary_re': r'^M-x Research - '},
{'name': 'Emacs.si (in person)',
 'url': 'https://dogodki.kompot.si/@emacssi',
 'source': 'https://dogodki.kompot.si/@emacssi/feed/ics'}]
# https://www.meetup.com/Emacs-SF/events/ical/',
all_timezones = []

def summarized_event(e, timezones):
  s = ''
  times = None
  if re.search('in person|v #živo|au Bib', e['SUMMARY']):
    m = re.search("Event time: (.*?)[<\n]", e.get('description') or '')
    if m:
      s += '- ' + m.group(1)
    elif e['DTSTART'].dt.tzinfo:
      tz = e['DTSTART'].dt.tzinfo
      times = [[e['DTSTART'].dt.astimezone(tz), tz.zone, e['DTSTART'].dt.astimezone(tz).utcoffset()]]
    else:
      s += '- see event for details'
  else:
    times = [[e['DTSTART'].dt.astimezone(pytz.timezone(t)), t, e['DTSTART'].dt.astimezone(pytz.timezone(t)).utcoffset()] for t in timezones]
    times.sort(key=lambda x: x[2])
  if times:
    for i, t in enumerate(times):
      if i == 0 or t[0].day != times[i - 1][0].day:
         if i > 0:
           s += " -- "
         s += t[0].strftime('%a %b %-d %H%M') + " " + t[1]
      else:
         s += " - " + t[0].strftime('%H%M') + " " + t[1]
  if e['LOCATION']:
    return "- %s %s %s" % (e['SUMMARY'], e['LOCATION'], s)
  else:
    return "- %s %s" % (e['SUMMARY'], s)

link = "https://calendar.google.com/calendar/ical/c_rkq3fc6u8k1nem23qegqc90l6c%40group.calendar.google.com/public/basic.ics"
f = urlopen(link)
cal = icalendar.Calendar.from_ical(f.read())
start_date = datetime.date(datetime.date.today().year, datetime.date.today().month, 1)

end_date = datetime.date(datetime.date.today().year + 1, datetime.date.today().month, 1)

for event in cal.walk():
  if event.name == 'VEVENT' and not '(ignore)' in event.name:
    event['UID'] = str(event['DTSTART'].dt.timestamp()) + '-' + event['UID']
    if event.get('location') == '':
      match = re.search(r'href="([^"]+)"', event.get('description'))
      if not match:
        match = re.search('^(http.*?)(&nbsp;|<br>|\n)', event.get('description'))
      if match:
        event['location'] = match.group(1)
      else:
        print(event.get('description'))
  if event.name == 'VTIMEZONE' and event['TZID'] not in all_timezones:
    all_timezones.append(event['TZID'])

def merge_cal(main_cal, name, url, start_date, end_date, info=None):
  try:
    if 'meetup.com' in url:
      data = requests.get(url, cookies=cj)
    else:
      data = requests.get(url)
    meetup_cal = icalendar.Calendar.from_ical(data.content)
  except Exception as e:
    print("Error with url: %s" % url)
    print(repr(e))
    return
  # Copy the timezone components
  for tz in meetup_cal.walk():
    if tz.name == 'VTIMEZONE' and tz['TZID'] not in all_timezones:
      all_timezones.append(tz['TZID'])
      main_cal.add_component(tz)
  meetup_events = recurring_ical_events.of(meetup_cal).between(start_date, end_date)
  for event in meetup_events:
    low = event['SUMMARY'].lower()
    if info and 'summary_re' in info:
      event['SUMMARY'] = re.sub(info['summary_re'], '', event['SUMMARY'])
    if re.search(r'virtual|hybrid|online', low):
      name = re.sub(r' \(in person\)', '', name)
    event['SUMMARY'] = name + ': ' + event['SUMMARY']
    event['UID'] = str(event['DTSTART'].dt.timestamp()) + '-' + event['UID']
    event['LOCATION'] = ('URL' in event and event['URL']) or (info and ('url' in info) and info['url'])
    main_cal.add_component(event)

def merge_meetup_events(cal, start_date, end_date):
  global other_meetups
  for name, identifier in other_meetups.items():
    url = "https://www.meetup.com/%s/events/ical/" % (identifier)
    merge_cal(cal, name, url, start_date, end_date)

if INCLUDE_MEETUPS:
  merge_meetup_events(cal, start_date, end_date)
if INCLUDE_OTHER_ICALS:
  for item in other_icals:
    merge_cal(cal, item['name'], item['source'], start_date, end_date, item)

def convert_events_to_utc(cal):
  # Convert everything to UTC?
  utc = pytz.timezone('UTC')
  for event in cal.walk():
    if event.name == 'VEVENT' in event.name:
      for attr in ['DTSTART', 'DTEND', 'DTSTAMP', 'RECURRENCE-ID']:
        if attr in event:
          event[attr].dt = event[attr].dt.astimezone(utc)
          event[attr].params.clear()



f = open('emacs-calendar.ics', 'wb')

f.write(cal.to_ical())
f.close()
events = recurring_ical_events.of(cal).between(start_date, end_date)
events.sort(key=lambda x: x['DTSTART'].dt)
files = {}
org_date = "%Y-%m-%d %a %H:%M" # 2006-11-01 Wed 19:15
# Prepare string for copying
highlight_start = datetime.datetime.now(pytz.UTC)
highlight_end = datetime.datetime.now(pytz.UTC) + relativedelta(weeks=+6)
all_end = datetime.datetime.now(pytz.UTC) + relativedelta(weeks=+1)

for t in timezones:
  stub = "emacs-calendar-" + re.sub('^.*?/', '', t).lower()
  ical_args = ["ical2html", "-l", "-f", "Times are in " + t, "-z", t, datetime.datetime.today().strftime("%Y%m01"), "P8W", "emacs-calendar.ics"]
  output = subprocess.check_output(ical_args).decode(sys.stdout.encoding)
  changed = re.sub(r'<span class=summary>([^<]+)</span>\n<pre><b class=location>([^<]+)</b></pre>',
                   r'<span class="summary"><a href="\2">\1</a></span>', output)
  f = open(stub + '.html', 'wb')
  f.write(changed.encode(sys.stdout.encoding))
  f.close()
  files[t] = open(stub + '.org', "w")

with open('events.csv', 'w', newline='') as csvfile:
  fieldnames = ['DTSTART', 'DTEND', 'LOCATION', 'SUMMARY', 'TEXT', 'TZID']
  writer = csv.DictWriter(csvfile, fieldnames=fieldnames, extrasaction='ignore')
  writer.writeheader()
  for e in events:
    writer.writerow({**e,
                     'DTSTART': e['DTSTART'].dt.isoformat(),
                     'DTEND': e['DTEND'].dt.isoformat(),
                     'TEXT': summarized_event(e, timezones)
                     })


for e in events:
  if 'DESCRIPTION' not in e:
    e['DESCRIPTION'] = ''
  desc = pypandoc.convert_text(e['DESCRIPTION'], 'org', format='html').replace('\\\\', '')
  utc = datetime.datetime.fromtimestamp(e['DTSTART'].dt.timestamp(), datetime.UTC)
  if utc >= highlight_start and utc <= highlight_end:
    if 'Mastering Emacs' in e['DESCRIPTION'] and utc > all_end:
      pass # do nothing
    else:
      print(summarized_event(e, timezones))
  for t in timezones:
    zone = pytz.timezone(t)
    start = e['DTSTART'].dt.astimezone(zone)
    end = e['DTEND'].dt.astimezone(zone)
    files[t].write("""* %s
:PROPERTIES:
:LOCATION: %s
:END:
<%s>--<%s>

%s

""" % (e['SUMMARY'], e['LOCATION'], start.strftime(org_date), end.strftime(org_date), desc))

Sync

rsync -avze ssh ./ web:/var/www/emacslife.com/calendar/ --exclude=.git

Convert timezones

(defun my-summarize-times (time timezones)
  (let (prev-day)
    (mapconcat
     (lambda (tz)
       (let ((cur-day (format-time-string "%a %b %-e" time tz))
             (cur-time (format-time-string "%H%MH %Z" time tz)))
         (if (equal prev-day cur-day)
             cur-time
           (setq prev-day cur-day)
           (concat cur-day " " cur-time))))
     timezones
     " / ")))

(defun my-org-summarize-event-in-timezones (timezones)
  (interactive (list (or timezones my-timezones)))
  (save-window-excursion
    (save-excursion
      (when (derived-mode-p 'org-agenda-mode) (org-agenda-goto))
      (when (re-search-forward org-element--timestamp-regexp nil (save-excursion (org-end-of-subtree) (point)))
        (goto-char (match-beginning 0))
        (let* ((times (org-element-timestamp-parser))
               (start-time (org-timestamp-to-time (org-timestamp-split-range times)))
               (msg (format "%s - %s - %s"
                            (org-get-heading t t t t)
                            (my-summarize-times start-time timezones)
                            ;; (cond
                            ;;  ((time-less-p (org-timestamp-to-time (org-timestamp-split-range times t)) (current-time))
                            ;;   "(past)")
                            ;;  ((time-less-p (current-time) start-time)
                            ;;   (concat "in " (format-seconds "%D %H %M%Z" (time-subtract start-time (current-time)))))
                            ;;  (t "(ongoing)"))
                            (org-entry-get (point) "LOCATION"))))
          (if (called-interactively-p 'any)
              (progn
                (message "%s" msg)
                (kill-new msg))
            msg))))))
my-org-summarize-event-in-timezones

Summarize upcoming ones

(defun my-summarize-upcoming-events (limit timezones)
  (interactive (list (org-read-date nil t) my-timezones))
  (let (result)
    (with-current-buffer (find-file-noselect "~/sync/emacs-calendar/emacs-calendar-toronto.org")
      (goto-char (point-min))
      (org-map-entries
       (lambda ()
         (save-excursion
           (when (re-search-forward org-element--timestamp-regexp nil (save-excursion (org-end-of-subtree) (point)))
             (goto-char (match-beginning 0))
             (let ((time (org-timestamp-to-time (org-timestamp-split-range (org-element-timestamp-parser)))))
               (when (and (time-less-p (current-time) time)
                          (time-less-p time limit))
                 (setq result (cons
                               (cons time
                                     (my-org-summarize-event-in-timezones timezones)) result)))))))))
    (setq result (mapconcat
                  (lambda (o) (format "- %s" (cdr  o)))
                  (sort result (lambda (a b)
                                 (time-less-p (car a) (car b))
                                 ))
                  "\n"))
    (if (interactive-p)
        (insert result)
      result)))
my-summarize-upcoming-events

Announcing Emacs events

(defun my-announce-on-irc (channels message host port)
  (with-temp-buffer
    (insert "PASS " erc-password "\n"
            "USER " erc-nick "\n"
            "NICK " erc-nick "\n"
            (mapconcat (lambda (o)
                         (format "PRIVMSG %s :%s\n" o message))
                       channels "")
            "QUIT\n")
    (call-process-region (point-min) (point-max) "ncat" nil 0 nil
                         "--ssl" host (number-to-string port))))

(defun my-announce-on-irc-and-twitter (time channels message host port)
  (when (< (time-to-seconds (subtract-time (current-time) time)) (* 5 60))
    (shell-command-to-string (format
                              (if my-laptop-p
                                  "zsh -l -c 'rvm use 2.4.1; t update %s'"
                                "bash -l -c 't update %s'")
                              (shell-quote-argument message)))
    (my-announce-on-irc channels message host port)))

(defun my-schedule-announcement (time message)
  (interactive (list (org-read-date t t) (read-string "Message: ")))
  (run-at-time time nil #'my-announce-on-irc-and-twitter time '("#emacs" "#emacsconf") message erc-server erc-port))

(defun my-org-table-as-alist (table)
  "Convert TABLE to an alist. Remember to set :colnames no."
  (let ((headers (seq-map 'intern (car table))))
    (cl-loop for x in (cdr table) collect (-zip headers x))))

(defun my-schedule-announcements-for-upcoming-emacs-meetups ()
  (interactive)
  (cancel-function-timers #'my-announce-on-irc-and-twitter)
  (let ((events (my-org-table-as-alist (pcsv-parse-file "events.csv")))
        (now (current-time))
        (before-limit (time-add (current-time) (seconds-to-time (* 14 24 60 60)))))
    (mapc (lambda (o)
            (unless (string-match "in person\\|workshop" (alist-get 'SUMMARY o))
              (let* ((start-time (encode-time (parse-time-string (alist-get 'DTSTART o))))
                     (fifteen-minutes-before (seconds-to-time (- (time-to-seconds start-time) (* 15 60)))))
                (when (and (time-less-p now fifteen-minutes-before)
                           (time-less-p fifteen-minutes-before before-limit))
                  (my-schedule-announcement fifteen-minutes-before
                                            (format "In 15 minutes: %s - see %s for details"
                                                    (alist-get 'SUMMARY o)
                                                    (alist-get 'LOCATION o))))
                (when (and (time-less-p now start-time)
                           (time-less-p start-time before-limit))
                  (my-schedule-announcement start-time
                                            (format "Starting now: %s - see %s for details"
                                                    (alist-get 'SUMMARY o)
                                                    (alist-get 'LOCATION o)))))))
          events)))

Update EmacsWiki

(use-package oddmuse
:load-path "~/vendor/oddmuse-el"
:if my-laptop-p
:ensure nil
:config (oddmuse-mode-initialize)
:hook (oddmuse-mode-hook .
          (lambda ()
            (unless (string-match "question" oddmuse-post)
              (when (string-match "EmacsWiki" oddmuse-wiki)
                (setq oddmuse-post (concat "uihnscuskc=1;" oddmuse-post)))
              (when (string-match "OddmuseWiki" oddmuse-wiki)
                (setq oddmuse-post (concat "ham=1;" oddmuse-post)))))))

nil

Back to top | E-mail me