-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathapp.py
127 lines (104 loc) · 4.57 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""Fetches Bluesky timeline, converts it to Atom, and serves it."""
import datetime
import logging
from urllib.parse import urljoin
from cachetools import cachedmethod, LRUCache
from cachetools.keys import hashkey
from flask import Flask, render_template, request
import flask_gae_static
from google.cloud import ndb
from granary import as1, atom, bluesky
from granary.bluesky import Bluesky
from oauth_dropins.webutil import appengine_config, appengine_info, flask_util, util
from oauth_dropins.webutil.models import JsonProperty
from requests.exceptions import HTTPError
# access tokens currently expire in 2h, refresh tokens expire in 90d
# https://github.com/bluesky-social/atproto/blob/5b0c2d7dd533711c17202cd61c0e101ef3a81971/packages/pds/src/auth.ts#L46
# https://github.com/bluesky-social/atproto/blob/5b0c2d7dd533711c17202cd61c0e101ef3a81971/packages/pds/src/auth.ts#L65
TOKEN_EXPIRATION = datetime.timedelta(hours=2)
# Flask app
app = Flask('bluesky-atom', static_folder=None)
app.template_folder = './templates'
app.config.from_mapping(
ENV='development' if appengine_info.DEBUG else 'production',
CACHE_TYPE='NullCache' if appengine_info.DEBUG else 'SimpleCache',
SECRET_KEY=util.read('flask_secret_key'),
)
app.after_request(flask_util.default_modern_headers)
app.register_error_handler(Exception, flask_util.handle_exception)
if appengine_info.DEBUG or appengine_info.LOCAL_SERVER:
flask_gae_static.init_app(app)
app.wsgi_app = flask_util.ndb_context_middleware(
app.wsgi_app, client=appengine_config.ndb_client)
bluesky_cache = LRUCache(maxsize=1000)
class Feed(ndb.Model):
handle = ndb.StringProperty(required=True)
password = ndb.StringProperty(required=True)
session = JsonProperty(default={})
# cache Bluesky instances to reuse access/refresh tokens
@cachedmethod(lambda self: bluesky_cache,
key=lambda self: hashkey(self.handle, self.password))
def bluesky(self):
def store_session(session):
logging.info(f'Storing Bluesky session for {self.handle}: {session}')
self.session = session
self.put()
return Bluesky(handle=self.handle, app_password=self.password,
access_token=self.session.get('accessJwt'),
refresh_token=self.session.get('refreshJwt'),
session_callback=store_session)
def get_bool_param(name):
val = request.values.get(name)
return val and val.strip().lower() not in ['false', 'no', 'off']
@app.get('/')
@flask_util.headers({'Cache-Control': 'public, max-age=86400'})
def home():
return render_template('index.html')
@app.get('/feed')
@flask_util.headers({'Cache-Control': 'public, max-age=300'})
def feed():
feed_id = flask_util.get_required_param('feed_id').strip()
if not util.is_int(feed_id):
flask_util.error(f'Expected integer feed_id; got {feed_id}')
feed = Feed.get_by_id(int(feed_id))
if not feed:
flask_util.error(f'Feed {feed_id} not found')
activities = []
for a in feed.bluesky().get_activities():
type = as1.object_type(a)
if type in ('post', 'update'):
type = as1.object_type(as1.get_object(a))
if ((get_bool_param('replies') or type != 'comment')
and (get_bool_param('reposts') or type != 'share')):
activities.append(a)
logging.info(f'Got {len(activities)} activities')
# Generate output
return atom.activities_to_atom(
activities, {}, title='bluesky-atom feed',
host_url=request.host_url,
request_url=request.url,
xml_base=Bluesky.BASE_URL,
), {'Content-Type': 'application/atom+xml'}
@app.post('/generate')
def generate():
handle = flask_util.get_required_param('handle').strip().lower()
password = flask_util.get_required_param('password').strip()
feed = Feed.query(Feed.handle == handle, Feed.password == password).get()
if not feed:
feed = Feed(handle=handle, password=password)
try:
feed.bluesky()
except HTTPError as e:
try:
resp = e.response.json()
msg = resp.get('message') or resp.get('error') or str(e)
except ValueError:
msg = str(e)
return render_template('index.html', error=msg), 502
feed.put()
params = {'feed_id': feed.key.id()}
for param in 'replies', 'reposts':
if get_bool_param(param):
params[param] = 'true'
feed_url = util.add_query_params(urljoin(request.host_url, '/feed'), params)
return render_template('index.html', feed_url=feed_url, request=request)