refactor /responses and user pages, add follower counts to user pages

for #274
more to come
pull/280/head
Ryan Barrett 2022-11-11 15:44:35 -08:00
rodzic 741107c3b5
commit e767cf5d1f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
8 zmienionych plików z 137 dodań i 80 usunięć

Wyświetl plik

@ -131,11 +131,15 @@ class Response(StringIdModel):
class Follower(StringIdModel):
"""A follower of a Bridgy Fed user.
Key name is 'USER_DOMAIN FOLLOWER_ID', e.g.:
'snarfed.org https://mastodon.social/@swentel'.
Key name is 'TO FROM', where each part is either a domain or an AP id, eg:
'snarfed.org https://mastodon.social/@swentel'.
Both parts are duplicated in the src and dest properties.
"""
STATUSES = ('active', 'inactive')
src = ndb.StringProperty()
dest = ndb.StringProperty()
# most recent AP Follow activity (JSON). must have a composite actor object
# with an inbox, publicInbox, or sharedInbox!
last_follow = ndb.TextProperty()
@ -145,13 +149,13 @@ class Follower(StringIdModel):
updated = ndb.DateTimeProperty(auto_now=True)
@classmethod
def _id(cls, user_domain, follower_id):
assert user_domain
assert follower_id
return '%s %s' % (user_domain, follower_id)
def _id(cls, dest, src):
assert src
assert dest
return '%s %s' % (dest, src)
@classmethod
def get_or_create(cls, user_domain, follower_id, **kwargs):
logger.info(f'new Follower for {user_domain} {follower_id}')
return cls.get_or_insert(cls._id(user_domain, follower_id), **kwargs)
def get_or_create(cls, dest, src, **kwargs):
logger.info(f'new Follower from {src} to {dest}')
return cls.get_or_insert(cls._id(dest, src), src=src, dest=dest, **kwargs)

106
pages.py
Wyświetl plik

