├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.rst ├── create.py ├── index.tmpl ├── prune.py ├── robots.txt ├── serve.py └── zones.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [djc] 2 | patreon: dochtman 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.pyc 3 | index.html 4 | tzdata-latest.tar.gz 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), 3 | to deal in the Software without restriction, including without limitation 4 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | and/or sell copies of the Software, and to permit persons to whom 6 | the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included 9 | in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 14 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 16 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 17 | IN THE SOFTWARE. 18 | 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GENTOO_TZDATA = $(shell ls /usr/portage/distfiles/tzdata20* 2>/dev/null | tail -n1) 2 | TZDATA_LATEST = $(or $(GENTOO_TZDATA),tzdata-latest.tar.gz) 3 | TZDATA_URL = ftp://ftp.iana.org/tz/tzdata-latest.tar.gz 4 | 5 | index.html: pruned.json index.tmpl 6 | @python3 create.py pruned.json > index.html 7 | 8 | pruned.json: source.json prune.py 9 | @python3 prune.py source.json > pruned.json 10 | 11 | source.json: zones.py $(TZDATA_LATEST) 12 | @python3 zones.py $(TZDATA_LATEST) > $@ 13 | 14 | tzdata-latest.tar.gz: 15 | @echo Downloading $(TZDATA_URL) 16 | @wget -q $(TZDATA_URL) 17 | 18 | serve: 19 | @python3 serve.py 20 | 21 | clean: 22 | rm -rf index.html *.json tzdata-latest.tar.gz 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | arewemeetingyet.com 2 | =================== 3 | 4 | A single-page web thingy that helps communicate (recurring) meeting times 5 | to globally distributed (and therefore timezone-challenged) participants. 6 | 7 | Requirements 8 | ------------ 9 | 10 | * A tzdata tarball (as distributed by IANA) 11 | * Python (tested with 2.7) 12 | 13 | How to install 14 | -------------- 15 | 16 | Simply run ``make``, which will retrieve the latest timezone information and 17 | build the templates. On gentoo, it will use the latest installed timezone data 18 | from ``/usr/portage/distfiles/tzdata20*``, on other systems it will download the 19 | latest timezone information from ftp.iana.org. If you are not on gentoo, you 20 | have to manually delete tzdata-latest.tar.gz and re-run ``make`` to update 21 | timezones. 22 | 23 | The JavaScript code currently assumes installation at a host root and 24 | redirection of all requests for that host to ``index.html``. On Apache, this 25 | can be achieved by employing the ``FallbackResource`` directive (consider 26 | also enabling ``AllowEncodedSlashes`` for your virtual host). 27 | 28 | To test locally, run ``make serve`` to spin up a simple http server on port 8000. 29 | -------------------------------------------------------------------------------- /create.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | if __name__ == '__main__': 4 | 5 | with open('pruned.json') as f: 6 | data = json.load(f) 7 | 8 | with open('index.tmpl') as f: 9 | tmpl = f.read() 10 | 11 | for k, v in data.items(): 12 | tmpl = tmpl.replace('{{ %s }}' % k, json.dumps(v)) 13 | 14 | print(tmpl) 15 | -------------------------------------------------------------------------------- /index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Are we meeting yet? 5 | 6 | 136 | 137 | 138 | 139 |
140 |

141 | Join this meeting 142 |

143 | Your time: 144 | 145 | 146 |

