├── .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 | 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 | 7 | 8 | ## now define the body of the template 9 |
10 |
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 | 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 | --------------------------------------------------------------------------------