-#!/usr/bin/env python2
-
-import web
-import os
+#!/usr/bin/python3
+from collections import namedtuple
import datetime
+import mimetypes
+import os
import re
-import smtplib
-import hashlib
import sys
from wsgiref.handlers import CGIHandler
entriesdir = os.path.join(pwd, "entries")
commentsdir = os.path.join(pwd, "comments")
templatesdir = os.path.join(pwd, "templates")
-
-web.config.debug = True
+staticsdir = os.path.join(pwd, "static")
monthnames = {"01": "January",
"02": "February",
"11": "November",
"12": "December"}
-urls = (
- '/', 'pastweek',
- '/week', 'pastweek' ,
- '/month', 'pastmonth' ,
- '/year', 'pastyear' ,
- '/feed', 'feed' ,
- '/(\d\d\d\d)/(\d\d)/(\d\d)', 'day' ,
- '/(\d\d\d\d)-(\d\d)-(\d\d)', 'day' ,
- '/(\d\d\d\d)/(\d\d)', 'month' ,
- '/(\d\d\d\d)', 'year',
- '', 'redir',
- )
-
-## This is the way to do it in web.py 0.3+
-app = web.application(urls, globals())
-
-## Workaround for autoreloading and sessions when using web.config.debug = True
-## Note: I don't actually use sessions
-#if web.config.get("_session") is None:
-# session = web.session.Session(app, web.session.DiskStore('sessions'), initializer={'count': 0})
-# web.config._session = session
-#else:
-# session = web.config._session
+STATUS_CODES = {
+ 200: "OK",
+ 302: "Found",
+ 404: "Not Found",
+ 500: "Internal Server Error",
+}
+
+class Response:
+ def __init__(self, status, headers, body):
+ self.status = status
+ self.headers = headers
+ self.body = body
+
+ def send(self, start_response):
+ status_str = STATUS_CODES[self.status]
+ resp_line = f"{self.status} {status_str}"
+ start_response(resp_line, self.headers)
+ return self.body
+
+class Route:
+ def __init__(self, path_re, fn, methods=["GET"], prefix_match=False):
+ self.path_re = re.compile(path_re)
+ self.fn = fn
+ self.methods = methods
+ self.prefix_match = prefix_match
+
+ def handle(self, match, env):
+ return self.fn(match, env)
+
+class Router:
+ def __init__(self, routes):
+ self.routes = routes
+
+ def match(self, env):
+ for route in self.routes:
+ if env["REQUEST_METHOD"] not in route.methods:
+ continue
+ if route.prefix_match:
+ m = route.path_re.match(env["PATH_INFO"])
+ else:
+ m = route.path_re.fullmatch(env["PATH_INFO"])
+ if m:
+ return (route, m)
+ return (None, None)
+
+# Helper functions
# read the paragraphs from a file
def readFile(filepath):
#timestamp = datetime.datetime.fromtimestamp(os.stat(filepath).st_mtime).isoformat()
return (nameline, emailline, websiteline, commentbody, timestamp, gravatar)
+def most_recent_entries_filelist(count):
+ files = sorted(os.listdir(entriesdir))
+ return files[-count:]
+
def getCommentFiles(date): # Returns a list of paths to comment files associated with date
filelist = [x for x in os.listdir(commentsdir) if re.match(date, x)]
return filelist
#return map( lambda x: x if "<ol>" in x or "<ul>" in x or "<pre>" in x or "<table>" in x or "<video>" in x else "<p>" + x + "</p>", paragraphs)
return [x if "<ol>" in x or "<ul>" in x or "<pre>" in x or "<table>" in x or "<video>" in x else "<p>" + x + "</p>" for x in paragraphs]
-
def getFileList(numdays=7):
files = sorted(os.listdir(entriesdir))
today = datetime.date.today()
retval = [ x for x in sorted(files) if x > then.isoformat() ]
return retval
-class year:
- def GET(self, theyear):
- files = sorted(os.listdir(entriesdir))
- filelist = [ os.path.join(entriesdir, x) for x in files if re.match(theyear,x) ]
- # It makes more sense to sort fixed chunks of time in chronological order, rather than feed-style
- #filelist.reverse()
- dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ]
- entries = [markdoku.markdown(readFile(x)) for x in filelist]
- commentcounts = map(len, map( getCommentFiles, dates))
- render = web.template.render(templatesdir)
- web.header("Content-Type","text/html; charset=utf-8")
- return render.multiday(zip(dates, entries, commentcounts), "Entries from %s" % theyear)
-
-class month:
- def GET(self, theyear, themonth):
- files = sorted(os.listdir(entriesdir))
- filelist = [ os.path.join(entriesdir, x) for x in files if re.match("%s-%s" % (theyear, themonth), x) ]
- #filelist.reverse()
- dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ]
- entries = [markdoku.markdown(readFile(x)) for x in filelist]
- commentcounts = map(len, map( getCommentFiles, dates))
- render = web.template.render(templatesdir)
- web.header("Content-Type","text/html; charset=utf-8")
- return render.multiday(zip(dates, entries, commentcounts), "Entries from %s, %s" % (monthnames[themonth], theyear))
-
-class day:
- def GET(self, year, month, day):
- filename = os.path.join(entriesdir, "%s-%s-%s.txt" % (year, month, day))
- web.header("Content-Type","text/html; charset=utf-8")
- if os.path.isfile(filename):
- entry = markdoku.markdown(readFile(filename))
- commentfilelist = os.listdir(commentsdir)
- files = sorted([os.path.join(commentsdir, filename) for filename in commentfilelist if re.match("(.*)%s-%s-%s_(\d\d\d\d\d\d).txt$" % (year, month, day), filename) ])
- comments = []
- for f in files:
- comments.append(loadComment(f))
- render = web.template.render(templatesdir)
- return render.day("%s-%s-%s" % (year, month, day), entry, comments)
- else:
- render = web.template.render(templatesdir)
- return render.noentry("%s-%s-%s" % (year, month, day))
-
- def verify(self, test):
- if len(test) != 4:
- return False
- try:
- x = int(test,10)
- except ValueError:
- return False
- return True
-
- def POST(self, year, month, day):
- i = web.input()
- bottest = i.bottest.strip()
- if not self.verify(bottest):
- web.redirect("http://127.0.0.1/go_away_bot.html")
- return
- name = i.name.strip()
- email = i.email.strip()
- wantsreply = True
- if 'wantsreply' in i:
- wantsreply = False
- website = i.website.strip()
- if website != "" and not website.startswith("http"):
- website = "http://" + website
- comment = i.comment.strip()
- timestamp = datetime.datetime.today().isoformat()
- # find a file name that isn't taken
- fname = ""
- for i in xrange(0, 1000000):
- fname = os.path.join(pwd, "pending", "%s-%s-%s_%06d.txt" % (year, month, day, i))
- if not os.path.exists(fname):
- break
- f = open(fname, "a+")
- f.write(name + "\n")
- if wantsreply:
- f.write(email + "\n")
- else:
- f.write(email + " noreply\n")
- f.write(website + "\n")
- f.write(timestamp + "\n")
- f.write(comment + "\n")
- f.close()
-
- s = smtplib.SMTP("127.0.0.1")
- s.sendmail("shortlog@zarvox.org", ["drew.m.fisher@gmail.com"], "To: Drew Fisher <drew.m.fisher@gmail.com>\nFrom: Shortlog <shortlog@zarvox.org>\nSubject: [shortlog] new comment on %s-%s-%s\n\nName: %s\nEmail: %s\nWebsite: %s\nComment: %s" % (year, month, day, name, email, website, comment ))
- s.quit()
- web.header('Content-Type', 'text/html')
- return "<html><body>Thanks! Your comment has been submitted and will be posted pending review.</body></html>"
-
-class pastyear:
- def GET(self):
- filelist = [ os.path.join(entriesdir, x) for x in getFileList(365)]
- filelist.reverse()
- dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ]
- entries = [markdoku.markdown(readFile(x)) for x in filelist]
- commentcounts = map(len, map( getCommentFiles, dates))
- render = web.template.render(templatesdir)
- web.header("Content-Type","text/html; charset=utf-8")
- return render.multiday(zip(dates, entries, commentcounts))
-
-class pastmonth:
- def GET(self):
- filelist = [ os.path.join(entriesdir, x) for x in getFileList(30)]
- filelist.reverse()
- dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ]
- entries = [markdoku.markdown(readFile(x)) for x in filelist]
- commentcounts = map(len, map( getCommentFiles, dates))
- render = web.template.render(templatesdir)
- web.header("Content-Type","text/html; charset=utf-8")
- return render.multiday(zip(dates, entries, commentcounts))
-
-class pastweek:
- def GET(self):
- filelist = [ os.path.join(entriesdir, x) for x in getFileList(7)]
- filelist.reverse()
- dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ]
- entries = [markdoku.markdown(readFile(x)) for x in filelist]
- commentcounts = map(len, map( getCommentFiles, dates))
- render = web.template.render(templatesdir)
- web.header("Content-Type","text/html; charset=utf-8")
- return render.multiday(zip(dates, entries, commentcounts))
-
-class redir:
- def GET(self):
- web.redirect("/")
-
-class feed:
- def GET(self):
- web.header('Content-Type', 'application/atom+xml')
- filelist = os.listdir(entriesdir)
- files = [os.path.join(entriesdir, f) for f in filelist if re.match("(.*)(\d\d\d\d-\d\d-\d\d).txt$", f)]
- stats = [os.stat(f) for f in files]
- decorated = [(stat.st_mtime, filename) for (filename, stat) in zip(files, stats)]
- decorated.sort(reverse=True)
- if len(decorated) == 0:
- return render.feed(datetime.datetime.today(), [] )
+# Templates
+
+def render_no_entry(date):
+ return f"""<!DOCTYPE HTML>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
+<title>Shortlog - a log of everyday things</title>
+</head>
+<body>
+<h1>Shortlog - a log of everyday things</h1>
+<h3><a href="/shortlog/">Home</a></h3>
+<p>There's no entry for {date}. Perhaps you reached this page by mistake?</p>
+</body>
+</html>
+"""
+
+def render_comments(comments):
+ rendered_comments = []
+ for comment in comments:
+ name = comment[0]
+ email = comment[1]
+ website = comment[2]
+ body = comment[3]
+ timestamp = comment[4]
+ gravatar = comment[5]
+ writer_line = None
+ name_block = f'<a href="{website}">{name}</a>' if website else name
+ rendered_paras = "\n".join(body)
+ rendered_comment = "\n".join([
+ '<div class="comment">',
+ f'<p><span style="float: right;"><img src="{gravatar}" alt="avatar from Gravatar"></span>',
+ f'<p>{name_block} | {timestamp}</p',
+ rendered_paras
+ ])
+ rendered_comments.append(rendered_comment)
+ else:
+ return ""
+
+ return "\n".join([
+ "<hr><h3>Comments:</h3>",
+ "\n".join(rendered_comments),
+ ])
+
+def render_page(content):
+ return f"""<!DOCTYPE HTML>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
+<title>Shortlog - a log of everyday things</title>
+<link rel="stylesheet" type="text/css" href="/static/style.css">
+<link rel="alternate" type="application/atom+xml" href="/shortlog/feed">
+</head>
+<body>
+{content}
+</body>
+</html>
+"""
+
+def render_multiday(entry_tuples, note):
+ entries = []
+ for entry in entry_tuples:
+ (date, entrytext, commentcount) = entry
+ commentcount_suffix = "s" if commentcount != 1 else ""
+ commentcount_label = f"{commentcount} comment{commentcount_suffix}"
+ entries.append("\n".join([
+ "<div>",
+ f'<h3><a href="/shortlog/{date}">{date}</a> | <a href="/shortlog/{date}#comments">{commentcount_label}</a></h3>',
+ entrytext,
+ ]))
+ all_entry_text = "\n".join(entries)
+ content = f"""<h1>Shortlog - a log of everyday things</h1>
+<h3>View the past: <a href="/shortlog/2013">2013</a> <a href="/shortlog/2012">2012</a> <a href="/shortlog/2011">2011</a> <a href="/shortlog/2010">2010</a> </h3>
+<h4><a href="/shortlog/feed">Atom Feed</a></h4>
+{note}
+{all_entry_text}
+"""
+ return render_page(content)
+
+def render_day(date, entry, comments):
+ comment_markup = render_comments(comments)
+ content = f"""<h1>Shortlog - a log of everyday things</h1>
+<h3><a href="/shortlog/">Home</a></h3>
+<div>
+ <h3><a href="/shortlog/{date}">{date}</a></h3>
+ {entry}
+</div>
+<div id="comments">
+{comment_markup}
+</div>"""
+ return render_page(content)
+
+def render_feed(timestamp, entries):
+ rendered_entries = []
+ for entry in entries:
+ rendered_entries.append(f""" <entry>
+ <title>{entry["date"]}</title>
+ <link href="https://zarvox.org/shortlog/{entry["date"]}" rel="alternate"></link>
+ <id>https://zarvox.org/shortlog/{entry["date"]}</id>
+ <updated>{entry["timestamp"]}</updated>
+ <summary type="html">{entry["content"]}</summary>
+ </entry>""")
+ entries_text = "\n".join(rendered_entries)
+ content = f"""<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
+ <title>Shortlog</title>
+ <subtitle>A log of everyday things</subtitle>
+ <link href="https://zarvox.org/shortlog/feed rel="self"></link>
+ <link href="https://zarvox.org/shortlog/" rel="alternate"></link>
+ <id>https://zarvox.org/shortlog</id>
+ <updated>{timestamp}</updated>
+ <author>
+ <name>Drew Fisher</name>
+ <email>zarvox@zarvox.org</email>
+ </author>
+{entries_text}
+</feed>
+"""
+ return content
+
+# Route definitions
+
+def serve_static(match, env):
+ requested_filename = env["PATH_INFO"][len("/static/"):]
+ path = os.path.normpath(os.path.join(staticsdir, requested_filename))
+ if os.path.commonprefix([path, staticsdir]) == staticsdir:
+ content_type = mimetypes.guess_type(path)
+ with open(path, "rb") as f:
+ content = f.read()
+ return Response(200, [("Content-Type", content_type[0])], [content])
+ return Response(404, [("Content-Type", "text/html")],
+ [b"<h1>404 Asset " + requested_filename.encode('utf-8') + b" Not Found :(</h1>"])
+
+def serve_test_page(match, env):
+ return Response(200, [("Content-Type", "text/html")],
+ [b"<h1>It works!</h1>"])
+
+def serve_year(match, env):
+ (year,) = match.groups()
+ files = sorted(os.listdir(entriesdir))
+ filelist = [ os.path.join(entriesdir, x) for x in files if re.match(year,x) ]
+ # It makes more sense to sort fixed chunks of time in chronological order, rather than feed-style
+ #filelist.reverse()
+ dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ]
+ entries = [markdoku.markdown(readFile(x)) for x in filelist]
+ commentcounts = map(len, map( getCommentFiles, dates))
+ note = f"Entries from {year}"
+ body = render_multiday(zip(dates, entries, commentcounts), note)
+ return Response(200, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')])
+
+def serve_month(match, env):
+ (year, month) = match.groups()
+ files = sorted(os.listdir(entriesdir))
+ filelist = [ os.path.join(entriesdir, x) for x in files if re.match(f"{year}-{month}", x) ]
+ #filelist.reverse()
+ dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ]
+ entries = [markdoku.markdown(readFile(x)) for x in filelist]
+ commentcounts = map(len, map( getCommentFiles, dates))
+ note = f"Entries from {monthnames[month]}, {year}"
+ body = render_multiday(zip(dates, entries, commentcounts), note)
+ return Response(200, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')])
+
+def serve_day(match, env):
+ (year, month, day) = match.groups()
+ datestr = f"{year}-{month}-{day}"
+ filename = os.path.join(entriesdir, f"{datestr}.txt")
+ if os.path.isfile(filename):
+ entry = markdoku.markdown(readFile(filename))
+ commentfilelist = os.listdir(commentsdir)
+ files = sorted([os.path.join(commentsdir, filename) for filename in commentfilelist if re.match("(.*)%s-%s-%s_(\d\d\d\d\d\d).txt$" % (year, month, day), filename) ])
+ comments = []
+ for f in files:
+ comments.append(loadComment(f))
+ body = render_day(datestr, entry, comments)
+ return Response(200, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')])
+ else:
+ body = render_no_entry(datestr)
+ return Response(404, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')])
+
+def serve_feed(match, env):
+ filelist = os.listdir(entriesdir)
+ files = [os.path.join(entriesdir, f) for f in filelist if re.match("(.*)(\d\d\d\d-\d\d-\d\d).txt$", f)]
+ stats = [os.stat(f) for f in files]
+ decorated = [(stat.st_mtime, filename) for (filename, stat) in zip(files, stats)]
+ decorated.sort(reverse=True)
+ if len(decorated) == 0:
+ body = render_feed(datetime.datetime.today(), [])
+ else:
group_timestamp = datetime.datetime.fromtimestamp(decorated[0][0]).isoformat() + "-06:00"
entries = []
for d in decorated[:10]:
"content": markdoku.markdown(readFile(d[1])),
"timestamp": (mtime.isoformat() + "-06:00") }
entries.append(second)
- render = web.template.render(templatesdir)
- return render.feed(group_timestamp, entries)
+ body = render_feed(group_timestamp, entries)
+ return Response(200, [('Content-Type', 'application/atom+xml')],
+ [body.encode('utf-8')])
+
+def serve_most_recent(match, env):
+ filelist = [ os.path.join(entriesdir, x) for x in most_recent_entries_filelist(20) ]
+ filelist.reverse()
+ dates = [ re.sub("(.*)(\d\d\d\d-\d\d-\d\d).txt", "\g<2>", filename) for filename in filelist ]
+ print(dates)
+ entries = [markdoku.markdown(readFile(x)) for x in filelist]
+ commentcounts = map(len, map( getCommentFiles, dates))
+ note = ""
+ body = render_multiday(zip(dates, entries, commentcounts), note)
+ return Response(200, [('Content-Type', 'text/html; charset=utf-8')], [body.encode('utf-8')])
+
+router = Router([
+ Route("/static/", serve_static, prefix_match=True),
+ Route("/(\d\d\d\d)-(\d\d)-(\d\d)", serve_day),
+ Route("/(\d\d\d\d)/(\d\d)/(\d\d)", serve_day),
+ Route("/(\d\d\d\d)-(\d\d)", serve_month),
+ Route("/(\d\d\d\d)/(\d\d)", serve_month),
+ Route("/(\d\d\d\d)", serve_year),
+ Route("/feed", serve_feed),
+ Route("/", serve_most_recent),
+])
+
+#def redir(match, env):
+# return Response(302, [("Location", "/")], [])
+
+#urls = (
+# '', 'redir',
+# '/week', 'pastweek' ,
+# '/month', 'pastmonth' ,
+# '/year', 'pastyear' ,
+# '/feed', 'feed' ,
+# '/', 'pastweek',
+# )
+
+def application(env, start_response):
+ (route, match) = router.match(env)
+
+ response = None
+ if route:
+ try:
+ response = route.handle(match, env)
+ except e:
+ print("EXCEPTION:", e)
+ pass
+ else:
+ print("No route matched", repr(env["PATH_INFO"]))
+ response = Response(404, [("Content-Type", "text/html")],
+ [b"<h1>404 Not Found :(</h1>"])
+
+
+ if response is None:
+ response = Response(500, [("Content-Type", "text/html")],
+ [b"<h1>Internal Server Error</h1>"])
-# web.py 0.3+ way
-application = web.application(urls, globals()).wsgifunc()
+ print(env["REQUEST_METHOD"], env["PATH_INFO"], response.status)
+ return response.send(start_response)
-# web.py 0.2 way
-#application = web.wsgifunc(web.webpyfunc(urls, globals()))
if __name__ == "__main__":
# CGI interface
CGIHandler().run(application)
- ## The web.py 0.3+ way
- #app.run()
- ## The web.py 0.2 way
- #web.run(urls, globals())
+ # cherrypy WSGI container
+ #import cherrypy
+ #cherrypy.tree.graft(application, "/")
+ #cherrypy.server.unsubscribe()
+ #server = cherrypy._cpserver.Server()
+ #server.socket_host = "127.0.0.1"
+ #server.socket_port= 5000
+ #server.thread_pool = 4
+ #server.subscribe()
+ #cherrypy.engine.start()
+ #cherrypy.engine.block()