kopia lustrzana https://github.com/snarfed/bridgy-fed
refactor /responses and user pages, add follower counts to user pages
for #274 more to comepull/280/head
rodzic
741107c3b5
commit
e767cf5d1f
22
models.py
22
models.py
|
@ -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
106
pages.py
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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 }}">← Newer</a>
|
||||
{% if after %}
|
||||
<a href="?after={{ after }}#responses">← Newer</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-sm-offset-6">
|
||||
{% if responses_before_link %}
|
||||
<a href="{{ responses_before_link }}">Older →</a>
|
||||
{% if before %}
|
||||
<a href="?before={{ before }}#responses">Older →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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 %}
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
Ładowanie…
Reference in New Issue