kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
23aff3b176
commit
8176cd1f56
116
pages.py
116
pages.py
|
@ -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'),
|
||||
)
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)))
|
||||
|
|
Ładowanie…
Reference in New Issue