├── woodwind ├── __init__.py ├── wsgi.py ├── static │ ├── logo.png │ ├── syndication-icons │ │ └── twitter.com.png │ ├── subscriptions.js │ ├── normalize.css.map │ ├── webaction.js │ ├── indieconfig.js │ ├── style.css.map │ ├── style.scss │ ├── normalize.css │ ├── normalize.scss │ └── style.css ├── templates │ ├── offline.jinja2 │ ├── subscribe.jinja2 │ ├── subscriptions_opml.xml │ ├── select-feed.jinja2 │ ├── settings_indie_config.jinja2 │ ├── settings_micropub.jinja2 │ ├── settings_action_urls.jinja2 │ ├── settings.jinja2 │ ├── feed.jinja2 │ ├── _reply.jinja2 │ ├── subscriptions.jinja2 │ ├── _entry.jinja2 │ └── base.jinja2 ├── __main__.py ├── extensions.py ├── websocket_server.py ├── sse_server.py ├── app.py ├── api.py ├── util.py ├── push.py ├── models.py ├── tasks.py └── views.py ├── timers.py ├── config.py.template ├── run.py ├── init_db.py ├── .gitignore ├── log.sh ├── vacuum.sql ├── woodwind-dev.ini ├── woodwind-sock.ini ├── frontend ├── manifest.json ├── package.json ├── sw.js ├── webaction.js ├── indieconfig.js └── feed.js ├── setup.py ├── deploy.sh ├── woodwind.ini ├── woodwind.cfg.template ├── NOTES.md ├── scripts ├── 20150318-clean-content.py └── 2015-03-26-normalize-urls.py ├── requirements.txt ├── fabfile.py ├── README.md └── LICENSE /woodwind/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import create_app 2 | 3 | __all__ = ['create_app'] 4 | -------------------------------------------------------------------------------- /woodwind/wsgi.py: -------------------------------------------------------------------------------- 1 | from . import create_app 2 | 3 | application = create_app('../woodwind.cfg') 4 | -------------------------------------------------------------------------------- /woodwind/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karadaisy/woodwind/HEAD/woodwind/static/logo.png -------------------------------------------------------------------------------- /woodwind/static/syndication-icons/twitter.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karadaisy/woodwind/HEAD/woodwind/static/syndication-icons/twitter.com.png -------------------------------------------------------------------------------- /timers.py: -------------------------------------------------------------------------------- 1 | from uwsgidecorators import timer 2 | from woodwind import tasks 3 | 4 | 5 | @timer(300) 6 | def tick(signum=None): 7 | tasks.q.enqueue(tasks.tick) 8 | -------------------------------------------------------------------------------- /config.py.template: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config: 5 | SECRET_KEY = 'super secret key' 6 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.getcwd() + '/db.sqlite' 7 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | def main(): 5 | from woodwind.app import create_app 6 | app = create_app() 7 | app.run(debug=True, port=4000) 8 | 9 | 10 | main() 11 | -------------------------------------------------------------------------------- /woodwind/templates/offline.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | 3 | {% block login %}{% endblock login %} 4 | 5 | {% block body %} 6 | 7 | Offline, and it feels so good 8 | 9 | {% endblock body %} -------------------------------------------------------------------------------- /init_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from woodwind import create_app 4 | from woodwind.extensions import db 5 | 6 | app = create_app() 7 | 8 | with app.app_context(): 9 | db.create_all() 10 | -------------------------------------------------------------------------------- /woodwind/__main__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['main'] 2 | 3 | 4 | def main(): 5 | from woodwind.app import create_app 6 | app = create_app() 7 | app.run(debug=True, port=4000) 8 | 9 | 10 | main() 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | *~ 4 | .sass-cache 5 | .sass-cacheconfig.py 6 | __pycache__ 7 | celerybeat-schedule 8 | celerybeat-schedule* 9 | config.py 10 | venv 11 | woodwind.cfg 12 | localcert 13 | -------------------------------------------------------------------------------- /log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | REMOTE_USER=kmahan 4 | HOST=orin.kylewm.com 5 | 6 | ssh -t $REMOTE_USER@$HOST bash -c "' 7 | 8 | set -x 9 | 10 | sudo tail -n 60 -f /var/log/upstart/woodwind.log 11 | '" 12 | -------------------------------------------------------------------------------- /vacuum.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM entry 2 | USING ( 3 | SELECT 4 | id, 5 | ROW_NUMBER() OVER (PARTITION BY feed_id ORDER BY retrieved DESC) AS row 6 | FROM entry 7 | ) AS numbered 8 | WHERE entry.id = numbered.id 9 | AND (row > 2000 OR retrieved < CURRENT_DATE - INTERVAL '365 days'); 10 | -------------------------------------------------------------------------------- /woodwind-dev.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | master=true 3 | processes=1 4 | #socket=/tmp/woodwind.sock 5 | #chmod-socket=666 6 | http-socket=:3000 7 | module=woodwind.wsgi 8 | import=timers 9 | attach-daemon=rqworker high low 10 | attach-daemon=python -m woodwind.websocket_server 11 | py-autoreload=3 12 | -------------------------------------------------------------------------------- /woodwind-sock.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | master=true 3 | processes=1 4 | socket=/tmp/woodwind.sock 5 | chmod-socket=666 6 | #http-socket=:3000 7 | module=woodwind.wsgi 8 | import=timers 9 | attach-daemon=rqworker high low 10 | attach-daemon=python -m woodwind.websocket_server 11 | py-autoreload=3 12 | -------------------------------------------------------------------------------- /woodwind/templates/subscribe.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | {% block body %} 3 |
7 | {% endblock body %} 8 | -------------------------------------------------------------------------------- /woodwind/templates/subscriptions_opml.xml: -------------------------------------------------------------------------------- 1 | 2 |7 | Each post will have Like, Repost, and Reply buttons that will post content to your site directly via micropub. See Micropub for details. 8 |
9 |10 | Configure micropub credentials. 11 |
12 | {% if current_user.micropub_endpoint or current_user.access_token %} 13 | 14 | 15 |16 | Update Syndication Targets 17 |
18 |19 | Reauthorize Micropub 20 |
21 |22 | Revoke Credentials 23 |
24 | {% else %} 25 |26 | Authorize Micropub 27 |
28 | {% endif %} 29 |
51 | {% if entry.get_property('start') %}
52 | start: {{ entry.get_property('start') }}
53 | {% endif %}
54 |
55 | {% if entry.get_property('end') %}
56 | end: {{ entry.get_property('end') }}
57 | {% endif %}
58 |
75 | {% if of == "like" %} 76 | Liked: 77 | {% else %} 78 | {{ of | title }}ed: 79 | {% endif %} 80 | {{ property }} 81 |
82 | {% endfor %} 83 |
27 | Woodwind
28 |
29 |
44 |
49 | Woodwind
50 | '
703 | ''
704 | ).format(tweet_url)
705 |
706 | return content
707 |
708 |
709 | @views.app_template_filter()
710 | def proxy_image(url):
711 | proxy_url = flask.current_app.config.get('IMAGEPROXY_URL')
712 | proxy_key = flask.current_app.config.get('IMAGEPROXY_KEY')
713 | if proxy_url and proxy_key:
714 | sig = base64.urlsafe_b64encode(
715 | hmac.new(proxy_key.encode(), url.encode(), hashlib.sha256).digest()
716 | ).decode()
717 | return '/'.join((proxy_url.rstrip('/'), 's' + sig, url))
718 |
719 | pilbox_url = flask.current_app.config.get('PILBOX_URL')
720 | pilbox_key = flask.current_app.config.get('PILBOX_KEY')
721 | if pilbox_url and pilbox_key:
722 | query = urllib.parse.urlencode({'url': url, 'op': 'noop'})
723 | sig = hmac.new(pilbox_key.encode(), query.encode(), hashlib.sha1).hexdigest()
724 | query += '&sig=' + sig
725 | return pilbox_url + '?' + query
726 |
727 | camo_url = flask.current_app.config.get('CAMO_URL')
728 | camo_key = flask.current_app.config.get('CAMO_KEY')
729 | if camo_url and camo_key:
730 | digest = hmac.new(camo_key.encode(), url.encode(), hashlib.sha1).hexdigest()
731 | return (urllib.parse.urljoin(camo_url, digest)
732 | + '?url=' + urllib.parse.quote_plus(url))
733 | return url
734 |
735 |
736 | @views.app_template_filter()
737 | def proxy_all(content):
738 | def repl(m):
739 | attrs = m.group(1)
740 | url = m.group(2)
741 | url = url.replace('&', '&')
742 | return '