@ -11,7 +11,10 @@ from oauth_dropins.webutil.flask_util import error
from app import app, cache
import common
from models import Response
from models import Follower, Response
PAGE_SIZE = 20
FOLLOWERS_UI_LIMIT = 999
@app.route('/')
@ -21,19 +24,64 @@ def front_page():
return render_template('index.html')
@app.get('/responses')
@app.get(f'/responses/<regex("{common.DOMAIN_RE}"):domain>')
def responses(domain=None):
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>')
@app.get(f'/responses/<regex("{common.DOMAIN_RE}"):domain>') # deprecated
def user(domain):
query = Response.query(
Response.status.IN(('new', 'complete', 'error')),
Response.domain == domain,
)
responses, before, after = fetch_page(query, Response)
followers = Follower.query(Follower.dest == domain)\
.count(limit=FOLLOWERS_UI_LIMIT)
followers = f'{followers}{"+" if followers == FOLLOWERS_UI_LIMIT else ""}'
following = Follower.query(Follower.src == domain)\
.count(limit=FOLLOWERS_UI_LIMIT)
following = f'{following}{"+" if following == FOLLOWERS_UI_LIMIT else ""}'
return render_template(
'user.html',
util=util,
**locals(),
)
@app.get('/recent')
@app.get('/responses') # deprecated
def recent():
"""Renders recent Responses, with links to logs."""
query = Response.query()\
.filter(Response.status.IN(('new', 'complete', 'error')))\
.order(-Response.updated)
query = Response.query(Response.status.IN(('new', 'complete', 'error')))
responses, before, after = fetch_page(query, Response)
return render_template(
'recent.html',
util=util,
**locals(),
)
if domain:
query = query.filter(Response.domain == domain)
# if there's a paging param (responses_before or responses_after), update
# query with it
def fetch_page(query, model_class):
"""Fetches a page of results from a datastore query.
Uses the `before` and `after` query params (if provided; should be ISO8601
timestamps) and the queried model class's `updated` property to identify the
page to fetch.
Populates a `log_url_path` property on each result entity that points to a
its most recent logged request.
Args:
query: :class:`ndb.Query`
model_class: ndb model class
Returns:
(results, new_before, new_after) tuple with:
results: list of query result entities
new_before, new_after: str query param values for `before` and `after`
to fetch the previous and next pages, respectively
"""
# if there's a paging param ('before' or 'after'), update query with it
# TODO: unify this with Bridgy's user page
def get_paging_param(param):
val = request.values.get(param)
@ -42,50 +90,42 @@ def responses(domain=None):
except BaseException:
error(f"Couldn't parse {param}, {val!r} as ISO8601")
before = get_paging_param('responses_before')
after = get_paging_param('responses_after')
before = get_paging_param('before')
after = get_paging_param('after')
if before and after:
error("can't handle both responses_before and responses_after")
error("can't handle both before and after")
elif after:
query = query.filter(Response.updated > after).order(Response.updated)
query = query.filter(model_class.updated > after).order(-model_class.updated)
elif before:
query = query.filter(Response.updated < before).order(-Response.updated)
query = query.filter(model_class.updated < before).order(-model_class.updated)
else:
query = query.order(-Response.updated)
query = query.order(-model_class.updated)
query_iter = query.iter()
responses = list(islice(query_iter, 0, 20))
for r in responses:
r.source_link = util.pretty_link(r.source())
r.target_link = util.pretty_link(r.target())
results = sorted(islice(query_iter, 0, 20), key=lambda r: r.updated, reverse=True)
for r in results:
r.log_url_path = '/log?' + urllib.parse.urlencode({
'key': r.key.id(),
'start_time': calendar.timegm(r.updated.timetuple()),
})
vars = {
'domain': domain,
'responses': sorted(responses, key=lambda r: r.updated, reverse=True),
}
# calculate new paging param(s)
has_next = results and query_iter.probably_has_next()
new_after = (
before if before
else responses[0].updated
if responses and query_iter.probably_has_next() and (before or after)
else results[0].updated if has_next and after
else None)
if new_after:
vars['responses_after_link'] = f'?responses_after={new_after.isoformat()}#responses'
new_after = new_after.isoformat()
new_before = (
after if after else
responses[-1].updated if
responses and query_iter.probably_has_next()
results[-1].updated if has_next
else None)
if new_before:
vars['responses_before_link'] = f'?responses_before={new_before.isoformat()}#responses'
new_before = new_before.isoformat()
return render_template('responses.html', **vars)
return results, new_before, new_after
@app.get('/stats')

Wyświetl plik

@ -94,7 +94,7 @@ The webmention source URL will usually be a proxy page on <code>fed.brid.gy</cod
</p>
<p>
You can see your recent interactions at <a href="https://fed.brid.gy/responses/[your-domain.com]">fed.brid.gy/responses/[your-domain.com]</a>.
You can see your recent interactions at <a href="https://fed.brid.gy/user/[your-domain.com]">fed.brid.gy/user/[your-domain.com]</a>.
</p>
</li>
@ -200,7 +200,7 @@ I love scotch. Scotchy scotchy scotch.
<li class="answer">
<p>If you sent a webmention, check the HTTP response code and body. It will usually describe the error.</p>
<p>If you got an HTTP 204 from an attempt to federate a response to Mastodon, that means Mastodon accepted the response. If it doesn't show up, that's a known inconsistency right now. We're actively working with them to debug these cases.</p>
<p>You can also <a href="/responses">see all recent Bridgy Fed requests here</a>, including raw logs. Warning: not for the faint of heart!</p>
<p>You can also <a href="/recent">see all recent Bridgy Fed requests here</a>, including raw logs. Warning: not for the faint of heart!</p>
</li>
<li id="cost" class="question">How much does it cost?</li>

Wyświetl plik

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}Bridgy Fed: Recent activity{% endblock %}
{% block content %}
<h2 class="row">Recent activity</h3>
{% include "responses.html" %}
{% endblock %}

Wyświetl plik

