├── app ├── base │ ├── __init__.py │ ├── constants.py │ └── util.py ├── django_settings.py ├── birdfeeder │ ├── __init__.py │ └── handlers │ │ ├── __init__.py │ │ ├── pingersecret.py.template │ │ ├── main.py │ │ ├── tools.py │ │ └── pinger.py ├── datasources │ ├── __init__.py │ ├── oauth2 │ │ ├── clients │ │ │ ├── __init__.py │ │ │ ├── imap.py │ │ │ └── smtp.py │ │ └── _version.py │ ├── test_oauth_access_token.py │ └── get_oauth_access_token.py ├── feedplayback │ └── __init__.py ├── mastofeeder │ ├── __init__.py │ └── handlers │ │ ├── __init__.py │ │ └── main.py ├── tweetdigest │ └── __init__.py ├── third_party │ ├── urllib3 │ │ ├── contrib │ │ │ ├── __init__.py │ │ │ ├── _securetransport │ │ │ │ └── __init__.py │ │ │ └── _appengine_environ.py │ │ ├── packages │ │ │ ├── __init__.py │ │ │ └── backports │ │ │ │ ├── __init__.py │ │ │ │ └── makefile.py │ │ ├── _version.py │ │ ├── util │ │ │ ├── queue.py │ │ │ ├── __init__.py │ │ │ └── proxy.py │ │ └── filepost.py │ ├── chardet │ │ ├── cli │ │ │ ├── __init__.py │ │ │ └── chardetect.py │ │ ├── metadata │ │ │ └── __init__.py │ │ ├── version.py │ │ ├── compat.py │ │ ├── euctwprober.py │ │ ├── euckrprober.py │ │ ├── gb2312prober.py │ │ ├── big5prober.py │ │ ├── enums.py │ │ ├── cp949prober.py │ │ ├── mbcsgroupprober.py │ │ └── utf8prober.py │ ├── idna-2.10.dist-info │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── WHEEL │ │ ├── RECORD │ │ └── LICENSE.rst │ ├── requests_toolbelt │ │ ├── auth │ │ │ ├── __init__.py │ │ │ └── _digest_auth_compat.py │ │ ├── cookies │ │ │ ├── __init__.py │ │ │ └── forgetful.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ └── deprecated.py │ │ ├── downloadutils │ │ │ └── __init__.py │ │ ├── adapters │ │ │ ├── __init__.py │ │ │ ├── host_header_ssl.py │ │ │ ├── fingerprint.py │ │ │ ├── ssl.py │ │ │ └── source.py │ │ ├── multipart │ │ │ └── __init__.py │ │ ├── exceptions.py │ │ ├── __init__.py │ │ ├── threaded │ │ │ └── thread.py │ │ └── sessions.py │ ├── chardet-4.0.0.dist-info │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── entry_points.txt │ │ └── WHEEL │ ├── six-1.16.0.dist-info │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── WHEEL │ │ ├── RECORD │ │ ├── LICENSE │ │ └── METADATA │ ├── certifi-2021.10.8.dist-info │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── WHEEL │ │ ├── RECORD │ │ └── LICENSE │ ├── decorator-4.4.2.dist-info │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── pbr.json │ │ ├── WHEEL │ │ ├── RECORD │ │ └── LICENSE.txt │ ├── python_dateutil-2.8.2.dist-info │ │ ├── zip-safe │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── WHEEL │ │ ├── RECORD │ │ └── LICENSE │ ├── requests-2.27.1.dist-info │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── WHEEL │ │ └── RECORD │ ├── urllib3-1.26.13.dist-info │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── WHEEL │ │ └── LICENSE.txt │ ├── idna │ │ ├── package_data.py │ │ ├── __init__.py │ │ ├── compat.py │ │ └── intranges.py │ ├── requests_toolbelt-0.9.1.dist-info │ │ ├── INSTALLER │ │ ├── top_level.txt │ │ ├── WHEEL │ │ ├── LICENSE │ │ └── AUTHORS.rst │ ├── dateutil │ │ ├── tzwin.py │ │ ├── zoneinfo │ │ │ ├── dateutil-zoneinfo.tar.gz │ │ │ └── rebuild.py │ │ ├── _version.py │ │ ├── __init__.py │ │ ├── tz │ │ │ ├── __init__.py │ │ │ └── _factories.py │ │ ├── _common.py │ │ ├── parser │ │ │ └── __init__.py │ │ ├── utils.py │ │ └── easter.py │ ├── certifi │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── core.py │ ├── pytz │ │ └── zoneinfo.zip │ ├── bin │ │ └── chardetect │ └── requests │ │ ├── __version__.py │ │ ├── certs.py │ │ ├── hooks.py │ │ ├── packages.py │ │ ├── _internal_utils.py │ │ └── compat.py ├── queue.yaml ├── static │ ├── googled756c8e5e6bbce3a.html │ ├── header.png │ ├── favicon.ico │ ├── feed-icon.png │ ├── twitter-icon.png │ ├── apple-touch-icon.png │ ├── twitter-sign-in.png │ ├── header-background.png │ ├── robots.txt │ ├── birdfeeder.js │ ├── mastofeeder.js │ ├── main.js │ └── util.js ├── cron.yaml ├── templates │ ├── tweetdigest │ │ ├── usernames.snippet │ │ ├── list.snippet │ │ ├── digest-contents.snippet │ │ ├── retired-digest.html │ │ ├── legacy-digest.html │ │ ├── retired-digest.atom │ │ ├── digest.atom │ │ ├── legacy-digest.atom │ │ ├── digest.html │ │ └── index.html │ ├── mastofeeder │ │ ├── footer.snippet │ │ ├── account-link.snippet │ │ ├── digest.html │ │ ├── intro.snippet │ │ ├── digest-contents.snippet │ │ ├── digest.atom │ │ ├── index-signed-out.html │ │ ├── feed.html │ │ ├── status-footer.snippet │ │ ├── feed.atom │ │ ├── status-group.snippet │ │ ├── index-signed-in.html │ │ └── status.snippet │ ├── birdfeeder │ │ ├── footer.snippet │ │ ├── intro.snippet │ │ ├── index-signed-out.html │ │ ├── backup.atom │ │ ├── index-signed-in.html │ │ ├── feed.atom │ │ └── backup.html │ ├── not-found.html │ ├── feedplayback │ │ ├── intro-body.snippet │ │ └── subscription.html │ ├── base │ │ ├── page.html │ │ └── status-group.snippet │ └── index.html ├── queue.yaml.disabled ├── dos.yaml ├── index.yaml ├── cron.yaml.disabled ├── app.yaml ├── appengine_config.py └── cron_tasks.py ├── worker ├── .vscode │ └── settings.json ├── src │ ├── lib │ │ ├── index.ts │ │ ├── assets │ │ │ ├── header.png │ │ │ ├── feed-icon.png │ │ │ ├── feed-icon@2x.png │ │ │ └── header-background.png │ │ ├── constants.ts │ │ ├── components │ │ │ ├── AccountLink.svelte │ │ │ ├── MastodonDebugHtml.svelte │ │ │ └── MastodonStatusFooter.svelte │ │ ├── masto-feeder │ │ │ └── types.ts │ │ ├── masto.ts │ │ └── kv.ts │ ├── routes │ │ └── masto-feeder │ │ │ ├── +layout.server.ts │ │ │ ├── sign-in-callback │ │ │ └── +server.ts │ │ │ ├── feed │ │ │ └── [feedId] │ │ │ │ ├── parent │ │ │ │ └── [statusId] │ │ │ │ │ └── +server.ts │ │ │ │ └── timeline │ │ │ │ └── +server.ts │ │ │ └── +page.server.ts │ ├── app.html │ └── app.d.ts ├── static │ └── favicon.ico ├── .prettierignore ├── .gitignore ├── vite.config.ts ├── svelte.config.js ├── wrangler.toml ├── tsconfig.json ├── eslint.config.js ├── README.md └── package.json ├── assets ├── favicon.psd ├── header.psd ├── profile icon.psd └── profile-icon.gif ├── .gitignore ├── twitter-digest-stub ├── app.yaml ├── main.py └── index.yaml ├── Makefile └── birdpinger └── README /app/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/django_settings.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/birdfeeder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/datasources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/feedplayback/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mastofeeder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/tweetdigest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/birdfeeder/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mastofeeder/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/datasources/oauth2/clients/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/urllib3/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/chardet/cli/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/third_party/chardet/metadata/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/urllib3/packages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/idna-2.10.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/cookies/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/chardet-4.0.0.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/idna-2.10.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | idna 2 | -------------------------------------------------------------------------------- /app/third_party/six-1.16.0.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/six-1.16.0.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | six 2 | -------------------------------------------------------------------------------- /app/third_party/urllib3/packages/backports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/certifi-2021.10.8.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/decorator-4.4.2.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/python_dateutil-2.8.2.dist-info/zip-safe: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/third_party/requests-2.27.1.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/downloadutils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/urllib3-1.26.13.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/urllib3/contrib/_securetransport/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/third_party/chardet-4.0.0.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | chardet 2 | -------------------------------------------------------------------------------- /app/third_party/idna/package_data.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.10' 2 | 3 | -------------------------------------------------------------------------------- /app/third_party/python_dateutil-2.8.2.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/requests-2.27.1.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt-0.9.1.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /app/third_party/urllib3-1.26.13.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /app/third_party/certifi-2021.10.8.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | certifi 2 | -------------------------------------------------------------------------------- /app/third_party/decorator-4.4.2.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | decorator 2 | -------------------------------------------------------------------------------- /app/third_party/python_dateutil-2.8.2.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | dateutil 2 | -------------------------------------------------------------------------------- /worker/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt-0.9.1.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | requests_toolbelt 2 | -------------------------------------------------------------------------------- /app/queue.yaml: -------------------------------------------------------------------------------- 1 | queue: 2 | - name: birdfeeder-update 3 | rate: 5/s 4 | bucket_size: 5 5 | -------------------------------------------------------------------------------- /app/static/googled756c8e5e6bbce3a.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googled756c8e5e6bbce3a.html -------------------------------------------------------------------------------- /assets/favicon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/assets/favicon.psd -------------------------------------------------------------------------------- /assets/header.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/assets/header.psd -------------------------------------------------------------------------------- /app/static/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/static/header.png -------------------------------------------------------------------------------- /app/third_party/decorator-4.4.2.dist-info/pbr.json: -------------------------------------------------------------------------------- 1 | {"is_release": false, "git_version": "8608a46"} -------------------------------------------------------------------------------- /app/third_party/idna/__init__.py: -------------------------------------------------------------------------------- 1 | from .package_data import __version__ 2 | from .core import * 3 | -------------------------------------------------------------------------------- /app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/static/favicon.ico -------------------------------------------------------------------------------- /app/static/feed-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/static/feed-icon.png -------------------------------------------------------------------------------- /app/third_party/dateutil/tzwin.py: -------------------------------------------------------------------------------- 1 | # tzwin has moved to dateutil.tz.win 2 | from .tz.win import * 3 | -------------------------------------------------------------------------------- /assets/profile icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/assets/profile icon.psd -------------------------------------------------------------------------------- /assets/profile-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/assets/profile-icon.gif -------------------------------------------------------------------------------- /worker/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /app/third_party/urllib3/_version.py: -------------------------------------------------------------------------------- 1 | # This file is protected via CODEOWNERS 2 | __version__ = "1.26.13" 3 | -------------------------------------------------------------------------------- /worker/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/worker/static/favicon.ico -------------------------------------------------------------------------------- /app/static/twitter-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/static/twitter-icon.png -------------------------------------------------------------------------------- /app/third_party/certifi/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import contents, where 2 | 3 | __version__ = "2021.10.08" 4 | -------------------------------------------------------------------------------- /app/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/static/apple-touch-icon.png -------------------------------------------------------------------------------- /app/static/twitter-sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/static/twitter-sign-in.png -------------------------------------------------------------------------------- /app/static/header-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/static/header-background.png -------------------------------------------------------------------------------- /app/third_party/pytz/zoneinfo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/third_party/pytz/zoneinfo.zip -------------------------------------------------------------------------------- /worker/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /worker/src/lib/assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/worker/src/lib/assets/header.png -------------------------------------------------------------------------------- /app/third_party/chardet-4.0.0.dist-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [console_scripts] 2 | chardetect = chardet.cli.chardetect:main 3 | 4 | -------------------------------------------------------------------------------- /worker/src/lib/assets/feed-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/worker/src/lib/assets/feed-icon.png -------------------------------------------------------------------------------- /worker/src/lib/assets/feed-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/worker/src/lib/assets/feed-icon@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | app/datasources/oauth_keys.py 3 | *.[68] 4 | birdpinger/birdpinger 5 | app/birdfeeder/handlers/pingersecret.py 6 | -------------------------------------------------------------------------------- /worker/src/lib/assets/header-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/worker/src/lib/assets/header-background.png -------------------------------------------------------------------------------- /app/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /tweet-digest/digest 3 | Disallow: /feed-playback/subscription 4 | Disallow: /bird-feeder/feed/ 5 | -------------------------------------------------------------------------------- /app/cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: Daily exception report 3 | url: /_ereporter?sender=mihai.parparita@gmail.com&delete=true 4 | schedule: every day 00:00 5 | -------------------------------------------------------------------------------- /app/third_party/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/streamspigot/HEAD/app/third_party/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz -------------------------------------------------------------------------------- /app/third_party/idna-2.10.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.33.6) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/third_party/six-1.16.0.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.36.2) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/third_party/certifi-2021.10.8.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.35.1) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/third_party/chardet-4.0.0.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.35.1) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/third_party/decorator-4.4.2.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.33.4) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/third_party/requests-2.27.1.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.37.1) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/third_party/urllib3-1.26.13.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.38.4) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/templates/tweetdigest/usernames.snippet: -------------------------------------------------------------------------------- 1 | {% for username in usernames %}{{ username }}{% if not forloop.last %}, {% endif %}{% endfor %} -------------------------------------------------------------------------------- /app/third_party/python_dateutil-2.8.2.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.36.2) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/templates/mastofeeder/footer.snippet: -------------------------------------------------------------------------------- 1 | Feeds are exported under randomly-generated URLs. Though they should not be guessable, they may end up "leaking" if accidentally sent to someone. 2 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt-0.9.1.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.32.3) 3 | Root-Is-Purelib: true 4 | Tag: py2-none-any 5 | Tag: py3-none-any 6 | 7 | -------------------------------------------------------------------------------- /app/third_party/dateutil/_version.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # file generated by setuptools_scm 3 | # don't change, don't track in version control 4 | version = '2.8.2' 5 | version_tuple = (2, 8, 2) 6 | -------------------------------------------------------------------------------- /app/queue.yaml.disabled: -------------------------------------------------------------------------------- 1 | queue: 2 | - name: feedplayback-advance 3 | rate: 1/s 4 | bucket_size: 1 5 | 6 | - name: birdfeeder-crawl-on-demand 7 | rate: 5/s 8 | bucket_size: 5 9 | 10 | -------------------------------------------------------------------------------- /app/templates/tweetdigest/list.snippet: -------------------------------------------------------------------------------- 1 | the {{ list_id }} 2 | list created by {{ list_owner }} -------------------------------------------------------------------------------- /twitter-digest-stub/app.yaml: -------------------------------------------------------------------------------- 1 | application: twitter-digest-hrd 2 | version: 2 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: true 6 | 7 | handlers: 8 | - url: /.* 9 | script: main.app 10 | 11 | -------------------------------------------------------------------------------- /worker/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # SvelteKit generated code 4 | /.svelte-kit 5 | 6 | # Build artifacts when building the worker 7 | .cloudflare 8 | 9 | # wrangler local state when running `wrangler dev` 10 | .wrangler 11 | -------------------------------------------------------------------------------- /worker/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const APP_NAME = "Stream Spigot"; 2 | 3 | export const ANCHOR_COLOR = "#2db300"; 4 | export const BUBBLE_COLOR = "#00000009"; 5 | export const BUBBLE_TEXT_COLOR = "#41419b"; 6 | export const USER_LINK_COLOR = "#666"; 7 | -------------------------------------------------------------------------------- /app/templates/birdfeeder/footer.snippet: -------------------------------------------------------------------------------- 1 | Though the feed URL that your timeline is available under is not guessable, you 2 | may inadvertedly "leak" it if you are not careful. For example, emailing an item 3 | from your timeline feed in Google Reader exposes the feed URL. -------------------------------------------------------------------------------- /app/third_party/dateutil/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | from ._version import version as __version__ 4 | except ImportError: 5 | __version__ = 'unknown' 6 | 7 | __all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', 8 | 'utils', 'zoneinfo'] 9 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/cookies/forgetful.py: -------------------------------------------------------------------------------- 1 | """The module containing the code for ForgetfulCookieJar.""" 2 | from requests.cookies import RequestsCookieJar 3 | 4 | 5 | class ForgetfulCookieJar(RequestsCookieJar): 6 | def set_cookie(self, *args, **kwargs): 7 | return 8 | -------------------------------------------------------------------------------- /app/third_party/chardet/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module exists only to simplify retrieving the version number of chardet 3 | from within setup.py and from chardet subpackages. 4 | 5 | :author: Dan Blanchard (dan.blanchard@gmail.com) 6 | """ 7 | 8 | __version__ = "4.0.0" 9 | VERSION = __version__.split('.') 10 | -------------------------------------------------------------------------------- /app/third_party/idna/compat.py: -------------------------------------------------------------------------------- 1 | from .core import * 2 | from .codec import * 3 | 4 | def ToASCII(label): 5 | return encode(label) 6 | 7 | def ToUnicode(label): 8 | return decode(label) 9 | 10 | def nameprep(s): 11 | raise NotImplementedError("IDNA 2008 does not utilise nameprep protocol") 12 | 13 | -------------------------------------------------------------------------------- /worker/src/routes/masto-feeder/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import {MastoFeederController} from "$lib/masto-feeder/controller"; 2 | 3 | export async function load(event) { 4 | const controller = new MastoFeederController(event); 5 | return { 6 | session: (await controller.getSession()) ?? undefined, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /app/birdfeeder/handlers/pingersecret.py.template: -------------------------------------------------------------------------------- 1 | # To not make it too easy for drive-by users to trigger birdfeeder pings, 2 | # rename this file to pingersecret.py, fillin in a value for the secret key, 3 | # and pass it to the birdfeeder binary with the --stream_spigot_secret command 4 | # line flag. 5 | 6 | SECRET = 'SECRET_KEY_GOES_HERE' -------------------------------------------------------------------------------- /app/third_party/certifi/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from certifi import contents, where 4 | 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument("-c", "--contents", action="store_true") 7 | args = parser.parse_args() 8 | 9 | if args.contents: 10 | print(contents()) 11 | else: 12 | print(where()) 13 | -------------------------------------------------------------------------------- /app/templates/mastofeeder/account-link.snippet: -------------------------------------------------------------------------------- 1 | {% with display_status.status.account as account %} 2 | 3 | {{ display_status.account_display_name }} 4 | 6 | @{{ account.username }} 7 | 8 | 9 | {% endwith %} 10 | -------------------------------------------------------------------------------- /app/templates/not-found.html: -------------------------------------------------------------------------------- 1 | {% extends "base/page.html" %} 2 | 3 | {% block intro %} 4 |
5 | Ooops, can't find that page. Go to the {{ APP_NAME }} 6 | homepage and see if you can find what you're looking for there. 7 |
8 | {% endblock %} 9 | 10 | {% block footer %} 11 | π 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /worker/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {sveltekit} from "@sveltejs/kit/vite"; 2 | import {defineConfig} from "vite"; 3 | 4 | export default defineConfig({ 5 | server: { 6 | host: true, 7 | port: 3413, 8 | }, 9 | preview: { 10 | host: true, 11 | port: 4413, 12 | }, 13 | plugins: [sveltekit()], 14 | }); 15 | -------------------------------------------------------------------------------- /app/third_party/bin/chardetect: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | 6 | from chardet.cli.chardetect import main 7 | 8 | if __name__ == '__main__': 9 | sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) 10 | sys.exit(main()) 11 | -------------------------------------------------------------------------------- /app/templates/feedplayback/intro-body.snippet: -------------------------------------------------------------------------------- 1 |This is a feed playback for the 2 | {{ feed_title }} feed.
3 | 4 |The first item appears below, and after this a new item will appear 5 | {{ frequency }} (there will be {{ item_count }} items total).
6 | 7 |See this page more 8 | information about feed playback.
9 | -------------------------------------------------------------------------------- /app/datasources/oauth2/_version.py: -------------------------------------------------------------------------------- 1 | # This is the version of this source code. 2 | 3 | verstr = "1.3.128" 4 | try: 5 | from pyutil.version_class import Version as pyutil_Version 6 | __version__ = pyutil_Version(verstr) 7 | except (ImportError, ValueError): 8 | # Maybe there is no pyutil installed. 9 | from distutils.version import LooseVersion as distutils_Version 10 | __version__ = distutils_Version(verstr) 11 | -------------------------------------------------------------------------------- /worker/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |Debug HTML output.
10 | {% endblock %} 11 | 12 | {% block body %} 13 | {{ digest_contents|safe }} 14 | {% endblock %} 15 | 16 | {% block footer %} 17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | requests-toolbelt.adapters 4 | ========================== 5 | 6 | See http://toolbelt.rtfd.org/ for documentation 7 | 8 | :copyright: (c) 2014 by Ian Cordasco and Cory Benfield 9 | :license: Apache v2.0, see LICENSE for more details 10 | """ 11 | 12 | from .ssl import SSLAdapter 13 | from .source import SourceAddressAdapter 14 | 15 | __all__ = ['SSLAdapter', 'SourceAddressAdapter'] 16 | -------------------------------------------------------------------------------- /app/templates/tweetdigest/retired-digest.html: -------------------------------------------------------------------------------- 1 | {% extends "base/page.html" %} 2 | 3 | {% block title %}{{ APP_NAME }} : Retired Tweet Digest{% endblock %} 4 | {% block subtitle %}Retired Tweet Digest{% endblock %} 5 | 6 | {% block intro %} 7 |Tweet Digest can no longer operate (due to Twitter restricting API access) and has thus been retired.
8 | {% endblock %} 9 | 10 | {% block footer %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /birdpinger/README: -------------------------------------------------------------------------------- 1 | To build and run 2 | 3 | 1. Install Go per http://golang.org/doc/install.html 4 | 2. go get github.com/araddon/httpstream 5 | 3. go get github.com/araddon/goauth 6 | 4. cd birdpinger 7 | 5. go build 8 | 6. ./birdpinger \ 9 | --twitter_username=[username] \ 10 | --twitter_password=[password] \ 11 | --stream_spigot_hostname=This {{ APP_NAME }} tool lets you subscribe to your 2 | Mastodon timeline and lists in a feedreader such as 3 | NetNewsWire, 4 | NewsBlur, 5 | Reeder or 6 | Feedly.
7 | 8 |By signining in with your Mastodon account, you enable Masto Feeder to 9 | generate feeds under "secret" URLs that will contain the statuses of 10 | accounts that you follow or have in a list.
11 | -------------------------------------------------------------------------------- /worker/src/lib/masto-feeder/types.ts: -------------------------------------------------------------------------------- 1 | export type MastoFeederApp = { 2 | instanceUrl: string; 3 | clientId: string; 4 | clientSecret: string; 5 | }; 6 | 7 | export type MastoFeederAuthRequest = { 8 | id: string; 9 | instanceUrl: string; 10 | }; 11 | 12 | export type MastoFeederSession = { 13 | sessionId: string; 14 | mastodonId: string; 15 | instanceUrl: string; 16 | feedId: string; 17 | accessToken: string; 18 | prefs?: MastoFeederPrefs; 19 | }; 20 | 21 | export type MastoFeederPrefs = { 22 | timeZone?: string; 23 | useLocalUrls?: boolean; 24 | }; 25 | -------------------------------------------------------------------------------- /worker/src/routes/masto-feeder/feed/[feedId]/parent/[statusId]/+server.ts: -------------------------------------------------------------------------------- 1 | import {MastoFeederController} from "$lib/masto-feeder/controller"; 2 | import {error, type RequestHandler} from "@sveltejs/kit"; 3 | 4 | export const GET: RequestHandler = async event => { 5 | const controller = new MastoFeederController(event); 6 | const {feedId, statusId} = event.params; 7 | if (!feedId) { 8 | return error(400, "Invalid feed ID"); 9 | } 10 | if (!statusId) { 11 | return error(400, "Invalid status ID"); 12 | } 13 | 14 | return controller.handleStatusParent(feedId, statusId); 15 | }; 16 | -------------------------------------------------------------------------------- /app/templates/mastofeeder/digest-contents.snippet: -------------------------------------------------------------------------------- 1 |This {{ APP_NAME }} tool lets you subscribe to your Twitter 2 | timeline in a feed reader such as NetNewsWire. By signing in with your 3 | Twitter account, you enable Bird Feeder to generate a feed under a "secret" 4 | URL that will contain the tweets of all the people that you follow.
5 | 6 |Note: feeds are expensive to provide and are only available to a 7 | few accounts.
8 | 9 |As a bonus for 10 | these 11 | uncertain times, this tool can also generate a backup of your 12 | tweets and favorites. This is available for all accounts.
13 | -------------------------------------------------------------------------------- /app/templates/tweetdigest/legacy-digest.html: -------------------------------------------------------------------------------- 1 | {% extends "base/page.html" %} 2 | 3 | {% block title %}{{ APP_NAME }} : Legacy Tweet Digest{% endblock %} 4 | {% block subtitle %}Legacy Tweet Digest{% endblock %} 5 | 6 | {% block intro %} 7 |You have been redirected from a twitter-digest.appspot.com URL.
8 | That application is no longer running.
If you still wish to get a digest of
11 | tweets, you can do at this URL:
12 | {{ digest_url }}
To create a new digest, see the 18 | Tweet Digest tool on 19 | {{ APP_NAME }}.
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /app/third_party/decorator-4.4.2.dist-info/RECORD: -------------------------------------------------------------------------------- 1 | decorator-4.4.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 2 | decorator-4.4.2.dist-info/LICENSE.txt,sha256=_RFmDKvwUyCCxFcGhi-vwpSQfsf44heBgkCkmZgGeC4,1309 3 | decorator-4.4.2.dist-info/METADATA,sha256=RYLh5Qy8XzYOcgCT6RsI_cTXG_PE1QvoAVT-u2vus80,4168 4 | decorator-4.4.2.dist-info/RECORD,, 5 | decorator-4.4.2.dist-info/WHEEL,sha256=h_aVn5OB2IERUjMbi2pucmR_zzWJtk303YXvhh60NJ8,110 6 | decorator-4.4.2.dist-info/pbr.json,sha256=AL84oUUWQHwkd8OCPhLRo2NJjU5MDdmXMqRHv-posqs,47 7 | decorator-4.4.2.dist-info/top_level.txt,sha256=Kn6eQjo83ctWxXVyBMOYt0_YpjRjBznKYVuNyuC_DSI,10 8 | decorator.py,sha256=aQ8Ozc-EK26xBTOXVR5A-8Szgx99_bhaexZSGNn38Yc,17222 9 | decorator.pyc,, 10 | -------------------------------------------------------------------------------- /app/templates/mastofeeder/digest.atom: -------------------------------------------------------------------------------- 1 | 2 |To get started, sign in to your Mastodon instance to allow Masto Feeder 15 | access to your timeline and lists.
16 | 17 | 21 | 22 | {% endblock %} 23 | 24 | {% block footer %} 25 | {% include "footer.snippet" %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /app/templates/birdfeeder/index-signed-out.html: -------------------------------------------------------------------------------- 1 | {% extends "base/page.html" %} 2 | 3 | {% block title %}{{ APP_NAME }} : Bird Feeder{% endblock %} 4 | {% block subtitle %}Bird Feeder{% endblock %} 5 | 6 | {% block bodystart %} 7 | 8 | {% endblock %} 9 | 10 | {% block intro %} 11 | {% include "intro.snippet" %} 12 | {% endblock %} 13 | 14 | {% block body %} 15 | 16 |To get started, sign in with Twitter to allow Bird Feeder access to your 17 | tweets.
18 | 19 |{% include "footer.snippet" %}
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /worker/src/routes/masto-feeder/feed/[feedId]/timeline/+server.ts: -------------------------------------------------------------------------------- 1 | import {MastoFeederController} from "$lib/masto-feeder/controller"; 2 | import {error, type RequestHandler} from "@sveltejs/kit"; 3 | 4 | export const GET: RequestHandler = async event => { 5 | const controller = new MastoFeederController(event); 6 | const {feedId} = event.params; 7 | if (!feedId) { 8 | return error(400, "Invalid feed ID"); 9 | } 10 | const {searchParams} = event.url; 11 | const debug = searchParams.get("debug") === "true"; 12 | const html = searchParams.get("html") === "true"; 13 | const includeStatusJson = searchParams.get("includeStatusJson") === "true"; 14 | 15 | return controller.handleTimelineFeed(feedId, { 16 | debug, 17 | html, 18 | includeStatusJson, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /app/birdfeeder/handlers/main.py: -------------------------------------------------------------------------------- 1 | import session 2 | 3 | class IndexHandler(session.SessionApiHandler): 4 | def _get_signed_in(self): 5 | twitter_user = self._api.GetUser(self._session.twitter_id) 6 | 7 | timeline_feed_url = self._session.get_timeline_feed_url() 8 | 9 | self._write_template('birdfeeder/index-signed-in.html', { 10 | 'twitter_user': twitter_user, 11 | 'sign_out_path': self._get_path('sign-out'), 12 | 'timeline_feed_url': timeline_feed_url, 13 | 'backup_path': self._get_path('backup'), 14 | 'allows_feed_updates': self._session.allows_feed_updates(), 15 | }) 16 | 17 | def _get_signed_out(self): 18 | self._write_template('birdfeeder/index-signed-out.html', { 19 | 'sign_in_path': self._get_path('sign-in'), 20 | }) 21 | -------------------------------------------------------------------------------- /app/templates/mastofeeder/feed.html: -------------------------------------------------------------------------------- 1 | {% extends "base/page.html" %} 2 | 3 | {% block title %}{{ APP_NAME }}: {{ feed_title }}{% endblock %} 4 | 5 | {% block subtitle %}{{ feed_title }}{% endblock %} 6 | 7 | {% block intro %} 8 |Debug HTML output.
9 | 14 | {% endblock %} 15 | 16 | {% block body %} 17 | {% for display_status in display_statuses %} 18 |Tweet Digest can no longer operate (due to Twitter restricting API access) and has thus been retired.
16 |{{ digest_errors }}
21 | {% endfilter %} 22 | {% endif %} 23 |You are subscribed to a twitter-digest.appspot.com feed
16 | URL. That application is no longer running.
If you still wish to get a digest of tweets, you can instead
19 | subscribe to this URL:
20 | {{ digest_url }}
To create a new digest, see the 23 | Tweet Digest tool on 24 | {{ APP_NAME }}.
25 |This is a feed playback for the 13 | {{ feed_title }} feed. It is set 14 | to update {{ frequency }}. It is currently at item 15 | {{ position }} of {{ item_count }}.
16 | 17 | 21 | 22 |You can subscribe to 23 | the playback feed 24 | (view in Google Reader). 25 |
26 | {% endblock %} 27 | 28 | {% block footer %} 29 |To play back another feed, see the 30 | Feed Playback tool on 31 | {{ APP_NAME }}.
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /worker/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import ts from "typescript-eslint"; 3 | import svelte from "eslint-plugin-svelte"; 4 | import prettier from "eslint-config-prettier"; 5 | import globals from "globals"; 6 | 7 | /** @type {import('eslint').Linter.FlatConfig[]} */ 8 | export default [ 9 | js.configs.recommended, 10 | ...ts.configs.recommended, 11 | ...svelte.configs["flat/recommended"], 12 | prettier, 13 | ...svelte.configs["flat/prettier"], 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.browser, 18 | ...globals.node, 19 | }, 20 | }, 21 | rules: { 22 | "no-constant-condition": "off", 23 | }, 24 | }, 25 | { 26 | files: ["**/*.svelte"], 27 | languageOptions: { 28 | parserOptions: { 29 | parser: ts.parser, 30 | }, 31 | }, 32 | rules: { 33 | "svelte/no-at-html-tags": "off", 34 | }, 35 | }, 36 | { 37 | ignores: [ 38 | "build/", 39 | ".svelte-kit/", 40 | "package/", 41 | ".cloudflare/", 42 | ".wrangler/", 43 | ], 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /app/templates/tweetdigest/digest.html: -------------------------------------------------------------------------------- 1 | {% extends "base/page.html" %} 2 | 3 | {% block title %}{{ APP_NAME }}: Tweet Digest for {{ title_date }}{% endblock %} 4 | 5 | {% block head %} 6 | 10 | 20 | {% endblock %} 21 | 22 | {% block subtitle %}Tweet Digest{% endblock %} 23 | {% block subsubtitle %}for {{ title_date }}{% endblock %} 24 | 25 | {% block intro %} 26 |This is a digest of {{ digest_source|safe }}. It is also available 27 | as a feed.
28 | {% endblock %} 29 | 30 | {% block body %} 31 |{{ digest_errors|safe }}
39 | {% endif %} 40 |To create a new digest, see the 41 | Tweet Digest tool on 42 | {{ APP_NAME }}.
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /app/third_party/requests/_internal_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | requests._internal_utils 5 | ~~~~~~~~~~~~~~ 6 | 7 | Provides utility functions that are consumed internally by Requests 8 | which depend on extremely few external helpers (such as compat) 9 | """ 10 | 11 | from .compat import is_py2, builtin_str, str 12 | 13 | 14 | def to_native_string(string, encoding='ascii'): 15 | """Given a string object, regardless of type, returns a representation of 16 | that string in the native string type, encoding and decoding where 17 | necessary. This assumes ASCII unless told otherwise. 18 | """ 19 | if isinstance(string, builtin_str): 20 | out = string 21 | else: 22 | if is_py2: 23 | out = string.encode(encoding) 24 | else: 25 | out = string.decode(encoding) 26 | 27 | return out 28 | 29 | 30 | def unicode_is_ascii(u_string): 31 | """Determine if unicode string only contains ASCII characters. 32 | 33 | :param str u_string: unicode string to check. Must be unicode 34 | and not Python 2 `str`. 35 | :rtype: bool 36 | """ 37 | assert isinstance(u_string, str) 38 | try: 39 | u_string.encode('ascii') 40 | return True 41 | except UnicodeEncodeError: 42 | return False 43 | -------------------------------------------------------------------------------- /app/third_party/idna-2.10.dist-info/RECORD: -------------------------------------------------------------------------------- 1 | idna-2.10.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 2 | idna-2.10.dist-info/LICENSE.rst,sha256=QSAUQg0kc9ugYRfD1Nng7sqm3eDKMM2VH07CvjlCbzI,1565 3 | idna-2.10.dist-info/METADATA,sha256=ZWCaQDBjdmSvx5EU7Cv6ORC-9NUQ6nXh1eXx38ySe40,9104 4 | idna-2.10.dist-info/RECORD,, 5 | idna-2.10.dist-info/WHEEL,sha256=8zNYZbwQSXoB9IfXOjPfeNwvAsALAjffgk27FqvCWbo,110 6 | idna-2.10.dist-info/top_level.txt,sha256=jSag9sEDqvSPftxOQy-ABfGV_RSy7oFh4zZJpODV8k0,5 7 | idna/__init__.py,sha256=9Nt7xpyet3DmOrPUGooDdAwmHZZu1qUAy2EaJ93kGiQ,58 8 | idna/__init__.pyc,, 9 | idna/codec.py,sha256=lvYb7yu7PhAqFaAIAdWcwgaWI2UmgseUua-1c0AsG0A,3299 10 | idna/codec.pyc,, 11 | idna/compat.py,sha256=R-h29D-6mrnJzbXxymrWUW7iZUvy-26TQwZ0ij57i4U,232 12 | idna/compat.pyc,, 13 | idna/core.py,sha256=jCoaLb3bA2tS_DDx9PpGuNTEZZN2jAzB369aP-IHYRE,11951 14 | idna/core.pyc,, 15 | idna/idnadata.py,sha256=gmzFwZWjdms3kKZ_M_vwz7-LP_SCgYfSeE03B21Qpsk,42350 16 | idna/idnadata.pyc,, 17 | idna/intranges.py,sha256=TY1lpxZIQWEP6tNqjZkFA5hgoMWOj1OBmnUG8ihT87E,1749 18 | idna/intranges.pyc,, 19 | idna/package_data.py,sha256=bxBjpLnE06_1jSYKEy5svOMu1zM3OMztXVUb1tPlcp0,22 20 | idna/package_data.pyc,, 21 | idna/uts46data.py,sha256=lMdw2zdjkH1JUWXPPEfFUSYT3Fyj60bBmfLvvy5m7ko,202084 22 | idna/uts46data.pyc,, 23 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Collection of exceptions raised by requests-toolbelt.""" 3 | 4 | 5 | class StreamingError(Exception): 6 | """Used in :mod:`requests_toolbelt.downloadutils.stream`.""" 7 | pass 8 | 9 | 10 | class VersionMismatchError(Exception): 11 | """Used to indicate a version mismatch in the version of requests required. 12 | 13 | The feature in use requires a newer version of Requests to function 14 | appropriately but the version installed is not sufficient. 15 | """ 16 | pass 17 | 18 | 19 | class RequestsVersionTooOld(Warning): 20 | """Used to indicate that the Requests version is too old. 21 | 22 | If the version of Requests is too old to support a feature, we will issue 23 | this warning to the user. 24 | """ 25 | pass 26 | 27 | 28 | class IgnoringGAECertificateValidation(Warning): 29 | """Used to indicate that given GAE validation behavior will be ignored. 30 | 31 | If the user has tried to specify certificate validation when using the 32 | insecure AppEngine adapter, it will be ignored (certificate validation will 33 | remain off), so we will issue this warning to the user. 34 | 35 | In :class:`requests_toolbelt.adapters.appengine.InsecureAppEngineAdapter`. 36 | """ 37 | pass 38 | -------------------------------------------------------------------------------- /app/cron.yaml.disabled: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: 1d/0 3 | url: /cron/feed-playback/advance?frequency=1d&frequency_modulo=0 4 | schedule: every day 00:00 5 | 6 | - description: 2d/0 7 | url: /cron/feed-playback/advance?frequency=2d&frequency_modulo=0 8 | schedule: every day 01:00 9 | 10 | - description: 2d/1 11 | url: /cron/feed-playback/advance?frequency=2d&frequency_modulo=1 12 | schedule: every day 02:00 13 | 14 | - description: 7d/0 15 | url: /cron/feed-playback/advance?frequency=7d&frequency_modulo=0 16 | schedule: every day 03:00 17 | 18 | - description: 7d/1 19 | url: /cron/feed-playback/advance?frequency=7d&frequency_modulo=1 20 | schedule: every day 04:00 21 | 22 | - description: 7d/2 23 | url: /cron/feed-playback/advance?frequency=7d&frequency_modulo=2 24 | schedule: every day 05:00 25 | 26 | - description: 7d/3 27 | url: /cron/feed-playback/advance?frequency=7d&frequency_modulo=3 28 | schedule: every day 06:00 29 | 30 | - description: 7d/4 31 | url: /cron/feed-playback/advance?frequency=7d&frequency_modulo=4 32 | schedule: every day 07:00 33 | 34 | - description: 7d/5 35 | url: /cron/feed-playback/advance?frequency=7d&frequency_modulo=5 36 | schedule: every day 08:00 37 | 38 | - description: 7d/6 39 | url: /cron/feed-playback/advance?frequency=7d&frequency_modulo=6 40 | schedule: every day 09:00 41 | -------------------------------------------------------------------------------- /app/templates/mastofeeder/status-footer.snippet: -------------------------------------------------------------------------------- 1 | {% with display_status.status as status %} 2 | 3 | {% spaceless %} 4 |{{ display_status.debug_json }}
25 | {% endif %}
26 | {% endspaceless %}
27 |
28 | {% endwith %}
29 |
--------------------------------------------------------------------------------
/app/third_party/requests_toolbelt/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | requests-toolbelt
4 | =================
5 |
6 | See http://toolbelt.rtfd.org/ for documentation
7 |
8 | :copyright: (c) 2014 by Ian Cordasco and Cory Benfield
9 | :license: Apache v2.0, see LICENSE for more details
10 | """
11 |
12 | from .adapters import SSLAdapter, SourceAddressAdapter
13 | from .auth.guess import GuessAuth
14 | from .multipart import (
15 | MultipartEncoder, MultipartEncoderMonitor, MultipartDecoder,
16 | ImproperBodyPartContentException, NonMultipartContentTypeException
17 | )
18 | from .streaming_iterator import StreamingIterator
19 | from .utils.user_agent import user_agent
20 |
21 | __title__ = 'requests-toolbelt'
22 | __authors__ = 'Ian Cordasco, Cory Benfield'
23 | __license__ = 'Apache v2.0'
24 | __copyright__ = 'Copyright 2014 Ian Cordasco, Cory Benfield'
25 | __version__ = '0.9.1'
26 | __version_info__ = tuple(int(i) for i in __version__.split('.'))
27 |
28 | __all__ = [
29 | 'GuessAuth', 'MultipartEncoder', 'MultipartEncoderMonitor',
30 | 'MultipartDecoder', 'SSLAdapter', 'SourceAddressAdapter',
31 | 'StreamingIterator', 'user_agent', 'ImproperBodyPartContentException',
32 | 'NonMultipartContentTypeException', '__title__', '__authors__',
33 | '__license__', '__copyright__', '__version__', '__version_info__',
34 | ]
35 |
--------------------------------------------------------------------------------
/app/birdfeeder/handlers/tools.py:
--------------------------------------------------------------------------------
1 | import base.handlers
2 | from birdfeeder import data
3 | import birdfeeder.handlers.feed
4 | import birdfeeder.handlers.update
5 |
6 | # Helper handler (for development) that updates a single user's timeline and
7 | # refreshes their feed within a single request.
8 | class UpdateFeedHandler(base.handlers.BaseHandler):
9 | def get(self):
10 | session = data.Session.get_by_twitter_id(self.request.get('twitter_id'))
11 |
12 | birdfeeder.handlers.update.update_timeline(session)
13 |
14 | # We render the feed handler inline instead of redirecting to it, so
15 | # that a browser reload will allow this handler (which also updates)
16 | # to be triggered
17 | feed_handler = birdfeeder.handlers.feed.TimelineFeedHandler()
18 | feed_handler._session = session
19 | feed_handler._api = session.create_api()
20 | feed_handler.initialize(self.request, self.response)
21 | feed_handler._get_signed_in()
22 |
23 | class StatusHandler(base.handlers.BaseHandler):
24 | def get(self, status_id):
25 | statuses = data.StatusData.get_by_status_ids([status_id])
26 | if not statuses:
27 | self._write_not_found()
28 | return
29 |
30 | status = statuses[0]
31 | self._write_json(status.original_json_dict, pretty_print=True)
32 |
--------------------------------------------------------------------------------
/app/third_party/chardet/compat.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # Contributor(s):
3 | # Dan Blanchard
4 | # Ian Cordasco
5 | #
6 | # This library is free software; you can redistribute it and/or
7 | # modify it under the terms of the GNU Lesser General Public
8 | # License as published by the Free Software Foundation; either
9 | # version 2.1 of the License, or (at your option) any later version.
10 | #
11 | # This library is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 | # Lesser General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU Lesser General Public
17 | # License along with this library; if not, write to the Free Software
18 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
19 | # 02110-1301 USA
20 | ######################### END LICENSE BLOCK #########################
21 |
22 | import sys
23 |
24 |
25 | if sys.version_info < (3, 0):
26 | PY2 = True
27 | PY3 = False
28 | string_types = (str, unicode)
29 | text_type = unicode
30 | iteritems = dict.iteritems
31 | else:
32 | PY2 = False
33 | PY3 = True
34 | string_types = (bytes, str)
35 | text_type = str
36 | iteritems = dict.items
37 |
--------------------------------------------------------------------------------
/app/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: python27
2 | api_version: 1
3 | threadsafe: false
4 |
5 | builtins:
6 | - appstats: on
7 | - remote_api: on
8 |
9 | handlers:
10 | - url: /static
11 | static_dir: static
12 |
13 | - url: /favicon.ico
14 | static_files: static/favicon.ico
15 | upload: static/favicon.ico
16 |
17 | - url: /apple-touch-icon.png
18 | static_files: static/apple-touch-icon.png
19 | upload: static/apple-touch-icon.png
20 |
21 | - url: /googled756c8e5e6bbce3a.html
22 | static_files: static/googled756c8e5e6bbce3a.html
23 | upload: static/googled756c8e5e6bbce3a.html
24 |
25 | - url: /robots.txt
26 | static_files: static/robots.txt
27 | upload: static/robots.txt
28 |
29 | - url: /(cron|tasks|tools)/.*
30 | script: cron_tasks.py
31 | login: admin
32 |
33 | - url: /_ereporter.*
34 | script: $PYTHON_LIB/google/appengine/ext/ereporter/report_generator.py
35 | login: admin
36 |
37 | - url: /admin/.*
38 | script: $PYTHON_LIB/google/appengine/ext/admin
39 | login: admin
40 |
41 | - url: .*
42 | script: main.py
43 | secure: always
44 |
45 | libraries:
46 | - name: django
47 | version: "1.4"
48 |
49 | env_variables:
50 | DJANGO_SETTINGS_MODULE: 'django_settings'
51 |
52 | automatic_scaling:
53 | min_idle_instances: automatic
54 | max_idle_instances: 1
55 | min_pending_latency: automatic
56 | max_pending_latency: 0.030s
57 | max_instances: 1
58 |
--------------------------------------------------------------------------------
/app/third_party/urllib3/util/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | # For backwards compatibility, provide imports that used to be here.
4 | from .connection import is_connection_dropped
5 | from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers
6 | from .response import is_fp_closed
7 | from .retry import Retry
8 | from .ssl_ import (
9 | ALPN_PROTOCOLS,
10 | HAS_SNI,
11 | IS_PYOPENSSL,
12 | IS_SECURETRANSPORT,
13 | PROTOCOL_TLS,
14 | SSLContext,
15 | assert_fingerprint,
16 | resolve_cert_reqs,
17 | resolve_ssl_version,
18 | ssl_wrap_socket,
19 | )
20 | from .timeout import Timeout, current_time
21 | from .url import Url, get_host, parse_url, split_first
22 | from .wait import wait_for_read, wait_for_write
23 |
24 | __all__ = (
25 | "HAS_SNI",
26 | "IS_PYOPENSSL",
27 | "IS_SECURETRANSPORT",
28 | "SSLContext",
29 | "PROTOCOL_TLS",
30 | "ALPN_PROTOCOLS",
31 | "Retry",
32 | "Timeout",
33 | "Url",
34 | "assert_fingerprint",
35 | "current_time",
36 | "is_connection_dropped",
37 | "is_fp_closed",
38 | "get_host",
39 | "parse_url",
40 | "make_headers",
41 | "resolve_cert_reqs",
42 | "resolve_ssl_version",
43 | "split_first",
44 | "ssl_wrap_socket",
45 | "wait_for_read",
46 | "wait_for_write",
47 | "SKIP_HEADER",
48 | "SKIPPABLE_HEADERS",
49 | )
50 |
--------------------------------------------------------------------------------
/app/appengine_config.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | appstats_RECORD_FRACTION = .1
4 |
5 | class BlockingMiddleware(object):
6 | def __init__(self, app):
7 | self._wrapped_app = app
8 |
9 | def __call__(self, environ, start_response):
10 | user_agent = environ.get('HTTP_USER_AGENT', '')
11 | # Scraper running on EC2 (IP addresses 50.18.2.106 and 50.18.73.38)
12 | # that's requesting lots of Tweet Digest pages.
13 | if user_agent == 'Python-urllib/2.7':
14 | logging.info('Blocked request')
15 | start_response('403 Forbidden', [('Content-type','text/plain')])
16 | return ['']
17 |
18 | # Google Reader and iGoogle are dead, yet their crawler still lives on.
19 | # (and generates ~1,200 requests per day). Block it, since presumably no
20 | # one is actually looking at the resuts.
21 | if user_agent.startswith('Feedfetcher-Google;'):
22 | logging.info('Blocked Feedfetcher request')
23 | start_response('403 Forbidden', [('Content-type','text/plain')])
24 | return ['']
25 |
26 | return self._wrapped_app(environ, start_response)
27 |
28 | def webapp_add_wsgi_middleware(app):
29 | from google.appengine.ext.appstats import recording
30 | app = recording.appstats_wsgi_middleware(app)
31 |
32 | app = BlockingMiddleware(app)
33 |
34 | return app
35 |
--------------------------------------------------------------------------------
/app/third_party/decorator-4.4.2.dist-info/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2005-2018, Michele Simionato
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are
6 | met:
7 |
8 | Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | Redistributions in bytecode form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19 | HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
22 | OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
24 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
25 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
26 | DAMAGE.
27 |
--------------------------------------------------------------------------------
/app/templates/base/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block title %}{{ APP_NAME }}{% endblock %}
5 |
6 |
7 | {% block head %}{% endblock %}
8 | {% if not IS_DEV_SERVER %}
9 |
19 | {% endif %}
20 |
21 |
22 | {% block bodystart %}{% endblock %}
23 |
24 |
25 | {{ APP_NAME }}
26 |
27 | {% block subtitle %}{% endblock %}
28 | {% block subsubtitle %}{% endblock %}
29 |
30 |
31 |
32 |
33 |
34 | {% block intro %}{% endblock %}
35 |
36 |
37 | {% block body %}{% endblock %}
38 |
39 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/templates/birdfeeder/backup.atom:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ feed_url }}
4 |
5 |
6 | Twitter Backup - {{ subtitle }}
7 | {{ backup_date_iso }}Z
8 |
9 | {{ APP_NAME }}
10 |
11 | {% for status_group in status_groups %}
12 | {% with status_group.display_statuses.0 as display_status %}
13 |
14 | {{ display_status.permalink }}
15 |
16 | {{ display_status.title_as_text }}
17 | {{ display_status.created_at_iso }}Z
18 | {{ display_status.created_at_iso }}Z
19 |
20 | {% filter force_escape %}
21 |
22 | {% with '0' as bottom_margin %}
23 | {% include 'base/status-group.snippet' %}
24 | {% endwith %}
25 |
26 | {% endfilter %}
27 |
28 |
29 | {% endwith %}
30 | {% endfor %}
31 |
32 |
--------------------------------------------------------------------------------
/app/cron_tasks.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sys
4 |
5 | # Tweak import path so that httplib2 (which lives in datasources) can be
6 | # imported as httplib2 while the app is running.
7 | # TODO(mihaip): move httplib2 (and oauth2 and python-twitter) into a third_party
8 | # directory.
9 | APP_DIR = os.path.abspath(os.path.dirname(__file__))
10 | DATASOURCES_DIR = os.path.join(APP_DIR, 'datasources')
11 | THIRD_PARTY_DIR = os.path.join(APP_DIR, 'third_party')
12 | sys.path.insert(0, DATASOURCES_DIR)
13 | sys.path.insert(0, THIRD_PARTY_DIR)
14 |
15 | from google.appengine.ext import webapp
16 | from google.appengine.ext.webapp import util
17 |
18 | import birdfeeder.handlers.tools
19 | import birdfeeder.handlers.update
20 | import feedplayback.handlers
21 |
22 | def main():
23 | application = webapp.WSGIApplication([
24 | ('/cron/feed-playback/advance', feedplayback.handlers.AdvanceCronHandler),
25 | ('/tasks/feed-playback/advance', feedplayback.handlers.AdvanceTaskHandler),
26 |
27 | ('/cron/bird-feeder/update', birdfeeder.handlers.update.UpdateCronHandler),
28 | ('/tasks/bird-feeder/update', birdfeeder.handlers.update.UpdateTaskHandler),
29 | ('/tools/bird-feeder/update-feed', birdfeeder.handlers.tools.UpdateFeedHandler),
30 | ('/tools/bird-feeder/status/(\d+)', birdfeeder.handlers.tools.StatusHandler),
31 | ],
32 | debug=True)
33 | util.run_wsgi_app(application)
34 |
35 |
36 | if __name__ == '__main__':
37 | main()
38 |
--------------------------------------------------------------------------------
/app/static/util.js:
--------------------------------------------------------------------------------
1 | export function fetchJson(url, jsonCallback, errorCallback, opt_postData) {
2 | fetch(url, {
3 | method: opt_postData ? "POST" : "GET",
4 | body: opt_postData,
5 | })
6 | .then((response) => response.json())
7 | .then((json) => jsonCallback(json))
8 | .catch((error) => errorCallback(error));
9 | }
10 |
11 | export function throttle(func, minTimeMs) {
12 | var timeout = null;
13 | return function () {
14 | if (timeout) {
15 | window.clearTimeout(timeout);
16 | }
17 | timeout = window.setTimeout(function () {
18 | timeout = null;
19 | func();
20 | }, minTimeMs);
21 | };
22 | }
23 |
24 | export function getIso8601DateString(date) {
25 | function pad(n) {
26 | return n < 10 ? "0" + n : n;
27 | }
28 |
29 | return (
30 | date.getFullYear() +
31 | "-" +
32 | pad(date.getMonth() + 1) +
33 | "-" +
34 | pad(date.getDate())
35 | );
36 | }
37 |
38 | export function $(selector) {
39 | return document.querySelector(selector);
40 | }
41 |
42 | export function htmlEscape(str) {
43 | return str
44 | .replace(/&/g, "&")
45 | .replace(//g, ">")
47 | .replace(/"/g, """)
48 | .replace(/'/g, "'");
49 | }
50 |
51 | export function exportFunction(path, func) {
52 | const parts = path.split(".");
53 | let obj = window;
54 | for (var i = 0; i < parts.length - 1; i++) {
55 | const part = parts[i];
56 | obj[part] = obj[part] || {};
57 | obj = obj[part];
58 | }
59 | obj[parts[parts.length - 1]] = func;
60 | }
61 |
--------------------------------------------------------------------------------
/app/third_party/requests_toolbelt/adapters/host_header_ssl.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | requests_toolbelt.adapters.host_header_ssl
4 | ==========================================
5 |
6 | This file contains an implementation of the HostHeaderSSLAdapter.
7 | """
8 |
9 | from requests.adapters import HTTPAdapter
10 |
11 |
12 | class HostHeaderSSLAdapter(HTTPAdapter):
13 | """
14 | A HTTPS Adapter for Python Requests that sets the hostname for certificate
15 | verification based on the Host header.
16 |
17 | This allows requesting the IP address directly via HTTPS without getting
18 | a "hostname doesn't match" exception.
19 |
20 | Example usage:
21 |
22 | >>> s.mount('https://', HostHeaderSSLAdapter())
23 | >>> s.get("https://93.184.216.34", headers={"Host": "example.org"})
24 |
25 | """
26 |
27 | def send(self, request, **kwargs):
28 | # HTTP headers are case-insensitive (RFC 7230)
29 | host_header = None
30 | for header in request.headers:
31 | if header.lower() == "host":
32 | host_header = request.headers[header]
33 | break
34 |
35 | connection_pool_kwargs = self.poolmanager.connection_pool_kw
36 |
37 | if host_header:
38 | connection_pool_kwargs["assert_hostname"] = host_header
39 | elif "assert_hostname" in connection_pool_kwargs:
40 | # an assert_hostname from a previous request may have been left
41 | connection_pool_kwargs.pop("assert_hostname", None)
42 |
43 | return super(HostHeaderSSLAdapter, self).send(request, **kwargs)
44 |
--------------------------------------------------------------------------------
/worker/README.md:
--------------------------------------------------------------------------------
1 | # Cloudflare Worker
2 |
3 | In-development replacement of the App Engine app with a Cloudflare Worker.
4 |
5 | ## Development
6 |
7 | Install dependencies:
8 |
9 | ```bash
10 | npm install
11 | ```
12 |
13 | Start the dev server:
14 |
15 | ```bash
16 | npm run dev
17 | ```
18 |
19 | It will be running at [localhost:3413](http://localhost:3413/).
20 |
21 | If working on things that need Cloudflare-specific functionality (e.g. KV namespaces) then you'll need to start the worker in dev mode:
22 |
23 | ```bash
24 | npm run worker-dev
25 | ```
26 |
27 | It will be running at [localhost:5413](http://localhost:5413/).
28 |
29 | The [main timeline handler](https://github.com/mihaip/streamspigot/blob/main/worker/src/routes/masto-feeder/feed/%5BfeedId%5D/timeline/+server.ts#L13) has support for a few query parameters to help with testing:
30 |
31 | - `debug=true`: show fewer posts (just the 10 most recent ones) to speed up loading
32 | - `html=true`: return HTML instead of an Atom feed, for easier in-browser viewing
33 | - `includeStatusJson=true`: include the full JSON of each status for introspection
34 |
35 | ## Building
36 |
37 | To build and run a preview version:
38 |
39 | ```bash
40 | npm run build
41 | npm run preview
42 | ```
43 |
44 | It will be running at [localhost:4413](http://localhost:4413/).
45 |
46 | ## Deployment
47 |
48 | To deploy the app, assuming you've run `wrangler login` to set up Cloudflare credentials:
49 |
50 | ```bash
51 | npm run worker-deploy
52 | ```
53 |
54 | It will be running at [streamspigot.mihai-parparita.workers.dev](https://streamspigot.mihai-parparita.workers.dev)
55 |
--------------------------------------------------------------------------------
/app/third_party/requests_toolbelt/adapters/fingerprint.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Submodule containing the implementation for the FingerprintAdapter.
3 |
4 | This file contains an implementation of a Transport Adapter that validates
5 | the fingerprints of SSL certificates presented upon connection.
6 | """
7 | from requests.adapters import HTTPAdapter
8 |
9 | from .._compat import poolmanager
10 |
11 |
12 | class FingerprintAdapter(HTTPAdapter):
13 | """
14 | A HTTPS Adapter for Python Requests that verifies certificate fingerprints,
15 | instead of certificate hostnames.
16 |
17 | Example usage:
18 |
19 | .. code-block:: python
20 |
21 | import requests
22 | import ssl
23 | from requests_toolbelt.adapters.fingerprint import FingerprintAdapter
24 |
25 | twitter_fingerprint = '...'
26 | s = requests.Session()
27 | s.mount(
28 | 'https://twitter.com',
29 | FingerprintAdapter(twitter_fingerprint)
30 | )
31 |
32 | The fingerprint should be provided as a hexadecimal string, optionally
33 | containing colons.
34 | """
35 |
36 | __attrs__ = HTTPAdapter.__attrs__ + ['fingerprint']
37 |
38 | def __init__(self, fingerprint, **kwargs):
39 | self.fingerprint = fingerprint
40 |
41 | super(FingerprintAdapter, self).__init__(**kwargs)
42 |
43 | def init_poolmanager(self, connections, maxsize, block=False):
44 | self.poolmanager = poolmanager.PoolManager(
45 | num_pools=connections,
46 | maxsize=maxsize,
47 | block=block,
48 | assert_fingerprint=self.fingerprint)
49 |
--------------------------------------------------------------------------------
/app/third_party/urllib3/packages/backports/makefile.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | backports.makefile
4 | ~~~~~~~~~~~~~~~~~~
5 |
6 | Backports the Python 3 ``socket.makefile`` method for use with anything that
7 | wants to create a "fake" socket object.
8 | """
9 | import io
10 | from socket import SocketIO
11 |
12 |
13 | def backport_makefile(
14 | self, mode="r", buffering=None, encoding=None, errors=None, newline=None
15 | ):
16 | """
17 | Backport of ``socket.makefile`` from Python 3.5.
18 | """
19 | if not set(mode) <= {"r", "w", "b"}:
20 | raise ValueError("invalid mode %r (only r, w, b allowed)" % (mode,))
21 | writing = "w" in mode
22 | reading = "r" in mode or not writing
23 | assert reading or writing
24 | binary = "b" in mode
25 | rawmode = ""
26 | if reading:
27 | rawmode += "r"
28 | if writing:
29 | rawmode += "w"
30 | raw = SocketIO(self, rawmode)
31 | self._makefile_refs += 1
32 | if buffering is None:
33 | buffering = -1
34 | if buffering < 0:
35 | buffering = io.DEFAULT_BUFFER_SIZE
36 | if buffering == 0:
37 | if not binary:
38 | raise ValueError("unbuffered streams must be binary")
39 | return raw
40 | if reading and writing:
41 | buffer = io.BufferedRWPair(raw, raw, buffering)
42 | elif reading:
43 | buffer = io.BufferedReader(raw, buffering)
44 | else:
45 | assert writing
46 | buffer = io.BufferedWriter(raw, buffering)
47 | if binary:
48 | return buffer
49 | text = io.TextIOWrapper(buffer, encoding, errors, newline)
50 | text.mode = mode
51 | return text
52 |
--------------------------------------------------------------------------------
/app/third_party/idna-2.10.dist-info/LICENSE.rst:
--------------------------------------------------------------------------------
1 | License
2 | -------
3 |
4 | License: bsd-3-clause
5 |
6 | Copyright (c) 2013-2020, Kim Davies. All rights reserved.
7 |
8 | Redistribution and use in source and binary forms, with or without
9 | modification, are permitted provided that the following conditions are met:
10 |
11 | #. Redistributions of source code must retain the above copyright
12 | notice, this list of conditions and the following disclaimer.
13 |
14 | #. Redistributions in binary form must reproduce the above
15 | copyright notice, this list of conditions and the following
16 | disclaimer in the documentation and/or other materials provided with
17 | the distribution.
18 |
19 | #. Neither the name of the copyright holder nor the names of the
20 | contributors may be used to endorse or promote products derived
21 | from this software without specific prior written permission.
22 |
23 | #. THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS "AS IS" AND ANY
24 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
26 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
27 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
31 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
33 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
34 | DAMAGE.
35 |
--------------------------------------------------------------------------------
/app/third_party/requests_toolbelt/threaded/thread.py:
--------------------------------------------------------------------------------
1 | """Module containing the SessionThread class."""
2 | import threading
3 | import uuid
4 |
5 | import requests.exceptions as exc
6 |
7 | from .._compat import queue
8 |
9 |
10 | class SessionThread(object):
11 | def __init__(self, initialized_session, job_queue, response_queue,
12 | exception_queue):
13 | self._session = initialized_session
14 | self._jobs = job_queue
15 | self._create_worker()
16 | self._responses = response_queue
17 | self._exceptions = exception_queue
18 |
19 | def _create_worker(self):
20 | self._worker = threading.Thread(
21 | target=self._make_request,
22 | name=uuid.uuid4(),
23 | )
24 | self._worker.daemon = True
25 | self._worker._state = 0
26 | self._worker.start()
27 |
28 | def _handle_request(self, kwargs):
29 | try:
30 | response = self._session.request(**kwargs)
31 | except exc.RequestException as e:
32 | self._exceptions.put((kwargs, e))
33 | else:
34 | self._responses.put((kwargs, response))
35 | finally:
36 | self._jobs.task_done()
37 |
38 | def _make_request(self):
39 | while True:
40 | try:
41 | kwargs = self._jobs.get_nowait()
42 | except queue.Empty:
43 | break
44 |
45 | self._handle_request(kwargs)
46 |
47 | def is_alive(self):
48 | """Proxy to the thread's ``is_alive`` method."""
49 | return self._worker.is_alive()
50 |
51 | def join(self):
52 | """Join this thread to the master thread."""
53 | self._worker.join()
54 |
--------------------------------------------------------------------------------
/app/templates/mastofeeder/feed.atom:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ feed_url }}
4 |
5 |
6 | {{ feed_title }}
7 | {{ updated_date_iso }}Z
8 | {% comment %}
9 | We could put in a per-post author name with the Twitter user's screen name,
10 | but that would just duplicate information in the body. By only having a
11 | feed-level author, the feed is still valid, but Google Reader won't display
12 | a per-post author line.
13 | {% endcomment %}
14 |
15 | {{ APP_NAME }} : Masto Feeder
16 |
17 | {% for display_status in display_statuses %}
18 |
19 | {{ display_status.id }}
20 |
21 | {{ display_status.title_as_text }}
22 | {{ display_status.created_at_iso }}
23 | {{ display_status.updated_at_iso }}
24 |
25 | {% filter force_escape %}
26 |
27 | {% include 'status.snippet' %}
28 |
29 | {% endfilter %}
30 |
31 | {% if include_status_json %}
32 |
33 | {{ display_status.debug_json }}
34 |
35 | {% endif %}
36 |
37 | {% endfor %}
38 |
39 |
--------------------------------------------------------------------------------
/app/birdfeeder/handlers/pinger.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | import base.handlers
5 | from birdfeeder import data
6 | import birdfeeder.handlers.update
7 | from pingersecret import SECRET
8 |
9 | class FollowingHandler(base.handlers.BaseHandler):
10 | def get(self):
11 | if self.request.get('secret') != SECRET:
12 | self._write_input_error('Setec Astronomy')
13 | return
14 |
15 | self._write_json(data.FollowingData.get_following_list())
16 |
17 | class PingHandler(base.handlers.BaseHandler):
18 | def post(self):
19 | if self.request.get('secret') != SECRET:
20 | self._write_input_error('Setec Astronomy')
21 | return
22 |
23 | update_twitter_id = long(self.request.get('update_twitter_id'))
24 | update_status_id = long(self.request.get('update_status_id'))
25 |
26 | logging.info('Got ping for status %d by %d' % (
27 | update_status_id, update_twitter_id))
28 |
29 | following_twitter_ids = data.FollowingData.get_following_twitter_ids(
30 | update_twitter_id)
31 | task_count = 0
32 | for following_twitter_id in following_twitter_ids:
33 | logging.info('Queueing update for %d' % following_twitter_id)
34 | session = data.Session.get_by_twitter_id(str(following_twitter_id))
35 |
36 | if session:
37 | session.enqueue_update_task(
38 | countdown=birdfeeder.handlers.update.PING_UPDATE_DELAY_SEC,
39 | expected_status_id=update_status_id,
40 | update_retry_count=0)
41 | task_count += 1
42 | else:
43 | logging.info('Ignored ping due to missing session');
44 |
45 | self.response.out.write('Queued %d updates' % task_count)
46 |
--------------------------------------------------------------------------------
/app/templates/mastofeeder/status-group.snippet:
--------------------------------------------------------------------------------
1 | {% spaceless %}
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 | {{ status_group.author_display_name }}
15 |
17 | @{{ status_group.user.username }}
18 |
19 |
20 |
21 | {% for display_status in status_group.display_statuses %}
22 |
23 | {% if display_status.status.reblog %}
24 | {% with display_status.reblog_display_status as display_status %}
25 | ↺ boosted
26 | {% include 'account-link.snippet' %}
27 |
28 | {{ display_status.content_as_html|safe }}
29 | {% endwith %}
30 | {% else %}
31 | {{ display_status.content_as_html|safe }}
32 | {% endif %}
33 | {% include 'status-footer.snippet' %}
34 |
35 | {% endfor %}
36 |
37 |
38 |
39 | {% endspaceless %}
40 |
--------------------------------------------------------------------------------
/worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "worker",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite dev",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11 | "lint": "prettier --check . && eslint .",
12 | "format": "prettier --write .",
13 | "worker-dev": "wrangler dev",
14 | "worker-deploy": "wrangler deploy"
15 | },
16 | "devDependencies": {
17 | "@cloudflare/workers-types": "^4.20250719.0",
18 | "@sveltejs/adapter-cloudflare": "^7.1.0",
19 | "@sveltejs/kit": "^2.25.1",
20 | "@sveltejs/vite-plugin-svelte": "^6.1.0",
21 | "@types/eslint": "^8.56.12",
22 | "eslint": "^8.57.1",
23 | "eslint-config-prettier": "^9.1.2",
24 | "eslint-plugin-svelte": "^2.46.1",
25 | "globals": "^15.15.0",
26 | "prettier": "^3.6.2",
27 | "prettier-plugin-svelte": "^3.4.0",
28 | "svelte": "^5.36.12",
29 | "svelte-check": "^4.3.0",
30 | "tslib": "^2.8.1",
31 | "typescript": "^5.8.3",
32 | "typescript-eslint": "^7.18.0",
33 | "vite": "^7.0.5",
34 | "wrangler": "^4.25.0"
35 | },
36 | "type": "module",
37 | "prettier": {
38 | "trailingComma": "es5",
39 | "bracketSameLine": true,
40 | "bracketSpacing": false,
41 | "tabWidth": 4,
42 | "semi": true,
43 | "singleQuote": false,
44 | "quoteProps": "preserve",
45 | "arrowParens": "avoid"
46 | },
47 | "dependencies": {
48 | "htmlparser2": "^10.0.0",
49 | "masto": "^7.5.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/templates/birdfeeder/index-signed-in.html:
--------------------------------------------------------------------------------
1 | {% extends "base/page.html" %}
2 |
3 | {% block title %}{{ APP_NAME}} : Bird Feeder{% endblock %}
4 | {% block subtitle %}Bird Feeder{% endblock %}
5 |
6 | {% block intro %}
7 | {% include "intro.snippet" %}
8 |
9 | You're signed in as {{ twitter_user.screen_name}}.
10 | Sign out.
11 | {% endblock %}
12 |
13 | {% block body %}
14 |
15 |
16 | {% if allows_feed_updates %}
17 | @{{ twitter_user.screen_name }} timeline feed
18 | {% endif %}
19 |
20 |
21 |
22 | @{{ twitter_user.screen_name }} Twitter data backups:
23 |
17 | Your @{{ mastodon_user.username }} timeline feed 18 | is ready. You can subscribe to the URL in your preferred feed reader. 19 |
20 | 21 | {% if lists_and_feed_paths %} 22 |You can also subscribe to feeds for your lists:
23 |You don't have any lists, but if you did, you could also subscribe to feeds for them.
30 | {% endif %} 31 | 32 |34 | You can also get digest feeds for your timeline or lists. These are feeds 35 | that contains all posts from the past day (UTC); At midnight, a new set of 36 | posts rolls over. This way you can avoid the distraction of 37 | constantly-updated feeds, but still not miss anything. 38 |
39 |5 | {{ APP_NAME }} is a collection of tools that let you keep up with 6 | the "real-time web" better (think 7 | flow control). 8 | Instead of living in fear of missing something, these tools let you consume 9 | information at your desired pace in efficient batches. 10 |
11 | {% endblock %} 12 | 13 | {% block body %} 14 | 20 | 21 | {% endblock %} 22 | 23 | {% block footer %} 24 |25 | Retired tools: 26 |
27 | 28 |
45 | {{ APP_NAME }} was created by Mihai
46 | Parparita, who can be reached at . To keep
47 | up with {{ APP_NAME }} developments, you can
48 |
follow us on Twitter.
49 |
52 | Source is available 53 | on GitHub. 54 |
55 | 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /app/third_party/requests/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | requests.compat 5 | ~~~~~~~~~~~~~~~ 6 | 7 | This module handles import compatibility issues between Python 2 and 8 | Python 3. 9 | """ 10 | 11 | try: 12 | import chardet 13 | except ImportError: 14 | import charset_normalizer as chardet 15 | 16 | import sys 17 | 18 | # ------- 19 | # Pythons 20 | # ------- 21 | 22 | # Syntax sugar. 23 | _ver = sys.version_info 24 | 25 | #: Python 2.x? 26 | is_py2 = (_ver[0] == 2) 27 | 28 | #: Python 3.x? 29 | is_py3 = (_ver[0] == 3) 30 | 31 | has_simplejson = False 32 | try: 33 | import simplejson as json 34 | has_simplejson = True 35 | except ImportError: 36 | import json 37 | 38 | # --------- 39 | # Specifics 40 | # --------- 41 | 42 | if is_py2: 43 | from urllib import ( 44 | quote, unquote, quote_plus, unquote_plus, urlencode, getproxies, 45 | proxy_bypass, proxy_bypass_environment, getproxies_environment) 46 | from urlparse import urlparse, urlunparse, urljoin, urlsplit, urldefrag 47 | from urllib2 import parse_http_list 48 | import cookielib 49 | from Cookie import Morsel 50 | from StringIO import StringIO 51 | # Keep OrderedDict for backwards compatibility. 52 | from collections import Callable, Mapping, MutableMapping, OrderedDict 53 | 54 | builtin_str = str 55 | bytes = str 56 | str = unicode 57 | basestring = basestring 58 | numeric_types = (int, long, float) 59 | integer_types = (int, long) 60 | JSONDecodeError = ValueError 61 | 62 | elif is_py3: 63 | from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag 64 | from urllib.request import parse_http_list, getproxies, proxy_bypass, proxy_bypass_environment, getproxies_environment 65 | from http import cookiejar as cookielib 66 | from http.cookies import Morsel 67 | from io import StringIO 68 | # Keep OrderedDict for backwards compatibility. 69 | from collections import OrderedDict 70 | from collections.abc import Callable, Mapping, MutableMapping 71 | if has_simplejson: 72 | from simplejson import JSONDecodeError 73 | else: 74 | from json import JSONDecodeError 75 | 76 | builtin_str = str 77 | str = str 78 | bytes = bytes 79 | basestring = (str, bytes) 80 | numeric_types = (int, float) 81 | integer_types = (int,) 82 | -------------------------------------------------------------------------------- /app/third_party/requests-2.27.1.dist-info/RECORD: -------------------------------------------------------------------------------- 1 | requests-2.27.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 2 | requests-2.27.1.dist-info/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142 3 | requests-2.27.1.dist-info/METADATA,sha256=fxIjGa_S2RlpD4oFJGdSuXQWFUGxjcgAhLYF9HwRp8Q,4984 4 | requests-2.27.1.dist-info/RECORD,, 5 | requests-2.27.1.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110 6 | requests-2.27.1.dist-info/top_level.txt,sha256=fMSVmHfb5rbGOo6xv-O_tUX6j-WyixssE-SnwcDRxNQ,9 7 | requests/__init__.py,sha256=BgNBrAYr3DdRWDtkc1IwoSraVBBz1712c_eX4G8h-ak,4924 8 | requests/__init__.pyc,, 9 | requests/__version__.py,sha256=q8miOQaomOv3S74lK4eQs1zZ5jwcnOusyEU-M2idhts,441 10 | requests/__version__.pyc,, 11 | requests/_internal_utils.py,sha256=Zx3PnEUccyfsB-ie11nZVAW8qClJy0gx1qNME7rgT18,1096 12 | requests/_internal_utils.pyc,, 13 | requests/adapters.py,sha256=YJf_0S2JL5fEs6yPKC3iQ-Iu0UZxDb9H6W5ipVBrIXE,21645 14 | requests/adapters.pyc,, 15 | requests/api.py,sha256=hjuoP79IAEmX6Dysrw8t032cLfwLHxbI_wM4gC5G9t0,6402 16 | requests/api.pyc,, 17 | requests/auth.py,sha256=OMoJIVKyRLy9THr91y8rxysZuclwPB-K1Xg1zBomUhQ,10207 18 | requests/auth.pyc,, 19 | requests/certs.py,sha256=dOB5rV2DZ13dEhq9BUa_4hd5kAqg59e_zUZB00faYz8,453 20 | requests/certs.pyc,, 21 | requests/compat.py,sha256=wy3bUOq8aKOE7mMZiLJJMXFCaXbhoS-baON60EDvAIg,2054 22 | requests/compat.pyc,, 23 | requests/cookies.py,sha256=Y-bKX6TvW3FnYlE6Au0SXtVVWcaNdFvuAwQxw-G0iTI,18430 24 | requests/cookies.pyc,, 25 | requests/exceptions.py,sha256=VUKyfNZmXjsjkPgipZupHfkcE3OVdYKx8GqfvWckfwA,3434 26 | requests/exceptions.pyc,, 27 | requests/help.py,sha256=ywPNssPohrch_Q_q4_JLJM1z2bP0TirHkA9QnoOF0sY,3968 28 | requests/help.pyc,, 29 | requests/hooks.py,sha256=QReGyy0bRcr5rkwCuObNakbYsc7EkiKeBwG4qHekr2Q,757 30 | requests/hooks.pyc,, 31 | requests/models.py,sha256=RfXfgGUZ5X6CWUhUODku7-MOQNWVs7lDZPV6bDC53cY,35051 32 | requests/models.pyc,, 33 | requests/packages.py,sha256=kr9J9dYZr9Ef4JmwHbCEUgwViwcCyOUPgfXZvIL84Os,932 34 | requests/packages.pyc,, 35 | requests/sessions.py,sha256=Zu-Y9YPlwTIsyFx1hvIrc3ziyeFpuFPqcOuSuz8BNWs,29835 36 | requests/sessions.pyc,, 37 | requests/status_codes.py,sha256=gT79Pbs_cQjBgp-fvrUgg1dn2DQO32bDj4TInjnMPSc,4188 38 | requests/status_codes.pyc,, 39 | requests/structures.py,sha256=msAtr9mq1JxHd-JRyiILfdFlpbJwvvFuP3rfUQT_QxE,3005 40 | requests/structures.pyc,, 41 | requests/utils.py,sha256=MKTK3du_WmmO2nv_SkeV880VwfIYhJbvd1Lz7uDioP8,33277 42 | requests/utils.pyc,, 43 | -------------------------------------------------------------------------------- /app/third_party/certifi/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | certifi.py 5 | ~~~~~~~~~~ 6 | 7 | This module returns the installation location of cacert.pem or its contents. 8 | """ 9 | import os 10 | 11 | try: 12 | from importlib.resources import path as get_path, read_text 13 | 14 | _CACERT_CTX = None 15 | _CACERT_PATH = None 16 | 17 | def where(): 18 | # This is slightly terrible, but we want to delay extracting the file 19 | # in cases where we're inside of a zipimport situation until someone 20 | # actually calls where(), but we don't want to re-extract the file 21 | # on every call of where(), so we'll do it once then store it in a 22 | # global variable. 23 | global _CACERT_CTX 24 | global _CACERT_PATH 25 | if _CACERT_PATH is None: 26 | # This is slightly janky, the importlib.resources API wants you to 27 | # manage the cleanup of this file, so it doesn't actually return a 28 | # path, it returns a context manager that will give you the path 29 | # when you enter it and will do any cleanup when you leave it. In 30 | # the common case of not needing a temporary file, it will just 31 | # return the file system location and the __exit__() is a no-op. 32 | # 33 | # We also have to hold onto the actual context manager, because 34 | # it will do the cleanup whenever it gets garbage collected, so 35 | # we will also store that at the global level as well. 36 | _CACERT_CTX = get_path("certifi", "cacert.pem") 37 | _CACERT_PATH = str(_CACERT_CTX.__enter__()) 38 | 39 | return _CACERT_PATH 40 | 41 | 42 | except ImportError: 43 | # This fallback will work for Python versions prior to 3.7 that lack the 44 | # importlib.resources module but relies on the existing `where` function 45 | # so won't address issues with environments like PyOxidizer that don't set 46 | # __file__ on modules. 47 | def read_text(_module, _path, encoding="ascii"): 48 | with open(where(), "r", encoding=encoding) as data: 49 | return data.read() 50 | 51 | # If we don't have importlib.resources, then we will just do the old logic 52 | # of assuming we're on the filesystem and munge the path directly. 53 | def where(): 54 | f = os.path.dirname(__file__) 55 | 56 | return os.path.join(f, "cacert.pem") 57 | 58 | 59 | def contents(): 60 | return read_text("certifi", "cacert.pem", encoding="ascii") 61 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/sessions.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from ._compat import urljoin 4 | 5 | 6 | class BaseUrlSession(requests.Session): 7 | """A Session with a URL that all requests will use as a base. 8 | 9 | Let's start by looking at an example: 10 | 11 | .. code-block:: python 12 | 13 | >>> from requests_toolbelt import sessions 14 | >>> s = sessions.BaseUrlSession( 15 | ... base_url='https://example.com/resource/') 16 | >>> r = s.get('sub-resource/' params={'foo': 'bar'}) 17 | >>> print(r.request.url) 18 | https://example.com/resource/sub-resource/?foo=bar 19 | 20 | Our call to the ``get`` method will make a request to the URL passed in 21 | when we created the Session and the partial resource name we provide. 22 | 23 | We implement this by overriding the ``request`` method so most uses of a 24 | Session are covered. (This, however, precludes the use of PreparedRequest 25 | objects). 26 | 27 | .. note:: 28 | 29 | The base URL that you provide and the path you provide are **very** 30 | important. 31 | 32 | Let's look at another *similar* example 33 | 34 | .. code-block:: python 35 | 36 | >>> from requests_toolbelt import sessions 37 | >>> s = sessions.BaseUrlSession( 38 | ... base_url='https://example.com/resource/') 39 | >>> r = s.get('/sub-resource/' params={'foo': 'bar'}) 40 | >>> print(r.request.url) 41 | https://example.com/sub-resource/?foo=bar 42 | 43 | The key difference here is that we called ``get`` with ``/sub-resource/``, 44 | i.e., there was a leading ``/``. This changes how we create the URL 45 | because we rely on :mod:`urllib.parse.urljoin`. 46 | 47 | To override how we generate the URL, sub-class this method and override the 48 | ``create_url`` method. 49 | 50 | Based on implementation from 51 | https://github.com/kennethreitz/requests/issues/2554#issuecomment-109341010 52 | """ 53 | 54 | base_url = None 55 | 56 | def __init__(self, base_url=None): 57 | if base_url: 58 | self.base_url = base_url 59 | super(BaseUrlSession, self).__init__() 60 | 61 | def request(self, method, url, *args, **kwargs): 62 | """Send the request after generating the complete URL.""" 63 | url = self.create_url(url) 64 | return super(BaseUrlSession, self).request( 65 | method, url, *args, **kwargs 66 | ) 67 | 68 | def create_url(self, url): 69 | """Create the URL based off this partial path.""" 70 | return urljoin(self.base_url, url) 71 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/adapters/ssl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | requests_toolbelt.ssl_adapter 5 | ============================= 6 | 7 | This file contains an implementation of the SSLAdapter originally demonstrated 8 | in this blog post: 9 | https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/ 10 | 11 | """ 12 | import requests 13 | 14 | from requests.adapters import HTTPAdapter 15 | 16 | from .._compat import poolmanager 17 | 18 | 19 | class SSLAdapter(HTTPAdapter): 20 | """ 21 | A HTTPS Adapter for Python Requests that allows the choice of the SSL/TLS 22 | version negotiated by Requests. This can be used either to enforce the 23 | choice of high-security TLS versions (where supported), or to work around 24 | misbehaving servers that fail to correctly negotiate the default TLS 25 | version being offered. 26 | 27 | Example usage: 28 | 29 | >>> import requests 30 | >>> import ssl 31 | >>> from requests_toolbelt import SSLAdapter 32 | >>> s = requests.Session() 33 | >>> s.mount('https://', SSLAdapter(ssl.PROTOCOL_TLSv1)) 34 | 35 | You can replace the chosen protocol with any that are available in the 36 | default Python SSL module. All subsequent requests that match the adapter 37 | prefix will use the chosen SSL version instead of the default. 38 | 39 | This adapter will also attempt to change the SSL/TLS version negotiated by 40 | Requests when using a proxy. However, this may not always be possible: 41 | prior to Requests v2.4.0 the adapter did not have access to the proxy setup 42 | code. In earlier versions of Requests, this adapter will not function 43 | properly when used with proxies. 44 | """ 45 | 46 | __attrs__ = HTTPAdapter.__attrs__ + ['ssl_version'] 47 | 48 | def __init__(self, ssl_version=None, **kwargs): 49 | self.ssl_version = ssl_version 50 | 51 | super(SSLAdapter, self).__init__(**kwargs) 52 | 53 | def init_poolmanager(self, connections, maxsize, block=False): 54 | self.poolmanager = poolmanager.PoolManager( 55 | num_pools=connections, 56 | maxsize=maxsize, 57 | block=block, 58 | ssl_version=self.ssl_version) 59 | 60 | if requests.__build__ >= 0x020400: 61 | # Earlier versions of requests either don't have this method or, worse, 62 | # don't allow passing arbitrary keyword arguments. As a result, only 63 | # conditionally define this method. 64 | def proxy_manager_for(self, *args, **kwargs): 65 | kwargs['ssl_version'] = self.ssl_version 66 | return super(SSLAdapter, self).proxy_manager_for(*args, **kwargs) 67 | -------------------------------------------------------------------------------- /app/third_party/dateutil/zoneinfo/rebuild.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import tempfile 4 | import shutil 5 | import json 6 | from subprocess import check_call, check_output 7 | from tarfile import TarFile 8 | 9 | from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME 10 | 11 | 12 | def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): 13 | """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* 14 | 15 | filename is the timezone tarball from ``ftp.iana.org/tz``. 16 | 17 | """ 18 | tmpdir = tempfile.mkdtemp() 19 | zonedir = os.path.join(tmpdir, "zoneinfo") 20 | moduledir = os.path.dirname(__file__) 21 | try: 22 | with TarFile.open(filename) as tf: 23 | for name in zonegroups: 24 | tf.extract(name, tmpdir) 25 | filepaths = [os.path.join(tmpdir, n) for n in zonegroups] 26 | 27 | _run_zic(zonedir, filepaths) 28 | 29 | # write metadata file 30 | with open(os.path.join(zonedir, METADATA_FN), 'w') as f: 31 | json.dump(metadata, f, indent=4, sort_keys=True) 32 | target = os.path.join(moduledir, ZONEFILENAME) 33 | with TarFile.open(target, "w:%s" % format) as tf: 34 | for entry in os.listdir(zonedir): 35 | entrypath = os.path.join(zonedir, entry) 36 | tf.add(entrypath, entry) 37 | finally: 38 | shutil.rmtree(tmpdir) 39 | 40 | 41 | def _run_zic(zonedir, filepaths): 42 | """Calls the ``zic`` compiler in a compatible way to get a "fat" binary. 43 | 44 | Recent versions of ``zic`` default to ``-b slim``, while older versions 45 | don't even have the ``-b`` option (but default to "fat" binaries). The 46 | current version of dateutil does not support Version 2+ TZif files, which 47 | causes problems when used in conjunction with "slim" binaries, so this 48 | function is used to ensure that we always get a "fat" binary. 49 | """ 50 | 51 | try: 52 | help_text = check_output(["zic", "--help"]) 53 | except OSError as e: 54 | _print_on_nosuchfile(e) 55 | raise 56 | 57 | if b"-b " in help_text: 58 | bloat_args = ["-b", "fat"] 59 | else: 60 | bloat_args = [] 61 | 62 | check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths) 63 | 64 | 65 | def _print_on_nosuchfile(e): 66 | """Print helpful troubleshooting message 67 | 68 | e is an exception raised by subprocess.check_call() 69 | 70 | """ 71 | if e.errno == 2: 72 | logging.error( 73 | "Could not find zic. Perhaps you need to install " 74 | "libc-bin or some other package that provides it, " 75 | "or it's not in your PATH?") 76 | -------------------------------------------------------------------------------- /app/third_party/python_dateutil-2.8.2.dist-info/RECORD: -------------------------------------------------------------------------------- 1 | dateutil/__init__.py,sha256=lXElASqwYGwqlrSWSeX19JwF5Be9tNecDa9ebk-0gmk,222 2 | dateutil/__init__.pyc,, 3 | dateutil/_common.py,sha256=77w0yytkrxlYbSn--lDVPUMabUXRR9I3lBv_vQRUqUY,932 4 | dateutil/_common.pyc,, 5 | dateutil/_version.py,sha256=awyHv2PYvDR84dxjrHyzmm8nieFwMjcuuShPh-QNkM4,142 6 | dateutil/_version.pyc,, 7 | dateutil/easter.py,sha256=dyBi-lKvimH1u_k6p7Z0JJK72QhqVtVBsqByvpEPKvc,2678 8 | dateutil/easter.pyc,, 9 | dateutil/parser/__init__.py,sha256=wWk6GFuxTpjoggCGtgkceJoti4pVjl4_fHQXpNOaSYg,1766 10 | dateutil/parser/__init__.pyc,, 11 | dateutil/parser/_parser.py,sha256=7klDdyicksQB_Xgl-3UAmBwzCYor1AIZqklIcT6dH_8,58796 12 | dateutil/parser/_parser.pyc,, 13 | dateutil/parser/isoparser.py,sha256=EtLY7w22HWx-XJpTWxJD3XNs6LBHRCps77tCdLnYad8,13247 14 | dateutil/parser/isoparser.pyc,, 15 | dateutil/relativedelta.py,sha256=GjVxqpAVWnG67rdbf7pkoIlJvQqmju9NSfGCcqblc7U,24904 16 | dateutil/relativedelta.pyc,, 17 | dateutil/rrule.py,sha256=b6GVV4MpZDbBhJ5qitQKRyx8-_OKyeAbk57or2A8AYU,66556 18 | dateutil/rrule.pyc,, 19 | dateutil/tz/__init__.py,sha256=F-Mz13v6jYseklQf9Te9J6nzcLDmq47gORa61K35_FA,444 20 | dateutil/tz/__init__.pyc,, 21 | dateutil/tz/_common.py,sha256=cgzDTANsOXvEc86cYF77EsliuSab8Puwpsl5-bX3_S4,12977 22 | dateutil/tz/_common.pyc,, 23 | dateutil/tz/_factories.py,sha256=unb6XQNXrPMveksTCU-Ag8jmVZs4SojoPUcAHpWnrvU,2569 24 | dateutil/tz/_factories.pyc,, 25 | dateutil/tz/tz.py,sha256=JotVjDcF16hzoouQ0kZW-5mCYu7Xj67NI-VQgnWapKE,62857 26 | dateutil/tz/tz.pyc,, 27 | dateutil/tz/win.py,sha256=xJszWgSwE1xPx_HJj4ZkepyukC_hNy016WMcXhbRaB8,12935 28 | dateutil/tz/win.pyc,, 29 | dateutil/tzwin.py,sha256=7Ar4vdQCnnM0mKR3MUjbIKsZrBVfHgdwsJZc_mGYRew,59 30 | dateutil/tzwin.pyc,, 31 | dateutil/utils.py,sha256=dKCchEw8eObi0loGTx91unBxm_7UGlU3v_FjFMdqwYM,1965 32 | dateutil/utils.pyc,, 33 | dateutil/zoneinfo/__init__.py,sha256=KYg0pthCMjcp5MXSEiBJn3nMjZeNZav7rlJw5-tz1S4,5889 34 | dateutil/zoneinfo/__init__.pyc,, 35 | dateutil/zoneinfo/dateutil-zoneinfo.tar.gz,sha256=AkcdBx3XkEZwMSpS_TmOEfrEFHLvgxPNDVIwGVxTVaI,174394 36 | dateutil/zoneinfo/rebuild.py,sha256=MiqYzCIHvNbMH-LdRYLv-4T0EIA7hDKt5GLR0IRTLdI,2392 37 | dateutil/zoneinfo/rebuild.pyc,, 38 | python_dateutil-2.8.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 39 | python_dateutil-2.8.2.dist-info/LICENSE,sha256=ugD1Gg2SgjtaHN4n2LW50jIeZ-2NqbwWPv-W1eF-V34,2889 40 | python_dateutil-2.8.2.dist-info/METADATA,sha256=RDHtGo7BnYRjmYxot_wlu_W3N2CyvPtvchbtyIlKKPA,8218 41 | python_dateutil-2.8.2.dist-info/RECORD,, 42 | python_dateutil-2.8.2.dist-info/WHEEL,sha256=Z-nyYpwrcSqxfdux5Mbn_DQ525iP7J2DG3JgGvOYyTQ,110 43 | python_dateutil-2.8.2.dist-info/top_level.txt,sha256=4tjdWkhRZvF7LA_BYe_L9gB2w_p2a-z5y6ArjaRkot8,9 44 | python_dateutil-2.8.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 45 | -------------------------------------------------------------------------------- /app/third_party/urllib3/filepost.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import binascii 4 | import codecs 5 | import os 6 | from io import BytesIO 7 | 8 | from .fields import RequestField 9 | from .packages import six 10 | from .packages.six import b 11 | 12 | writer = codecs.lookup("utf-8")[3] 13 | 14 | 15 | def choose_boundary(): 16 | """ 17 | Our embarrassingly-simple replacement for mimetools.choose_boundary. 18 | """ 19 | boundary = binascii.hexlify(os.urandom(16)) 20 | if not six.PY2: 21 | boundary = boundary.decode("ascii") 22 | return boundary 23 | 24 | 25 | def iter_field_objects(fields): 26 | """ 27 | Iterate over fields. 28 | 29 | Supports list of (k, v) tuples and dicts, and lists of 30 | :class:`~urllib3.fields.RequestField`. 31 | 32 | """ 33 | if isinstance(fields, dict): 34 | i = six.iteritems(fields) 35 | else: 36 | i = iter(fields) 37 | 38 | for field in i: 39 | if isinstance(field, RequestField): 40 | yield field 41 | else: 42 | yield RequestField.from_tuples(*field) 43 | 44 | 45 | def iter_fields(fields): 46 | """ 47 | .. deprecated:: 1.6 48 | 49 | Iterate over fields. 50 | 51 | The addition of :class:`~urllib3.fields.RequestField` makes this function 52 | obsolete. Instead, use :func:`iter_field_objects`, which returns 53 | :class:`~urllib3.fields.RequestField` objects. 54 | 55 | Supports list of (k, v) tuples and dicts. 56 | """ 57 | if isinstance(fields, dict): 58 | return ((k, v) for k, v in six.iteritems(fields)) 59 | 60 | return ((k, v) for k, v in fields) 61 | 62 | 63 | def encode_multipart_formdata(fields, boundary=None): 64 | """ 65 | Encode a dictionary of ``fields`` using the multipart/form-data MIME format. 66 | 67 | :param fields: 68 | Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). 69 | 70 | :param boundary: 71 | If not specified, then a random boundary will be generated using 72 | :func:`urllib3.filepost.choose_boundary`. 73 | """ 74 | body = BytesIO() 75 | if boundary is None: 76 | boundary = choose_boundary() 77 | 78 | for field in iter_field_objects(fields): 79 | body.write(b("--%s\r\n" % (boundary))) 80 | 81 | writer(body).write(field.render_headers()) 82 | data = field.data 83 | 84 | if isinstance(data, int): 85 | data = str(data) # Backwards compatibility 86 | 87 | if isinstance(data, six.text_type): 88 | writer(body).write(data) 89 | else: 90 | body.write(data) 91 | 92 | body.write(b"\r\n") 93 | 94 | body.write(b("--%s--\r\n" % (boundary))) 95 | 96 | content_type = str("multipart/form-data; boundary=%s" % boundary) 97 | 98 | return body.getvalue(), content_type 99 | -------------------------------------------------------------------------------- /app/third_party/requests_toolbelt/adapters/source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | requests_toolbelt.source_adapter 4 | ================================ 5 | 6 | This file contains an implementation of the SourceAddressAdapter originally 7 | demonstrated on the Requests GitHub page. 8 | """ 9 | from requests.adapters import HTTPAdapter 10 | 11 | from .._compat import poolmanager, basestring 12 | 13 | 14 | class SourceAddressAdapter(HTTPAdapter): 15 | """ 16 | A Source Address Adapter for Python Requests that enables you to choose the 17 | local address to bind to. This allows you to send your HTTP requests from a 18 | specific interface and IP address. 19 | 20 | Two address formats are accepted. The first is a string: this will set the 21 | local IP address to the address given in the string, and will also choose a 22 | semi-random high port for the local port number. 23 | 24 | The second is a two-tuple of the form (ip address, port): for example, 25 | ``('10.10.10.10', 8999)``. This will set the local IP address to the first 26 | element, and the local port to the second element. If ``0`` is used as the 27 | port number, a semi-random high port will be selected. 28 | 29 | .. warning:: Setting an explicit local port can have negative interactions 30 | with connection-pooling in Requests: in particular, it risks 31 | the possibility of getting "Address in use" errors. The 32 | string-only argument is generally preferred to the tuple-form. 33 | 34 | Example usage: 35 | 36 | .. code-block:: python 37 | 38 | import requests 39 | from requests_toolbelt.adapters.source import SourceAddressAdapter 40 | 41 | s = requests.Session() 42 | s.mount('http://', SourceAddressAdapter('10.10.10.10')) 43 | s.mount('https://', SourceAddressAdapter(('10.10.10.10', 8999))) 44 | """ 45 | def __init__(self, source_address, **kwargs): 46 | if isinstance(source_address, basestring): 47 | self.source_address = (source_address, 0) 48 | elif isinstance(source_address, tuple): 49 | self.source_address = source_address 50 | else: 51 | raise TypeError( 52 | "source_address must be IP address string or (ip, port) tuple" 53 | ) 54 | 55 | super(SourceAddressAdapter, self).__init__(**kwargs) 56 | 57 | def init_poolmanager(self, connections, maxsize, block=False): 58 | self.poolmanager = poolmanager.PoolManager( 59 | num_pools=connections, 60 | maxsize=maxsize, 61 | block=block, 62 | source_address=self.source_address) 63 | 64 | def proxy_manager_for(self, *args, **kwargs): 65 | kwargs['source_address'] = self.source_address 66 | return super(SourceAddressAdapter, self).proxy_manager_for( 67 | *args, **kwargs) 68 | -------------------------------------------------------------------------------- /app/third_party/dateutil/tz/_factories.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import weakref 3 | from collections import OrderedDict 4 | 5 | from six.moves import _thread 6 | 7 | 8 | class _TzSingleton(type): 9 | def __init__(cls, *args, **kwargs): 10 | cls.__instance = None 11 | super(_TzSingleton, cls).__init__(*args, **kwargs) 12 | 13 | def __call__(cls): 14 | if cls.__instance is None: 15 | cls.__instance = super(_TzSingleton, cls).__call__() 16 | return cls.__instance 17 | 18 | 19 | class _TzFactory(type): 20 | def instance(cls, *args, **kwargs): 21 | """Alternate constructor that returns a fresh instance""" 22 | return type.__call__(cls, *args, **kwargs) 23 | 24 | 25 | class _TzOffsetFactory(_TzFactory): 26 | def __init__(cls, *args, **kwargs): 27 | cls.__instances = weakref.WeakValueDictionary() 28 | cls.__strong_cache = OrderedDict() 29 | cls.__strong_cache_size = 8 30 | 31 | cls._cache_lock = _thread.allocate_lock() 32 | 33 | def __call__(cls, name, offset): 34 | if isinstance(offset, timedelta): 35 | key = (name, offset.total_seconds()) 36 | else: 37 | key = (name, offset) 38 | 39 | instance = cls.__instances.get(key, None) 40 | if instance is None: 41 | instance = cls.__instances.setdefault(key, 42 | cls.instance(name, offset)) 43 | 44 | # This lock may not be necessary in Python 3. See GH issue #901 45 | with cls._cache_lock: 46 | cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) 47 | 48 | # Remove an item if the strong cache is overpopulated 49 | if len(cls.__strong_cache) > cls.__strong_cache_size: 50 | cls.__strong_cache.popitem(last=False) 51 | 52 | return instance 53 | 54 | 55 | class _TzStrFactory(_TzFactory): 56 | def __init__(cls, *args, **kwargs): 57 | cls.__instances = weakref.WeakValueDictionary() 58 | cls.__strong_cache = OrderedDict() 59 | cls.__strong_cache_size = 8 60 | 61 | cls.__cache_lock = _thread.allocate_lock() 62 | 63 | def __call__(cls, s, posix_offset=False): 64 | key = (s, posix_offset) 65 | instance = cls.__instances.get(key, None) 66 | 67 | if instance is None: 68 | instance = cls.__instances.setdefault(key, 69 | cls.instance(s, posix_offset)) 70 | 71 | # This lock may not be necessary in Python 3. See GH issue #901 72 | with cls.__cache_lock: 73 | cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) 74 | 75 | # Remove an item if the strong cache is overpopulated 76 | if len(cls.__strong_cache) > cls.__strong_cache_size: 77 | cls.__strong_cache.popitem(last=False) 78 | 79 | return instance 80 | 81 | -------------------------------------------------------------------------------- /app/templates/base/status-group.snippet: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 |{{ status.AsOriginalJsonString }}
45 | {% endif %}
46 | {% endspaceless %}
47 | This is a backup of {{ status_groups|length }} tweets. It is also available as JSON.
101 | {% endblock %} 102 | 103 | {% block body %} 104 | 105 |This {{ APP_NAME }} tool lets you read 12 | Twitter updates in a more manageable 13 | fashion. Just pick the Twitter list or usernames you'd like to generate a 14 | digest for, and you will see all updates made by them during the past full 15 | day (GMT). At midnight, a new set of updates rolls over.
16 | 17 |The digest is available as both a webpage that you can 18 | visit periodically, or as an Atom feed that you subscribe to get updates 19 | (grouped as one feed item) in your 20 | favorite feed reader.
21 | 22 |To get started, either choose a 23 | Twitter list or 24 | enter one or more Twitter usernames below (one username per line) and use 25 | the links that will appear at the bottom.
26 | {% endblock %} 27 | 28 | {% block body %} 29 || 33 | 40 | | 41 |or | 42 |43 | 53 | | 54 |
85 | Keep in mind that digests can only be created for public Twitter accounts and 86 | lists. 87 | 88 |
89 | {% endblock %} 90 | -------------------------------------------------------------------------------- /app/third_party/python_dateutil-2.8.2.dist-info/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017- Paul Ganssle