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:
- emacs-calendar-vancouver.org
- emacs-calendar-chicago.org
- emacs-calendar-toronto.org
- emacs-calendar-gmt.org
- emacs-calendar-berlin.org
- emacs-calendar-kolkata.org
- emacs-calendar-singapore.org
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.*?)( |<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