147 | 152 | 153 | 195 | 196 | 204 | 205 |
206 | 207 | 849 | 850 | 851 | -------------------------------------------------------------------------------- /prune.py: -------------------------------------------------------------------------------- 1 | import os, sys, json 2 | 3 | LITERALS = {'only', 'max'} 4 | 5 | def minimize(full, year): 6 | 7 | new = {}, {} 8 | for rule, data in full['rules'].items(): 9 | for part in data: 10 | 11 | if part[1] not in LITERALS and int(part[1]) < year: 12 | continue 13 | if part[1] == 'only' and int(part[0]) < year: 14 | continue 15 | 16 | new[1].setdefault(rule, []).append(part) 17 | 18 | for zone, data in full['zones'].items(): 19 | for part in data: 20 | 21 | if part['u'] and int(part['u'][0]) < year: 22 | continue 23 | 24 | new[0].setdefault(zone, []).append(part) 25 | if part['r'] is not None and part['r'] not in new[1]: 26 | part['r'] = None 27 | 28 | return {'zones': new[0], 'rules': new[1]} 29 | 30 | if __name__ == '__main__': 31 | 32 | with open(sys.argv[1]) as f: 33 | full = json.load(f) 34 | 35 | pruned = minimize(full, 2013) 36 | print(json.dumps(pruned)) 37 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /serve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | from http.server import SimpleHTTPRequestHandler, HTTPServer 6 | 7 | class Handler(SimpleHTTPRequestHandler): 8 | def do_GET(self): 9 | self.path = 'index.html' 10 | return SimpleHTTPRequestHandler.do_GET(self) 11 | 12 | port = sys.argv[1] if len(sys.argv) > 1 else 8000 13 | httpd = HTTPServer(('0.0.0.0', port), Handler) 14 | 15 | print ("Serving HTTP on port %d" % (port)) 16 | httpd.serve_forever() 17 | -------------------------------------------------------------------------------- /zones.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys, os, re, tarfile, json 3 | 4 | FILES = { 5 | 'africa', 'antarctica', 'asia', 'australasia', 6 | 'europe', 'northamerica', 'southamerica', 7 | } 8 | 9 | WS_SPLIT = re.compile("[ \t]+") 10 | 11 | def lines(fn): 12 | 13 | with tarfile.open(fn, 'r:*') as tar: 14 | for info in tar: 15 | 16 | if not info.isfile() or info.name not in FILES: 17 | continue 18 | 19 | f = tar.extractfile(info) 20 | for ln in f: 21 | ln = ln.decode('iso-8859-1') 22 | ln = ln.rstrip() 23 | ln = ln.split('#', 1)[0] 24 | ln = ln.rstrip(' \t') 25 | if ln: 26 | yield ln 27 | 28 | f.close() 29 | 30 | def offset(s): 31 | 32 | if s in {'-', '0'}: 33 | return 0 34 | 35 | dir, s = (-1, s[1:]) if s[0] == '-' else (1, s) 36 | words = [int(n) for n in s.split(':')] 37 | assert 1 <= len(words) < 4, words 38 | words = words + [0] * (3 - len(words)) 39 | 40 | assert 0 <= words[0] < 24, words 41 | assert 0 <= words[1] < 60, words 42 | assert 0 <= words[2] < 60, words 43 | return dir * sum((i * num) for (i, num) in zip(words, (3600, 60, 1))) 44 | 45 | def zoneline(ls): 46 | ls[1] = None if ls[1] == '-' else ls[1] 47 | tmp = offset(ls[0]), ls[1], ls[2], ls[3:] 48 | return {k: v for (k, v) in zip('orfu', tmp)} 49 | 50 | def parse(fn): 51 | 52 | zones, rules, zone = {}, {}, None 53 | for ln in lines(fn): 54 | 55 | # see zic(8) for documentation 56 | words = WS_SPLIT.split(ln) 57 | if words[0] == 'Zone': 58 | assert words[1] not in zones, words[1] 59 | zone = [] 60 | zone.append(zoneline(words[2:])) 61 | if '/' in words[1]: 62 | zones[words[1]] = zone 63 | 64 | elif words[0] == '': 65 | assert zone is not None 66 | zone.append(zoneline(words[1:])) 67 | 68 | elif words[0] == 'Rule': 69 | zone = None 70 | words[8] = offset(words[8]) 71 | rule = rules.setdefault(words[1], []) 72 | rule.append(words[2:]) 73 | 74 | elif words[0] == 'Link': 75 | zone = None # ignore 76 | else: 77 | assert False, ln 78 | 79 | return {'zones': zones, 'rules': rules} 80 | 81 | if __name__ == '__main__': 82 | 83 | path = sys.argv[1] 84 | version = re.match(r'tzdata(.*)\.tar\.gz$', os.path.basename(path)) 85 | if version is None: 86 | raise StandardError('argument must be tzdata archive') 87 | 88 | print(json.dumps(parse(path))) 89 | --------------------------------------------------------------------------------