Emacs calendar

Upcoming events

Error with url: https://www.meetup.com/Emacs-Madrid/events/ical/

  • Emacs FFM: Emacs FFM Meetup! October 2021 https://www.meetup.com/emacs-ffm/events/281293223/ Wed Oct 20 1000 Vancouver / 1200 Chicago / 1300 Toronto / 1700 GMT / 1900 Berlin / 2230 Kolkata – Thu Oct 21 0100 Singapore
  • Emacs APAC (virtual, in English) https://emacs-apac.gitlab.io/ Sat Oct 23 0130 Vancouver / 0330 Chicago / 0430 Toronto / 0830 GMT / 1030 Berlin / 1400 Kolkata / 1630 Singapore
  • Emacs Berlin (virtual, in English) https://emacs-berlin.org/ Wed Oct 27 0930 Vancouver / 1130 Chicago / 1230 Toronto / 1630 GMT / 1830 Berlin / 2200 Kolkata – Thu Oct 28 0030 Singapore
  • EmacsNYC: Monthly Online Meetup - Lightning Talks https://www.meetup.com/New-York-Emacs-Meetup/events/281224309/ Mon Nov 1 1600 Vancouver / 1800 Chicago / 1900 Toronto / 2300 GMT – Tue Nov 2 0000 Berlin / 0430 Kolkata / 0700 Singapore
  • EmacsATX: TBD https://www.meetup.com/EmacsATX/events/281302253/ Wed Nov 3 1630 Vancouver / 1830 Chicago / 1930 Toronto / 2330 GMT – Thu Nov 4 0030 Berlin / 0500 Kolkata / 0730 Singapore
  • Emacs Paris (virtual, in French) https://www.emacs-doctor.com/emacs-paris-user-group/ Thu Nov 4 0930 Vancouver / 1130 Chicago / 1230 Toronto / 1630 GMT / 1730 Berlin / 2200 Kolkata – Fri Nov 5 0030 Singapore
  • M-x Research (contact them for password): TBA https://m-x-research.github.io/ Fri Nov 5 0800 Vancouver / 1000 Chicago / 1100 Toronto / 1500 GMT / 1600 Berlin / 2030 Kolkata / 2300 Singapore
  • M-x Research (contact them for password): TBA https://m-x-research.github.io/ Fri Nov 19 0700 Vancouver / 0900 Chicago / 1000 Toronto / 1500 GMT / 1600 Berlin / 2030 Kolkata / 2300 Singapore
  • Emacs Berlin (virtual, in English) https://emacs-berlin.org/ Wed Nov 24 0930 Vancouver / 1130 Chicago / 1230 Toronto / 1730 GMT / 1830 Berlin / 2300 Kolkata – Thu Nov 25 0130 Singapore
  • Emacs APAC (virtual, in English) https://emacs-apac.gitlab.io/ Sat Nov 27 0030 Vancouver / 0230 Chicago / 0330 Toronto / 0830 GMT / 0930 Berlin / 1400 Kolkata / 1630 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:

(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
from urllib.request import urlopen
from icalendar import Calendar
from datetime import date, datetime
from dateutil.relativedelta import *
import recurring_ical_events
import pytz
import re
import pypandoc
import subprocess
import sys
import csv

#                 'Singapore': 'Emacs-SG',
other_meetups = {'EmacsNYC': 'New-York-Emacs-Meetup',
                 'EmacsSF': 'Emacs-SF',
                 'EmacsATX': 'EmacsATX',
                 'Boulder': 'Boulder-Emacs-Meetup',
                 'Pelotas, Brazil': 'Pelotas-Emacs-Meetup',
                 'Sao Paulo': 'Grupo-de-usuarios-de-Emacs-de-Sao-Paulo',
                 'Emacs FFM': 'emacs-ffm',
                 'London Emacs Hacking': 'London-Emacs-Hacking',
                 'London Emacs Lisp': 'London-Emacs-Lisp-Meetup',
                 'Stockholm': 'Stockholm-Emacs-Meetup',
                 'Madrid': 'Emacs-Madrid',
                 'Finland': 'Finland-Emacs-User-Group',
                 'Amsterdam': 'Amsterdam-Emacs-Users-Group',
                 'GenEmacs': 'GenEmacs',
                 'Johannesburg': 'Jozi-Emacs-Meetup',
                 'Delhi': 'Emacs-Delhi',
                 'Pune': 'the-peg'}
other_icals = [{'name': 'Atelier Emacs (in French)',
                'source': 'https://mobilizon.fr/@communaute_emacs_francophone/feed/ics'},
               {'name': 'M-x Research (contact them for password)',
                '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 - '}]
# https://www.meetup.com/Emacs-SF/events/ical/',

def summarized_event(e, timezones):
  times = [[e['DTSTART'].dt.astimezone(pytz.timezone(t[0])), t[0], e['DTSTART'].dt.astimezone(pytz.timezone(t[0])).utcoffset()] for t in timezones]
  times.sort(key=lambda x: x[2])
  s = ""
  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') + " " + re.sub('^.*?/', '', t[1])
    else:
       s += " / " + t[0].strftime('%H%M') + " " + re.sub('^.*?/', '', t[1])
  return "- %s %s %s" % (e['SUMMARY'], e['LOCATION'], s)
                     

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

for event in cal.walk():
  if event.name == 'VEVENT':
    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'))
                        
def merge_cal(main_cal, name, url, start_date, end_date, info=None):
   try:
     meetup_cal = Calendar.from_ical(urlopen(url).read())
   except:
     print("Error with url: %s" % url)
     return
   meetup_events = recurring_ical_events.of(meetup_cal).between(start_date, end_date)
   for event in meetup_events:
     if info and 'summary_re' in info:
       event['SUMMARY'] = re.sub(info['summary_re'], '', event['SUMMARY'])
     event['SUMMARY'] = name + ': ' + event['SUMMARY']
     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)
 
merge_meetup_events(cal, start_date, end_date)
for item in other_icals:
  merge_cal(cal, item['name'], item['source'], start_date, end_date, item)

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.utcnow()
highlight_end = datetime.utcnow() + relativedelta(weeks=+6)

for t in timezones:
  stub = "emacs-calendar-" + re.sub('^.*?/', '', t[0]).lower()
  ical_args = ["ical2html", "-l", "-f", "Times are in " + t[0], "-z", t[0], 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[0]] = open(stub + '.org', "w")

with open('events.csv', 'w', newline='') as csvfile:
  fieldnames = ['DTSTART', 'DTEND', 'LOCATION', 'SUMMARY', 'TEXT']
  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:
  desc = pypandoc.convert_text(e['DESCRIPTION'], 'org', format='html').replace('\\\\', '')
  utc = datetime.utcfromtimestamp(e['DTSTART'].dt.timestamp())
  if utc >= highlight_start and utc <= highlight_end:
    print(summarized_event(e, timezones))
  for t in timezones:
    zone = pytz.timezone(t[0])
    start = e['DTSTART'].dt.astimezone(zone)
    end = e['DTEND'].dt.astimezone(zone)
    files[t[0]].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 "~/code/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)
            (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