kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
afa16d3864
commit
c1e0a08f72
17
models.py
17
models.py
|
@ -957,7 +957,7 @@ class Follower(ndb.Model):
|
||||||
filter_prop == g.user.key,
|
filter_prop == g.user.key,
|
||||||
).order(-Follower.updated)
|
).order(-Follower.updated)
|
||||||
|
|
||||||
followers, before, after = fetch_page(query, Follower)
|
followers, before, after = fetch_page(query, Follower, by=Follower.updated)
|
||||||
users = ndb.get_multi(f.from_ if collection == 'followers' else f.to
|
users = ndb.get_multi(f.from_ if collection == 'followers' else f.to
|
||||||
for f in followers)
|
for f in followers)
|
||||||
User.load_multi(u for u in users if u)
|
User.load_multi(u for u in users if u)
|
||||||
|
@ -968,12 +968,11 @@ class Follower(ndb.Model):
|
||||||
return followers, before, after
|
return followers, before, after
|
||||||
|
|
||||||
|
|
||||||
def fetch_page(query, model_class):
|
def fetch_page(query, model_class, by=None):
|
||||||
"""Fetches a page of results from a datastore query.
|
"""Fetches a page of results from a datastore query.
|
||||||
|
|
||||||
Uses the ``before`` and ``after`` query params (if provided; should be
|
Uses the ``before`` and ``after`` query params (if provided; should be
|
||||||
ISO8601 timestamps) and the queried model class's ``updated`` property to
|
ISO8601 timestamps) and the ``by`` property to identify the page to fetch.
|
||||||
identify the page to fetch.
|
|
||||||
|
|
||||||
Populates a ``log_url_path`` property on each result entity that points to a
|
Populates a ``log_url_path`` property on each result entity that points to a
|
||||||
its most recent logged request.
|
its most recent logged request.
|
||||||
|
@ -981,6 +980,8 @@ def fetch_page(query, model_class):
|
||||||
Args:
|
Args:
|
||||||
query (google.cloud.ndb.query.Query)
|
query (google.cloud.ndb.query.Query)
|
||||||
model_class (class)
|
model_class (class)
|
||||||
|
by (ndb.model.Property): paging property, eg :attr:`models.Object.updated`
|
||||||
|
or :attr:`models.Object.created`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(list of Object or Follower, str, str) tuple: (results, new_before,
|
(list of Object or Follower, str, str) tuple: (results, new_before,
|
||||||
|
@ -988,6 +989,8 @@ def fetch_page(query, model_class):
|
||||||
``before`` and ``after`` to fetch the previous and next pages,
|
``before`` and ``after`` to fetch the previous and next pages,
|
||||||
respectively
|
respectively
|
||||||
"""
|
"""
|
||||||
|
assert by
|
||||||
|
|
||||||
# if there's a paging param ('before' or 'after'), update query with it
|
# if there's a paging param ('before' or 'after'), update query with it
|
||||||
# TODO: unify this with Bridgy's user page
|
# TODO: unify this with Bridgy's user page
|
||||||
def get_paging_param(param):
|
def get_paging_param(param):
|
||||||
|
@ -1006,11 +1009,11 @@ def fetch_page(query, model_class):
|
||||||
if before and after:
|
if before and after:
|
||||||
error("can't handle both before and after")
|
error("can't handle both before and after")
|
||||||
elif after:
|
elif after:
|
||||||
query = query.filter(model_class.updated >= after).order(model_class.updated)
|
query = query.filter(by >= after).order(by)
|
||||||
elif before:
|
elif before:
|
||||||
query = query.filter(model_class.updated < before).order(-model_class.updated)
|
query = query.filter(by < before).order(-by)
|
||||||
else:
|
else:
|
||||||
query = query.order(-model_class.updated)
|
query = query.order(-by)
|
||||||
|
|
||||||
query_iter = query.iter()
|
query_iter = query.iter()
|
||||||
results = sorted(itertools.islice(query_iter, 0, PAGE_SIZE),
|
results = sorted(itertools.islice(query_iter, 0, PAGE_SIZE),
|
||||||
|
|
70
pages.py
70
pages.py
|
@ -9,7 +9,7 @@ from flask import g, render_template, request
|
||||||
from google.cloud.ndb import tasklets
|
from google.cloud.ndb import tasklets
|
||||||
from google.cloud.ndb.query import AND, OR
|
from google.cloud.ndb.query import AND, OR
|
||||||
from google.cloud.ndb.stats import KindStat
|
from google.cloud.ndb.stats import KindStat
|
||||||
from granary import as1, as2, atom, microformats2, rss
|
from granary import as1, atom, microformats2, rss
|
||||||
import humanize
|
import humanize
|
||||||
from oauth_dropins.webutil import flask_util, logs, util
|
from oauth_dropins.webutil import flask_util, logs, util
|
||||||
from oauth_dropins.webutil.flask_util import error, flash, redirect
|
from oauth_dropins.webutil.flask_util import error, flash, redirect
|
||||||
|
@ -42,6 +42,9 @@ def load_user(protocol, id):
|
||||||
:class:`werkzeug.exceptions.HTTPException` on error or redirect
|
:class:`werkzeug.exceptions.HTTPException` on error or redirect
|
||||||
"""
|
"""
|
||||||
assert id
|
assert id
|
||||||
|
if protocol == 'ap' and not id.startswith('@'):
|
||||||
|
id = '@' + id
|
||||||
|
|
||||||
cls = PROTOCOLS[protocol]
|
cls = PROTOCOLS[protocol]
|
||||||
g.user = cls.get_by_id(id)
|
g.user = cls.get_by_id(id)
|
||||||
|
|
||||||
|
@ -95,15 +98,12 @@ def web_user_redirects(**kwargs):
|
||||||
# WARNING: this overrides the /ap/... actor URL route in activitypub.py, *only*
|
# WARNING: this overrides the /ap/... actor URL route in activitypub.py, *only*
|
||||||
# for handles with leading @ character. be careful when changing this route!
|
# for handles with leading @ character. be careful when changing this route!
|
||||||
@app.get(f'/ap/@<id>', defaults={'protocol': 'ap'})
|
@app.get(f'/ap/@<id>', defaults={'protocol': 'ap'})
|
||||||
def user(protocol, id):
|
def profile(protocol, id):
|
||||||
if protocol == 'ap' and not id.startswith('@'):
|
|
||||||
id = '@' + id
|
|
||||||
|
|
||||||
load_user(protocol, id)
|
load_user(protocol, id)
|
||||||
|
|
||||||
query = Object.query(OR(Object.users == g.user.key,
|
query = Object.query(OR(Object.users == g.user.key,
|
||||||
Object.notify == g.user.key))
|
Object.notify == g.user.key))
|
||||||
objects, before, after = fetch_objects(query)
|
objects, before, after = fetch_objects(query, by=Object.updated)
|
||||||
|
|
||||||
followers = Follower.query(Follower.to == g.user.key,
|
followers = Follower.query(Follower.to == g.user.key,
|
||||||
Follower.status == 'active')\
|
Follower.status == 'active')\
|
||||||
|
@ -115,15 +115,27 @@ def user(protocol, id):
|
||||||
.count(limit=FOLLOWERS_UI_LIMIT)
|
.count(limit=FOLLOWERS_UI_LIMIT)
|
||||||
following = f'{following}{"+" if following == FOLLOWERS_UI_LIMIT else ""}'
|
following = f'{following}{"+" if following == FOLLOWERS_UI_LIMIT else ""}'
|
||||||
|
|
||||||
return render_template(
|
return render_template('profile.html', logs=logs, util=util, g=g, **locals())
|
||||||
'profile.html',
|
|
||||||
follow_url=request.values.get('url'),
|
|
||||||
logs=logs,
|
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>/home')
|
||||||
util=util,
|
def home(protocol, id):
|
||||||
address=request.args.get('address'),
|
load_user(protocol, id)
|
||||||
g=g,
|
|
||||||
**locals(),
|
query = Object.query(Object.feed == g.user.key)
|
||||||
)
|
objects, before, after = fetch_objects(query, by=Object.created)
|
||||||
|
|
||||||
|
return render_template('home.html', logs=logs, util=util, g=g, **locals())
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>/notifications')
|
||||||
|
def notifications(protocol, id):
|
||||||
|
load_user(protocol, id)
|
||||||
|
|
||||||
|
query = Object.query(Object.notify == g.user.key)
|
||||||
|
objects, before, after = fetch_objects(query, by=Object.updated)
|
||||||
|
|
||||||
|
return render_template('notifications.html', logs=logs, util=util, g=g, **locals())
|
||||||
|
|
||||||
|
|
||||||
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>/<any(followers,following):collection>')
|
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>/<any(followers,following):collection>')
|
||||||
|
@ -134,10 +146,10 @@ def followers_or_following(protocol, id, collection):
|
||||||
return render_template(
|
return render_template(
|
||||||
f'{collection}.html',
|
f'{collection}.html',
|
||||||
address=request.args.get('address'),
|
address=request.args.get('address'),
|
||||||
as2=as2,
|
follow_url=request.values.get('url'),
|
||||||
g=g,
|
g=g,
|
||||||
util=util,
|
util=util,
|
||||||
**locals()
|
**locals(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,13 +161,13 @@ def feed(protocol, id):
|
||||||
|
|
||||||
load_user(protocol, id)
|
load_user(protocol, id)
|
||||||
|
|
||||||
objects = Object.query(OR(Object.feed == g.user.key,
|
query = Object.query(Object.feed == g.user.key)
|
||||||
# backward compatibility
|
# .order(-Object.created) \
|
||||||
AND(Object.users == g.user.key,
|
# .fetch(PAGE_SIZE)
|
||||||
Object.labels == 'feed'))) \
|
# activities = [obj.as1 for obj in objects if not obj.deleted]
|
||||||
.order(-Object.created) \
|
|
||||||
.fetch(PAGE_SIZE)
|
objects, _, _ = fetch_objects(query, by=Object.created)
|
||||||
activities = [obj.as1 for obj in objects if not obj.deleted]
|
activities = [obj.as1 for obj in objects]
|
||||||
|
|
||||||
# hydrate authors, actors, objects from stored Objects
|
# hydrate authors, actors, objects from stored Objects
|
||||||
fields = 'author', 'actor', 'object'
|
fields = 'author', 'actor', 'object'
|
||||||
|
@ -230,7 +242,7 @@ def bridge_user():
|
||||||
return render_template('bridge_user.html')
|
return render_template('bridge_user.html')
|
||||||
|
|
||||||
|
|
||||||
def fetch_objects(query):
|
def fetch_objects(query, by=None):
|
||||||
"""Fetches a page of :class:`models.Object` entities from a datastore query.
|
"""Fetches a page of :class:`models.Object` entities from a datastore query.
|
||||||
|
|
||||||
Wraps :func:`models.fetch_page` and adds attributes to the returned
|
Wraps :func:`models.fetch_page` and adds attributes to the returned
|
||||||
|
@ -238,16 +250,22 @@ def fetch_objects(query):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query (ndb.Query)
|
query (ndb.Query)
|
||||||
|
by (ndb.model.Property): either :attr:`models.Object.updated` or
|
||||||
|
:attr:`models.Object.created`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(list of models.Object, str, str) tuple:
|
(list of models.Object, str, str) tuple:
|
||||||
(results, new ``before`` query param, new ``after`` query param)
|
(results, new ``before`` query param, new ``after`` query param)
|
||||||
to fetch the previous and next pages, respectively
|
to fetch the previous and next pages, respectively
|
||||||
"""
|
"""
|
||||||
objects, new_before, new_after = fetch_page(query, Object)
|
assert by is Object.updated or by is Object.created
|
||||||
|
objects, new_before, new_after = fetch_page(query, Object, by=by)
|
||||||
|
|
||||||
# synthesize human-friendly content for objects
|
# synthesize human-friendly content for objects
|
||||||
for i, obj in enumerate(objects):
|
for i, obj in enumerate(objects):
|
||||||
|
if obj.deleted:
|
||||||
|
continue
|
||||||
|
|
||||||
obj_as1 = obj.as1
|
obj_as1 = obj.as1
|
||||||
inner_obj = as1.get_object(obj_as1)
|
inner_obj = as1.get_object(obj_as1)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "user_base.html" %}
|
||||||
|
{% set tab = "home" %}
|
||||||
|
|
||||||
|
{% block subtabs %}
|
||||||
|
<div class="row">
|
||||||
|
Subscribe:
|
||||||
|
<a href="{{ g.user.user_page_path('feed') }}">HTML</a>
|
||||||
|
· <a href="{{ g.user.user_page_path('feed?format=atom') }}">Atom</a>
|
||||||
|
· <a href="{{ g.user.user_page_path('feed?format=rss') }}">RSS</a>
|
||||||
|
</div>
|
||||||
|
{% endblock subtabs %}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "user_base.html" %}
|
||||||
|
{% set tab = "notifications" %}
|
||||||
|
|
||||||
|
{% block subtabs %}
|
||||||
|
<div class="row">
|
||||||
|
Subscribe:
|
||||||
|
<a href="{{ g.user.user_page_path('feed') }}">HTML</a>
|
||||||
|
· <a href="{{ g.user.user_page_path('feed?format=atom') }}">Atom</a>
|
||||||
|
· <a href="{{ g.user.user_page_path('feed?format=rss') }}">RSS</a>
|
||||||
|
</div>
|
||||||
|
{% endblock subtabs %}
|
|
@ -9,7 +9,7 @@
|
||||||
·
|
·
|
||||||
|
|
||||||
{% if g.user.LABEL != 'activitypub' %}
|
{% if g.user.LABEL != 'activitypub' %}
|
||||||
Bridged to
|
bridged to
|
||||||
<nobr title="Fediverse address">
|
<nobr title="Fediverse address">
|
||||||
<img class="logo" src="/static/fediverse_logo.svg">
|
<img class="logo" src="/static/fediverse_logo.svg">
|
||||||
{{ g.user.ap_address() }}
|
{{ g.user.ap_address() }}
|
||||||
|
@ -24,8 +24,3 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock subtabs %}
|
{% endblock subtabs %}
|
||||||
|
|
||||||
{#
|
|
||||||
{% block feed %}
|
|
||||||
{% endblock feed %}
|
|
||||||
#}
|
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
<!-- tabs -->
|
<!-- tabs -->
|
||||||
<div class="row tabs">
|
<div class="row tabs">
|
||||||
<a>
|
<a></a>
|
||||||
<a href="{{ g.user.user_page_path() }}"
|
<a href="{{ g.user.user_page_path() }}"
|
||||||
{% if tab == 'profile' %}class="active-tab"{% endif %}
|
{% if tab == 'profile' %}class="active-tab"{% endif %}
|
||||||
>👤 Profile</a><a
|
>👤 Profile</a><a
|
||||||
|
@ -51,17 +51,10 @@
|
||||||
href="{{ g.user.user_page_path('notifications') }}"
|
href="{{ g.user.user_page_path('notifications') }}"
|
||||||
{% if tab == 'notifications' %}class="active-tab"{% endif %}
|
{% if tab == 'notifications' %}class="active-tab"{% endif %}
|
||||||
>🔔 Notifications</a>
|
>🔔 Notifications</a>
|
||||||
<a>
|
<a></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block subtabs %}
|
{% block subtabs %}
|
||||||
<div class="row">
|
|
||||||
· <a href="{{ g.user.user_page_path('feed') }}">HTML</a>
|
|
||||||
· <a href="{{ g.user.user_page_path('feed?format=atom') }}">Atom</a>
|
|
||||||
· <a href="{{ g.user.user_page_path('feed?format=rss') }}">RSS</a>
|
|
||||||
· <a href="/.well-known/webfinger?resource=acct:{{ g.user.ap_address() }}">Webfinger</a>
|
|
||||||
· <a href="{{ g.user.ap_actor() }}">ActivityPub</a>
|
|
||||||
</div>
|
|
||||||
{% endblock subtabs %}
|
{% endblock subtabs %}
|
||||||
|
|
||||||
{% block feed %}
|
{% block feed %}
|
||||||
|
|
|
@ -44,8 +44,8 @@ class PagesTest(TestCase):
|
||||||
self.assert_equals(200, got.status_code)
|
self.assert_equals(200, got.status_code)
|
||||||
|
|
||||||
def test_user_fake(self):
|
def test_user_fake(self):
|
||||||
self.make_user('foo.com', cls=Fake)
|
self.make_user('fake:foo', cls=Fake)
|
||||||
got = self.client.get('/fa/foo.com')
|
got = self.client.get('/fa/fake:foo')
|
||||||
self.assert_equals(200, got.status_code)
|
self.assert_equals(200, got.status_code)
|
||||||
|
|
||||||
def test_user_page_handle(self):
|
def test_user_page_handle(self):
|
||||||
|
@ -171,9 +171,19 @@ class PagesTest(TestCase):
|
||||||
self.assertIn('@follow@stored', body)
|
self.assertIn('@follow@stored', body)
|
||||||
self.assertIn('@me@plus.google.com', body)
|
self.assertIn('@me@plus.google.com', body)
|
||||||
|
|
||||||
|
def test_home_fake(self):
|
||||||
|
self.make_user('fake:foo', cls=Fake)
|
||||||
|
got = self.client.get('/fa/fake:foo/home')
|
||||||
|
self.assert_equals(200, got.status_code)
|
||||||
|
|
||||||
|
def test_home_objects(self):
|
||||||
|
self.add_objects()
|
||||||
|
got = self.client.get('/web/user.com/home')
|
||||||
|
self.assert_equals(200, got.status_code)
|
||||||
|
|
||||||
def test_followers_fake(self):
|
def test_followers_fake(self):
|
||||||
self.make_user('foo.com', cls=Fake)
|
self.make_user('fake:foo', cls=Fake)
|
||||||
got = self.client.get('/fa/foo.com/followers')
|
got = self.client.get('/fa/fake:foo/followers')
|
||||||
self.assert_equals(200, got.status_code)
|
self.assert_equals(200, got.status_code)
|
||||||
|
|
||||||
def test_followers_empty(self):
|
def test_followers_empty(self):
|
||||||
|
@ -216,8 +226,8 @@ class PagesTest(TestCase):
|
||||||
self.assertNotIn('class="follower', got.get_data(as_text=True))
|
self.assertNotIn('class="follower', got.get_data(as_text=True))
|
||||||
|
|
||||||
def test_following_fake(self):
|
def test_following_fake(self):
|
||||||
self.make_user('foo.com', cls=Fake)
|
self.make_user('fake:foo', cls=Fake)
|
||||||
got = self.client.get('/fa/foo.com/following')
|
got = self.client.get('/fa/fake:foo/following')
|
||||||
self.assert_equals(200, got.status_code)
|
self.assert_equals(200, got.status_code)
|
||||||
|
|
||||||
def test_following_user_not_found(self):
|
def test_following_user_not_found(self):
|
||||||
|
@ -247,8 +257,8 @@ class PagesTest(TestCase):
|
||||||
self.assert_equals('/web/user.com/feed', got.headers['Location'])
|
self.assert_equals('/web/user.com/feed', got.headers['Location'])
|
||||||
|
|
||||||
def test_feed_fake(self):
|
def test_feed_fake(self):
|
||||||
self.make_user('foo.com', cls=Fake)
|
self.make_user('fake:foo', cls=Fake)
|
||||||
got = self.client.get('/fa/foo.com/feed')
|
got = self.client.get('/fa/fake:foo/feed')
|
||||||
self.assert_equals(200, got.status_code)
|
self.assert_equals(200, got.status_code)
|
||||||
|
|
||||||
def test_feed_html_empty(self):
|
def test_feed_html_empty(self):
|
||||||
|
|
Ładowanie…
Reference in New Issue