├── .gitignore
├── README.md
├── config.py
├── config.py.sample
├── micromemories
├── __init__.py
├── app.py
├── controllers
│ ├── __init__.py
│ └── root.py
├── fetch.py
├── model
│ └── __init__.py
├── templates
│ ├── error.html
│ ├── index.html
│ └── layout.html
└── wsgi.py
├── setup.py
└── zappa_settings.json.sample
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | __pycache__
3 | *.egg-info
4 | conf.py
5 | zappa_settings.json
6 | venv
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Micro Memories
2 | ==============
3 |
4 | Code used for creating an "On This Day" page for a Micro.blog hosted website.
5 |
6 | Currently used on [my wife's site](http://cleverangel.org/on-this-day).
7 |
8 | To use, simply create a page on your Micro.blog website with the following
9 | content:
10 |
11 | ```html
12 |
13 | Loading...
14 |
15 |
16 |
17 | ```
18 |
19 | This will inject some JavaScript into the page, which will then discover and
20 | crawl your `/archive` page, and populate the content for you.
21 |
22 | Make sure to pass the appropriate time zone. If none is specified in the request
23 | for the JavaScript, then 'US/Pacific' will be assumed. For a full listing of
24 | available time zone strings, refer to [the IANA time zone
25 | database](https://www.iana.org/time-zones).
26 |
27 | Requirements
28 | ------------
29 |
30 | Micro Memories is known to work on all of the standard themes in Micro.blog. If
31 | you are using a custom theme, you need to ensure that your theme makes proper
32 | use of [microformats](http://microformats.org/wiki/microformats2), especially
33 | the [h-entry](http://microformats.org/wiki/microformats2#h-entry) microformat.
34 | The [open source Micro.blog themes](https://github.com/microdotblog) are a good
35 | place to look for guidance.
36 |
37 | To ensure a good experience, your posts should be marked up with the `h-entry`
38 | microformat, with a `u-url` property, a `p-name` property, a `dt-published`
39 | property, and a `e-content` property. To check how your posts parse with a
40 | microformats2 parser, you can use the tool on
41 | [microformats.io](https://microformats.io) to verify that all properties are
42 | being discovered.
43 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | # Server Specific Configurations
2 | server = {
3 | 'port': '8080',
4 | 'host': '127.0.0.1'
5 | }
6 |
7 | # Pecan Application Configurations
8 | app = {
9 | 'root': 'micromemories.controllers.root.RootController',
10 | 'modules': ['micromemories'],
11 | 'template_path': '%(confdir)s/micromemories/templates',
12 | 'debug': True,
13 | 'errors': {
14 | 404: '/error/404',
15 | '__force_dict__': True
16 | }
17 | }
18 |
19 | logging = {
20 | 'root': {'level': 'INFO', 'handlers': ['console']},
21 | 'loggers': {
22 | 'micromemories': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False},
23 | 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False},
24 | 'py.warnings': {'handlers': ['console']},
25 | '__force_dict__': True
26 | },
27 | 'handlers': {
28 | 'console': {
29 | 'level': 'DEBUG',
30 | 'class': 'logging.StreamHandler',
31 | 'formatter': 'color'
32 | }
33 | },
34 | 'formatters': {
35 | 'simple': {
36 | 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
37 | '[%(threadName)s] %(message)s')
38 | },
39 | 'color': {
40 | '()': 'pecan.log.ColorFormatter',
41 | 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]'
42 | '[%(threadName)s] %(message)s'),
43 | '__force_dict__': True
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/config.py.sample:
--------------------------------------------------------------------------------
1 | # Server Specific Configurations
2 | server = {
3 | 'port': '8080',
4 | 'host': '0.0.0.0'
5 | }
6 |
7 | # Pecan Application Configurations
8 | app = {
9 | 'root': 'micromemories.controllers.root.RootController',
10 | 'modules': ['micromemories'],
11 | 'template_path': '%(confdir)s/micromemories/templates',
12 | 'debug': True,
13 | 'errors': {
14 | 404: '/error/404',
15 | '__force_dict__': True
16 | }
17 | }
18 |
19 | logging = {
20 | 'root': {'level': 'INFO', 'handlers': ['console']},
21 | 'loggers': {
22 | 'micromemories': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False},
23 | 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False},
24 | 'py.warnings': {'handlers': ['console']},
25 | '__force_dict__': True
26 | },
27 | 'handlers': {
28 | 'console': {
29 | 'level': 'DEBUG',
30 | 'class': 'logging.StreamHandler',
31 | 'formatter': 'color'
32 | }
33 | },
34 | 'formatters': {
35 | 'simple': {
36 | 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
37 | '[%(threadName)s] %(message)s')
38 | },
39 | 'color': {
40 | '()': 'pecan.log.ColorFormatter',
41 | 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]'
42 | '[%(threadName)s] %(message)s'),
43 | '__force_dict__': True
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/micromemories/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cleverdevil/micromemories/1fa7045f51e54ca6375f0bf3f48c0ec79602107d/micromemories/__init__.py
--------------------------------------------------------------------------------
/micromemories/app.py:
--------------------------------------------------------------------------------
1 | from pecan import make_app
2 |
3 |
4 | def setup_app(config):
5 |
6 | app_conf = dict(config.app)
7 |
8 | return make_app(
9 | app_conf.pop('root'),
10 | logging=getattr(config, 'logging', {}),
11 | **app_conf
12 | )
13 |
--------------------------------------------------------------------------------
/micromemories/controllers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cleverdevil/micromemories/1fa7045f51e54ca6375f0bf3f48c0ec79602107d/micromemories/controllers/__init__.py
--------------------------------------------------------------------------------
/micromemories/controllers/root.py:
--------------------------------------------------------------------------------
1 | from pecan import expose, redirect, request
2 | from pecan.hooks import PecanHook, HookController
3 | from webob.exc import status_map
4 | from datetime import datetime
5 | from urllib.parse import urlparse
6 |
7 | from .. import fetch
8 |
9 | import time
10 | import pytz
11 | import requests
12 |
13 |
14 | JAVASCRIPT = '''var container = document.getElementById('on-this-day');
15 |
16 | function renderPost(post) {
17 | var postEl = document.createElement('div');
18 | postEl.className = 'post';
19 | container.appendChild(postEl);
20 |
21 | if (post['properties']['name'] != null) {
22 | var titleEl = document.createElement('h2');
23 | titleEl.className = 'p-name';
24 | titleEl.innerText = post['properties']['name'][0];
25 | postEl.appendChild(titleEl);
26 | }
27 |
28 | var permalinkEl = document.createElement('a');
29 | permalinkEl.className = 'post-date u-url';
30 | permalinkEl.href = post['properties']['url'][0];
31 | postEl.appendChild(permalinkEl);
32 |
33 | var publishedEl = document.createElement('time');
34 | publishedEl.className = 'dt-published';
35 | publishedEl.datetime = post['properties']['published'][0];
36 |
37 | var published = post['properties']['published'][0];
38 | published = new Date(published.slice(0,19).replace(' ', 'T'));
39 |
40 | publishedEl.innerText = published.toDateString();
41 | permalinkEl.appendChild(publishedEl);
42 |
43 | var contentEl = document.createElement('div');
44 | contentEl.className = 'e-content';
45 | contentEl.innerHTML = post['properties']['content'][0]['html'];
46 | postEl.appendChild(contentEl);
47 | }
48 |
49 | function renderNoContent() {
50 | var noPostsEl = document.createElement('p');
51 | noPostsEl.innerText = 'No posts found for this day. Check back tomorrow!';
52 | container.appendChild(noPostsEl);
53 | }
54 |
55 | var xhr = new XMLHttpRequest();
56 | xhr.responseType = "json";
57 | xhr.open('GET', "https://micromemories.cleverdevil.io/posts?tz=%(timezone)s", true);
58 | xhr.send();
59 |
60 | xhr.onreadystatechange = function(e) {
61 | if (xhr.readyState == 4 && xhr.status == 200) {
62 | container.innerHTML = '';
63 | if (xhr.response.length == 0) {
64 | renderNoContent();
65 | } else {
66 | xhr.response.forEach(function(post) {
67 | renderPost(post);
68 | });
69 | }
70 | }
71 | }'''
72 |
73 |
74 | class CorsHook(PecanHook):
75 |
76 | def after(self, state):
77 | state.response.headers['Access-Control-Allow-Origin'] = '*'
78 | state.response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
79 | state.response.headers['Access-Control-Allow-Headers'] = 'origin, referer, authorization, accept'
80 |
81 |
82 |
83 | class RootController(HookController):
84 |
85 | __hooks__ = [CorsHook()]
86 |
87 | @expose(template='index.html')
88 | def index(self):
89 | return dict()
90 |
91 | @expose('json')
92 | def posts(self, month=None, day=None, tz='US/Pacific', url=None):
93 | if not month:
94 | today = pytz.utc.localize(datetime.utcnow())
95 | today = today.astimezone(pytz.timezone(tz))
96 | month = today.month
97 | day = today.day
98 |
99 | if url is None:
100 | referer = request.headers.get('Referer')
101 | if not referer:
102 | return []
103 |
104 | referer = urlparse(referer)
105 | url = '%s://%s/archive/index.json' % (
106 | referer.scheme,
107 | referer.netloc
108 | )
109 |
110 | print('Fetching ->', url)
111 | response = requests.get(url)
112 | if response.status_code == 404:
113 | return []
114 | else:
115 | print('Fetching ->', url)
116 | response = requests.get(url)
117 |
118 | response.encoding = 'utf-8'
119 | content = response.text
120 |
121 | print('Got content, now checking headers')
122 |
123 | if response.headers['Content-Type'] != 'application/json':
124 | print('Response not JSON, returning []')
125 | return []
126 |
127 | print('Fetching items!')
128 |
129 | items = fetch.items_for(
130 | content=content,
131 | month=int(month),
132 | day=int(day),
133 | full_content=True
134 | )
135 |
136 | return items
137 |
138 | @expose(content_type='application/javascript')
139 | def js(self, tz='US/Pacific'):
140 | return JAVASCRIPT % {'timezone': tz}
141 |
142 | @expose('error.html')
143 | def error(self, status):
144 | try:
145 | status = int(status)
146 | except ValueError: # pragma: no cover
147 | status = 500
148 | message = getattr(status_map.get(status), 'explanation', '')
149 | return dict(status=status, message=message)
150 |
--------------------------------------------------------------------------------
/micromemories/fetch.py:
--------------------------------------------------------------------------------
1 | from concurrent import futures
2 |
3 | import json
4 | import mf2py
5 | import re
6 |
7 |
8 | def handle_child(child):
9 | full_child = mf2py.parse(url=child['url'], html_parser='lxml')
10 |
11 | result = [
12 | item for item in full_child['items']
13 | if item['type'][0] == 'h-entry'
14 | ]
15 |
16 | if len(result):
17 | result = result[0]
18 | result['properties']['url'] = [child['url']]
19 | return result
20 |
21 | return None
22 |
23 |
24 | def items_for(content, month=1, day=1, full_content=False):
25 | feed = json.loads(content)
26 | matching_date = re.compile(r'\d\d\d\d-%.2d-%.2d.*' % (month, day))
27 |
28 | results = []
29 | for child in feed.get('items', []):
30 | if matching_date.match(child['date_published']):
31 | results.append(child)
32 |
33 | # if we are to fetch full content, handle that in multiple
34 | # threads to speed things up
35 | if full_content:
36 | with futures.ThreadPoolExecutor() as executor:
37 | full_results = executor.map(handle_child, results)
38 |
39 | results = [
40 | result for result in full_results
41 | if result is not None
42 | ]
43 |
44 | return results
45 |
--------------------------------------------------------------------------------
/micromemories/model/__init__.py:
--------------------------------------------------------------------------------
1 | from pecan import conf # noqa
2 |
3 |
4 | def init_model():
5 | """
6 | This is a stub method which is called at application startup time.
7 |
8 | If you need to bind to a parsed database configuration, set up tables or
9 | ORM classes, or perform any database initialization, this is the
10 | recommended place to do it.
11 |
12 | For more information working with databases, and some common recipes,
13 | see https://pecan.readthedocs.io/en/latest/databases.html
14 | """
15 | pass
16 |
--------------------------------------------------------------------------------
/micromemories/templates/error.html:
--------------------------------------------------------------------------------
1 | <%inherit file="layout.html" />
2 |
3 | ## provide definitions for blocks we want to redefine
4 | <%def name="title()">
5 | Server Error ${status}
6 | %def>
7 |
8 | ## now define the body of the template
9 |
10 | Server Error ${status}
11 |
12 | ${message}
13 |
--------------------------------------------------------------------------------
/micromemories/templates/index.html:
--------------------------------------------------------------------------------
1 | <%inherit file="layout.html" />
2 |
3 | ## provide definitions for blocks we want to redefine
4 | <%def name="title()">
5 | Micro Memories
6 | %def>
7 |
8 | ## now define the body of the template
9 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/micromemories/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${self.title()}
4 |
5 |
6 | ${self.body()}
7 |
8 |
9 |
10 | <%def name="title()">
11 | Default Title
12 | %def>
13 |
--------------------------------------------------------------------------------
/micromemories/wsgi.py:
--------------------------------------------------------------------------------
1 | from pecan.deploy import deploy
2 | app = deploy('config.py')
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | try:
3 | from setuptools import setup, find_packages
4 | except ImportError:
5 | from ez_setup import use_setuptools
6 | use_setuptools()
7 | from setuptools import setup, find_packages
8 |
9 | setup(
10 | name='micromemories',
11 | version='0.1',
12 | description='',
13 | author='',
14 | author_email='',
15 | install_requires=[
16 | "pecan",
17 | "mf2py",
18 | "zappa",
19 | "pytz"
20 | ],
21 | test_suite='micromemories',
22 | zip_safe=False,
23 | include_package_data=True,
24 | packages=find_packages(exclude=['ez_setup'])
25 | )
26 |
--------------------------------------------------------------------------------
/zappa_settings.json.sample:
--------------------------------------------------------------------------------
1 | {
2 | "dev": {
3 | "app_function": "micromemories.wsgi.app",
4 | "aws_region": "us-east-1",
5 | "profile_name": "default",
6 | "project_name": "micromemories",
7 | "runtime": "python3.6",
8 | "s3_bucket": "micromemories-zappa-dev-sample",
9 | "keep_warm": false
10 | }
11 | }
12 |
--------------------------------------------------------------------------------