<<announce>>
(defun my-prepare-calendar-for-export ()
(interactive)
(with-current-buffer (find-file-noselect "~/sync/emacs-calendar/README.org")
(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))
(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)
(find-file “~/sync/emacs-news/index.org”) elisp:(org-export-to-file ‘html “README.html”) elisp:my-schedule-announcements-for-upcoming-emacs-meetups
- Hispa Emacs (virtual) https://hispa-emacs.org/ Wed Dec 14 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Thu Dec 15 0000 Asia/Singapore
- EmacsSF (in person): Ho ho ho it’s coffee.el https://www.meetup.com/emacs-sf/events/290226928/ Sat Dec 17 1400 America/Toronto
- M-x Research (contact them for password): TBA https://m-x-research.github.io/ Tue Dec 20 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Wed Dec 21 0000 Asia/Singapore
- Emacs APAC (virtual) https://emacs-apac.gitlab.io/ Sat Dec 24 0030 America/Vancouver - 0230 America/Chicago - 0330 America/Toronto - 0830 Etc/GMT - 0930 Europe/Berlin - 1400 Asia/Kolkata - 1630 Asia/Singapore
- Emacs Berlin (hybrid, in English) https://emacs-berlin.org/ Wed Dec 28 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1730 Etc/GMT - 1830 Europe/Berlin - 2300 Asia/Kolkata – Thu Dec 29 0130 Asia/Singapore
- M-x Research (contact them for password): TBA https://m-x-research.github.io/ Tue Jan 3 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Wed Jan 4 0000 Asia/Singapore
- EmacsATX: Emacs Social https://www.meetup.com/emacsatx/events/290200609/ Wed Jan 4 1630 America/Vancouver - 1830 America/Chicago - 1930 America/Toronto – Thu Jan 5 0030 Etc/GMT - 0130 Europe/Berlin - 0600 Asia/Kolkata - 0830 Asia/Singapore
- Emacs Paris (virtual, in French) https://www.emacs-doctor.com/emacs-paris-user-group/ Thu Jan 5 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1730 Etc/GMT - 1830 Europe/Berlin - 2300 Asia/Kolkata – Fri Jan 6 0130 Asia/Singapore
- Atelier Emacs Montpellier (in person) https://lebib.org/date/atelier-emacs Fri Jan 13 1200 America/Toronto
- M-x Research (contact them for password): TBA https://m-x-research.github.io/ Tue Jan 17 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Wed Jan 18 0000 Asia/Singapore
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!
- America/Vancouver
- America/Chicago
- America/Toronto
- Etc/GMT
- Europe/Berlin
- Asia/Kolkata
- Asia/Singapore
pip3 install icalevents recurring_ical_events pypandoc
from urllib.request import urlopen
import icalendar
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 (in person)': 'Emacs-SF',
'EmacsATX': 'EmacsATX',
'Boulder': 'Boulder-Emacs-Meetup',
'Pelotas, Brazil': 'Pelotas-Emacs-Meetup',
'Emacs FFM': 'emacs-ffm',
'London Emacs Hacking': 'London-Emacs-Hacking',
'London Emacs Lisp': 'London-Emacs-Lisp-Meetup',
'Stockholm': 'Stockholm-Emacs-Meetup',
'Finland': 'Finland-Emacs-User-Group',
'Delhi': 'Emacs-Delhi',
'Pune': 'the-peg'}
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 (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/',
all_timezones = []
def summarized_event(e, timezones):
if 'in person' in e['SUMMARY']:
tz = e['DTSTART'].dt.tzinfo
times = [[e['DTSTART'].dt.astimezone(tz), tz.zone, e['DTSTART'].dt.astimezone(tz).utcoffset()]]
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])
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') + " " + 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 = date(date.today().year, date.today().month, 1)
end_date = date(date.today().year + 1, 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:
meetup_cal = icalendar.Calendar.from_ical(urlopen(url).read())
except:
print("Error with url: %s" % url)
return
# Copy the timezone components
for tz in meetup_cal.walk():
if tz.name == 'VTIMEZONE' and tz['TZID'] not in all_timezones:
print(tz)
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:
if info and 'summary_re' in info:
event['SUMMARY'] = re.sub(info['summary_re'], '', event['SUMMARY'])
if '(virtual)' in event['SUMMARY'] or '(hybrid)' in event['SUMMARY']:
name = re.sub(' \(in person\)', '')
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)
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)
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.utcnow()
highlight_end = datetime.utcnow() + relativedelta(weeks=+6)
for t in timezones:
stub = "emacs-calendar-" + re.sub('^.*?/', '', t).lower()
ical_args = ["ical2html", "-l", "-f", "Times are in " + t, "-z", t, 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:
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)
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))
VTIMEZONE({‘TZID’: vText(‘b’America/Los_Angeles”), ‘TZURL’: ‘http://tzurl.org/zoneinfo-outlook/America/Los_Angeles’, ‘X-LIC-LOCATION’: vText(‘b’America/Los_Angeles”)}, DAYLIGHT({‘TZOFFSETFROM’: <icalendar.prop.vUTCOffset object at 0x7f70766face0>, ‘TZOFFSETTO’: <icalendar.prop.vUTCOffset object at 0x7f70766f8880>, ‘TZNAME’: vText(‘b’PDT”), ‘DTSTART’: <icalendar.prop.vDDDTypes object at 0x7f70766fbeb0>, ‘RRULE’: vRecur({‘FREQ’: [‘YEARLY’], ‘BYMONTH’: [3], ‘BYDAY’: [‘2SU’]})}), STANDARD({‘TZOFFSETFROM’: <icalendar.prop.vUTCOffset object at 0x7f70766f9d80>, ‘TZOFFSETTO’: <icalendar.prop.vUTCOffset object at 0x7f70766fbdf0>, ‘TZNAME’: vText(‘b’PST”), ‘DTSTART’: <icalendar.prop.vDDDTypes object at 0x7f70766f9900>, ‘RRULE’: vRecur({‘FREQ’: [‘YEARLY’], ‘BYMONTH’: [11], ‘BYDAY’: [‘1SU’]})})) VTIMEZONE({‘TZID’: vText(‘b’America/Chicago”), ‘TZURL’: ‘http://tzurl.org/zoneinfo-outlook/America/Chicago’, ‘X-LIC-LOCATION’: vText(‘b’America/Chicago”)}, DAYLIGHT({‘TZOFFSETFROM’: <icalendar.prop.vUTCOffset object at 0x7f70770c9a80>, ‘TZOFFSETTO’: <icalendar.prop.vUTCOffset object at 0x7f70770c9810>, ‘TZNAME’: vText(‘b’CDT”), ‘DTSTART’: <icalendar.prop.vDDDTypes object at 0x7f70770cbeb0>, ‘RRULE’: vRecur({‘FREQ’: [‘YEARLY’], ‘BYMONTH’: [3], ‘BYDAY’: [‘2SU’]})}), STANDARD({‘TZOFFSETFROM’: <icalendar.prop.vUTCOffset object at 0x7f70770c8760>, ‘TZOFFSETTO’: <icalendar.prop.vUTCOffset object at 0x7f70770c8610>, ‘TZNAME’: vText(‘b’CST”), ‘DTSTART’: <icalendar.prop.vDDDTypes object at 0x7f70771c9c30>, ‘RRULE’: vRecur({‘FREQ’: [‘YEARLY’], ‘BYMONTH’: [11], ‘BYDAY’: [‘1SU’]})}))
- Hispa Emacs (virtual) https://hispa-emacs.org/ Wed Dec 14 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Thu Dec 15 0000 Asia/Singapore
- EmacsSF (in person): Ho ho ho it’s coffee.el https://www.meetup.com/emacs-sf/events/290226928/ Sat Dec 17 1100 America/Los_Angeles
- M-x Research (contact them for password): TBA https://m-x-research.github.io/ Tue Dec 20 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Wed Dec 21 0000 Asia/Singapore
- Emacs APAC (virtual) https://emacs-apac.gitlab.io/ Sat Dec 24 0030 America/Vancouver - 0230 America/Chicago - 0330 America/Toronto - 0830 Etc/GMT - 0930 Europe/Berlin - 1400 Asia/Kolkata - 1630 Asia/Singapore
- Emacs Berlin (hybrid, in English) https://emacs-berlin.org/ Wed Dec 28 0930 America/Vancouver - 1130 America/Chicago - 1230 America/Toronto - 1730 Etc/GMT - 1830 Europe/Berlin - 2300 Asia/Kolkata – Thu Dec 29 0130 Asia/Singapore
- M-x Research (contact them for password): TBA https://m-x-research.github.io/ Tue Jan 3 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Wed Jan 4 0000 Asia/Singapore
- EmacsATX: Emacs Social https://www.meetup.com/emacsatx/events/290200609/ Wed Jan 4 1630 America/Vancouver - 1830 America/Chicago - 1930 America/Toronto – Thu Jan 5 0030 Etc/GMT - 0130 Europe/Berlin - 0600 Asia/Kolkata - 0830 Asia/Singapore
- Emacs Paris (virtual, in French) https://www.emacs-doctor.com/emacs-paris-user-group/ Thu Jan 5 0830 America/Vancouver - 1030 America/Chicago - 1130 America/Toronto - 1630 Etc/GMT - 1730 Europe/Berlin - 2200 Asia/Kolkata – Fri Jan 6 0030 Asia/Singapore
- Atelier Emacs Montpellier (in person) https://lebib.org/date/atelier-emacs Fri Jan 13 1800 Europe/Paris
- M-x Research (contact them for password): TBA https://m-x-research.github.io/ Tue Jan 17 0800 America/Vancouver - 1000 America/Chicago - 1100 America/Toronto - 1600 Etc/GMT - 1700 Europe/Berlin - 2130 Asia/Kolkata – Wed Jan 18 0000 Asia/Singapore
rsync -avze ssh ./ web:/var/www/emacslife.com/calendar/ --exclude=.git
sent 5,602 bytes received 388 bytes 11,980.00 bytes/sec total size is 2,501,195 speedup is 417.56
(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))))))
(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)))
(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" (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)))
(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)))))))
elisp:(oddmuse-edit “EmacsWiki” “Usergroups”)
- (View America/Vancouver agenda View America/Chicago agenda View America/Toronto agenda View Etc/GMT agenda View Europe/Berlin agenda View Asia/Kolkata agenda View Asia/Singapore agenda)