Activity => Object: update pages.py

#286
activity-redesign
Ryan Barrett 2023-01-28 15:07:05 -08:00
rodzic 23aff3b176
commit 8176cd1f56
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
3 zmienionych plików z 91 dodań i 102 usunięć

116
pages.py
Wyświetl plik

@ -7,7 +7,7 @@ import urllib.parse
from flask import redirect, render_template, request
from google.cloud.ndb.stats import KindStat
from granary import as2, atom, microformats2, rss
from granary import as1, as2, atom, microformats2, rss
import humanize
from oauth_dropins.webutil import flask_util, logs, util
from oauth_dropins.webutil.flask_util import error, flash, redirect
@ -16,9 +16,8 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
from app import app, cache
import common
from common import DOMAIN_RE, PAGE_SIZE
from models import Follower, User, Activity
from models import Follower, Object, User
ACTIVITIES_FETCH_LIMIT = 200
FOLLOWERS_UI_LIMIT = 999
logger = logging.getLogger(__name__)
@ -79,11 +78,11 @@ def user(domain):
assert not user.use_instead
query = Activity.query(
Activity.status.IN(('new', 'complete', 'error')),
Activity.domain == domain,
query = Object.query(
Object.status.IN(('new', 'in progress', 'complete')),
Object.domains == domain,
)
activities, before, after = fetch_activities(query)
objects, before, after = fetch_objects(query)
followers = Follower.query(Follower.dest == domain, Follower.status == 'active')\
.count(limit=FOLLOWERS_UI_LIMIT)
@ -134,12 +133,11 @@ def feed(domain):
if not (user := User.get_by_id(domain)):
return render_template('user_not_found.html', domain=domain), 404
activities, _, _ = Activity.query(
Activity.domain == domain, Activity.direction == 'in'
).order(-Activity.created
).fetch_page(PAGE_SIZE)
as1_activities = [a for a in [a.to_as1() for a in activities]
if a and a.get('verb') not in ('like', 'update', 'follow')]
objects, _, _ = Object.query(
Object.domains == domain, Object.labels == 'feed') \
.order(-Object.created) \
.fetch_page(PAGE_SIZE)
activities = [json_loads(obj.as1) for obj in objects]
actor = {
'displayName': domain,
@ -148,23 +146,23 @@ def feed(domain):
title = f'Bridgy Fed feed for {domain}'
if format == 'html':
entries = [microformats2.object_to_html(a) for a in as1_activities]
entries = [microformats2.object_to_html(a) for a in activities]
return render_template('feed.html', util=util, **locals())
elif format == 'atom':
body = atom.activities_to_atom(as1_activities, actor=actor, title=title,
body = atom.activities_to_atom(activities, actor=actor, title=title,
request_url=request.url)
return body, {'Content-Type': atom.CONTENT_TYPE}
elif format == 'rss':
body = rss.from_activities(as1_activities, actor=actor, title=title,
body = rss.from_activities(activities, actor=actor, title=title,
feed_url=request.url)
return body, {'Content-Type': rss.CONTENT_TYPE}
@app.get('/recent')
def recent():
"""Renders recent activities, with links to logs."""
query = Activity.query(Activity.status.IN(('new', 'complete', 'error')))
activities, before, after = fetch_activities(query)
"""Renders recent objects, with links to logs."""
query = Object.query(Object.status.IN(('new', 'in progress', 'complete')))
objects, before, after = fetch_objects(query)
return render_template(
'recent.html',
show_domains=True,
@ -174,42 +172,31 @@ def recent():
)
def fetch_activities(query):
"""Fetches a page of Activity entities from a datastore query.
def fetch_objects(query):
"""Fetches a page of Object entities from a datastore query.
Wraps :func:`common.fetch_page` and adds attributes to the returned Activity
entities for rendering in activities.html.
Wraps :func:`common.fetch_page` and adds attributes to the returned Object
entities for rendering in objects.html.
Args:
query: :class:`ndb.Query`
Returns:
(results, new_before, new_after) tuple with:
results: list of Activity entities
results: list of Object entities
new_before, new_after: str query param values for `before` and `after`
to fetch the previous and next pages, respectively
"""
orig_activities, new_before, new_after = common.fetch_page(query, Activity)
activities = []
orig_objects, new_before, new_after = common.fetch_page(query, Object)
objects = []
seen = set()
# synthesize human-friendly content for activities
for i, activity in enumerate(orig_activities):
a = activity.to_as1()
if not a:
continue
# de-dupe
ids = set((a[field] for field in ('id', 'url') if a.get(field)))
if ids & seen:
continue
seen.update(ids)
activities.append(activity)
# synthesize human-friendly content for objects
for i, obj in enumerate(orig_objects):
obj_as1 = json_loads(obj.as1)
# synthesize text snippet
verb = a.get('verb') or a.get('objectType')
obj = util.get_first(a, 'object') or {}
type = as1.object_type(obj_as1)
phrases = {
'article': 'posted',
'comment': 'replied',
@ -226,29 +213,36 @@ def fetch_activities(query):
'share': 'reposted',
'stop-following': 'unfollowed',
}
activity.phrase = phrases.get(verb)
obj.phrase = phrases.get(type)
obj_content = obj.get('content') or obj.get('displayName')
obj_url = util.get_first(obj, 'url') or obj.get('id')
if obj_url:
obj_content = util.pretty_link(obj_url, text=obj_content)
elif (activity.domain and
a.get('id', '').strip('/') == f'https://{activity.domain[0]}'):
activity.phrase = 'updated'
a['content'] = 'their profile'
# TODO: unify inner object loading? optionally fetch external?
inner_obj = util.get_first(obj_as1, 'object') or {}
if isinstance(inner_obj, str):
inner_obj = Object.get_by_id(inner_obj)
if inner_obj:
inner_obj = json_loads(inner_obj.as1)
activity.content = a.get('content') or a.get('displayName')
activity.url = util.get_first(a, 'url')
content = inner_obj.get('content') or inner_obj.get('displayName')
url = util.get_first(inner_obj, 'url') or inner_obj.get('id')
if url:
content = util.pretty_link(url, text=content)
elif (obj.domains and
obj_as1.get('id', '').strip('/') == f'https://{obj.domains[0]}'):
obj.phrase = 'updated'
obj_as1['content'] = 'their profile'
if (verb in ('like', 'follow', 'repost', 'share') or
not activity.content):
if activity.url:
activity.phrase = util.pretty_link(activity.url, text=activity.phrase)
if obj_content:
activity.content = obj_content
activity.url = obj_url
obj.content = obj_as1.get('content') or obj_as1.get('displayName')
obj.url = util.get_first(obj_as1, 'url')
return activities, new_before, new_after
if (type in ('like', 'follow', 'repost', 'share') or
not obj.content):
if obj.url:
obj.phrase = util.pretty_link(obj.url, text=obj.phrase)
if content:
obj.content = content
obj.url = url
return objects, new_before, new_after
@app.get('/stats')
@ -260,7 +254,7 @@ def stats():
return render_template(
'stats.html',
users=count('MagicKey'),
activities=count('Response'),
objects=count('Object'),
followers=count('Follower'),
)

Wyświetl plik

@ -1,26 +1,26 @@
<div class="row">
<ul class="user-items">
{% for a in activities %}
{% for obj in objects %}
<li class="row">
<div class="col-sm-{{ 7 if show_domains else 10 }}">
{{ a.actor_link()|safe }}
{{ a.phrase|safe }}
<a target="_blank" href="{{ a.url }}">
{{ a.content|default('--', true)|striptags|truncate(50) }}
{{ obj.actor_link()|safe }}
{{ obj.phrase|safe }}
<a target="_blank" href="{{ obj.url }}">
{{ obj.content|default('--', true)|striptags|truncate(50) }}
</a>
</div>
{% if show_domains %}
<div class="col-sm-3">
{% for domain in a.domain %}
{% for domain in obj.domains %}
{% if loop.index0 == 3 %}
<span id="more-domains" style="display: none">
{% endif %}
<a href="/user/{{ domain }}">🌐 {{ domain }}</a>
<br>
{% endfor %}
{% if a.domain|length > 3 %}
{% if obj.domains|length > 3 %}
</span>
<a onclick="toggle('more-domains'); toggle('show-more-domains'); return false"
id="show-more-domains" href="#" />...</a>
@ -29,12 +29,12 @@
{% endif %}
<div class="col-sm-2">
{% if a.status == 'error' %}
{% if obj.status == 'error' %}
<span title="Error" class="glyphicon glyphicon-exclamation-sign"></span>
{% else %}{% if a.status == 'new' %}
{% else %}{% if obj.status == 'new' %}
<span title="Processing" class="glyphicon glyphicon-transfer"></span>
{% endif %}{% endif %}
{{ logs.maybe_link(a.updated, a.key.id())|safe }}
{{ logs.maybe_link(obj.updated, obj.key.id())|safe }}
</div>
</li>
{% else %}

Wyświetl plik

@ -2,26 +2,28 @@
from oauth_dropins.webutil import util
from oauth_dropins.webutil.util import json_dumps, json_loads
from granary import as2, atom, microformats2, rss
import common
from models import Activity, Follower, User
from . import testutil
from .test_activitypub import (
from granary.tests.test_bluesky import REPLY_BSKY
from granary.tests.test_as1 import (
ACTOR,
COMMENT,
FOLLOW_WITH_ACTOR,
FOLLOW_WITH_OBJECT,
LIKE,
MENTION,
NOTE,
REPLY,
)
import common
from models import Object, Follower, User
from . import testutil
from .test_webmention import ACTOR_MF2
def contents(activities):
return [a['object']['content'] for a in activities]
return [(a.get('object') or a)['content'] for a in activities]
class PagesTest(testutil.TestCase):
EXPECTED_AS1 = [as2.to_as1(REPLY), as2.to_as1(NOTE)]
EXPECTED_AS1 = [COMMENT, NOTE]
EXPECTED = contents(EXPECTED_AS1)
def setUp(self):
@ -29,32 +31,25 @@ class PagesTest(testutil.TestCase):
self.user = User.get_or_create('foo.com')
@staticmethod
def add_activities():
Activity(id='a', domain=['foo.com'], direction='in',
source_as2=json_dumps(NOTE)).put()
# profile update
Activity(id='g', domain=['foo.com'], direction='out',
source_mf2=json_dumps(ACTOR_MF2)).put()
def add_objects():
# post
Object(id='a', domains=['foo.com'], labels=['feed'],
as1=json_dumps(NOTE)).put()
# different domain
Activity(id='b', domain=['bar.org'], direction='in',
source_as2=json_dumps(MENTION)).put()
# empty, should be skipped
Activity(id='c', domain=['foo.com'], direction='in').put()
Activity(id='d', domain=['foo.com'], direction='in',
source_as2=json_dumps(REPLY)).put()
# wrong direction
Activity(id='e', domain=['foo.com'], direction='out',
source_as2=json_dumps(NOTE)).put()
# skip Likes
Activity(id='f', domain=['foo.com'], direction='in',
source_as2=json_dumps(LIKE)).put()
Object(id='b', domains=['bar.org'], labels=['feed'],
as1=json_dumps(MENTION)).put()
# reply
Object(id='d', domains=['foo.com'], labels=['feed'],
as1=json_dumps(COMMENT)).put()
# not feed
Object(id='e', domains=['foo.com'], as1=json_dumps(NOTE)).put()
def test_user(self):
got = self.client.get('/user/foo.com')
self.assert_equals(200, got.status_code)
def test_user_activities(self):
self.add_activities()
def test_user_objects(self):
self.add_objects()
got = self.client.get('/user/foo.com')
self.assert_equals(200, got.status_code)
@ -125,7 +120,7 @@ class PagesTest(testutil.TestCase):
self.assert_equals([], microformats2.html_to_activities(got.text))
def test_feed_html(self):
self.add_activities()
self.add_objects()
got = self.client.get('/user/foo.com/feed')
self.assert_equals(200, got.status_code)
self.assert_equals(self.EXPECTED,
@ -137,7 +132,7 @@ class PagesTest(testutil.TestCase):
self.assert_equals([], atom.atom_to_activities(got.text))
def test_feed_atom(self):
self.add_activities()
self.add_objects()
got = self.client.get('/user/foo.com/feed?format=atom')
self.assert_equals(200, got.status_code)
self.assert_equals(self.EXPECTED, contents(atom.atom_to_activities(got.text)))
@ -148,7 +143,7 @@ class PagesTest(testutil.TestCase):
self.assert_equals([], rss.to_activities(got.text))
def test_feed_rss(self):
self.add_activities()
self.add_objects()
got = self.client.get('/user/foo.com/feed?format=rss')
self.assert_equals(200, got.status_code)
self.assert_equals(self.EXPECTED, contents(rss.to_activities(got.text)))