@ -1,50 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Bridgy Fed: {{ domain or 'Recent activity' }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mobile-web-app-capable" content="yes"/>
<link rel="stylesheet" href="/oauth_dropins_static/bootstrap.min.css" />
<link rel="stylesheet" href="/static/style.css" type="text/css" />
</head>
<!-- <table> -->
<!-- <tr><th>Source</th> <th>Target</th> <th>Protocol</th> <th>Status</th> <th>Time (click for log)</th></tr> -->
<body>
<main class="tp-main lead container">
<h1>Bridgy Fed</h1>
<h3>{{ domain or 'Recent activity' }}</h3>
<br>
<table>
<tr><th>Source</th> <th>Target</th> <th>Protocol</th> <th>Status</th> <th>Time (click for log)</th></tr>
{% for r in responses %}
<tr>
<td>{{ r.source_link|safe }}</td>
<td>{{ r.target_link|safe }}</td>
<td>{{ r.protocol }}</td>
<td>{{ r.status }}</td>
<td>
<div class="row">
<div class="col-sm-3">{{ util.pretty_link(r.source())|safe }}</div>
<div class="col-sm-3">{{ util.pretty_link(r.target())|safe }}</div>
<div class="col-sm-2">{{ r.protocol }}</div>
<div class="col-sm-2">{{ r.status }}</div>
<div class="col-sm-2">
{% if r.log_url_path %}<a href="{{ r.log_url_path }}">{% endif %}
{{ r.updated.replace(microsecond=0) }}
{% if r.log_url_path %}</a>{% endif %}
</td>
</tr>
</div>
</div>
{% else %}
<div class="row">None</div>
{% endfor %}
</table>
<!-- </table> -->
<div class="row">
<div class="col-sm-3">
{% if responses_after_link %}
<a href="{{ responses_after_link }}">&larr; Newer</a>
{% if after %}
<a href="?after={{ after }}#responses">&larr; Newer</a>
{% endif %}
</div>
<div class="col-sm-3 col-sm-offset-6">
{% if responses_before_link %}
<a href="{{ responses_before_link }}">Older &rarr;</a>
{% if before %}
<a href="?before={{ before }}#responses">Older &rarr;</a>
{% endif %}
</div>
</div>
</main>
</body>
</html>

Wyświetl plik

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block title %}Bridgy Fed: {{ domain }}{% endblock %}
{% block content %}
<h2 class="row">
<a href="https://{{ domain }}/">🌐 {{ domain }}</a>
</h2>
<h3 class="row">
<a href="/user/{{ domain }}/followers">{{ followers }} follower{% if followers != '1' %}s{% endif %}</a>
<!-- | <a href="/user/{{ domain }}/following">{{ following }} following</a> -->
</h3>
{% include "responses.html" %}
{% endblock %}

Wyświetl plik

@ -366,7 +366,7 @@ class ActivityPubTest(testutil.TestCase):
self.assertEqual(FOLLOW_WITH_ACTOR, json_loads(resp.source_as2))
# check that we stored a Follower object
follower = Follower.get_by_id('www.realize.be %s' % (FOLLOW['actor']))
follower = Follower.get_by_id(f'www.realize.be {FOLLOW["actor"]}')
self.assertEqual('active', follower.status)
self.assertEqual(FOLLOW_WRAPPED_WITH_ACTOR, json_loads(follower.last_follow))
@ -378,7 +378,7 @@ class ActivityPubTest(testutil.TestCase):
got = self.client.post('/foo.com/inbox', json=UNDO_FOLLOW_WRAPPED)
self.assertEqual(200, got.status_code)
follower = Follower.get_by_id('www.realize.be %s' % FOLLOW['actor'])
follower = Follower.get_by_id(f'www.realize.be {FOLLOW["actor"]}')
self.assertEqual('inactive', follower.status)
def test_inbox_undo_follow_doesnt_exist(self, mock_head, mock_get, mock_post):

Wyświetl plik

@ -192,7 +192,7 @@ def host_meta_xrds():
app.add_url_rule(f'/acct:<regex("{common.DOMAIN_RE}"):domain>',
view_func=User.as_view('user'))
view_func=User.as_view('actor_acct'))
app.add_url_rule('/.well-known/webfinger', view_func=Webfinger.as_view('webfinger'))
app.add_url_rule('/.well-known/host-meta', view_func=HostMeta.as_view('hostmeta'))
app.add_url_rule('/.well-known/host-meta.json', view_func=HostMeta.as_view('hostmeta-json'))