add timeline feeds for posts from fediverse followings

for #265
pull/292/head
Ryan Barrett 2022-11-17 07:38:52 -08:00
rodzic 023f2aa536
commit 4e0fb6536c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
6 zmienionych plików z 143 dodań i 1 usunięć

Wyświetl plik

@ -19,6 +19,13 @@ indexes:
- name: updated
direction: desc
- kind: Response
properties:
- name: direction
- name: domain
- name: created
direction: desc
- kind: Follower
properties:
- name: dest

Wyświetl plik

@ -7,6 +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 oauth_dropins.webutil import flask_util, logs, util
from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.util import json_dumps, json_loads
@ -108,6 +109,44 @@ def following(domain):
)
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>/feed')
def feed(domain):
format = request.args.get('format', 'html')
if format not in ('html', 'atom', 'rss'):
error(f'format {format} not supported; expected html, atom, or rss')
as2_activities, _, _ = Activity.query(
Activity.domain == domain, Activity.direction == 'in'
).order(-Activity.created
).fetch_page(PAGE_SIZE)
as1_activities = [as2.to_as1(json_loads(a.source_as2))
for a in as2_activities
if a.source_as2]
as1_activities = [a for a in as1_activities
if a.get('verb') not in ('like', 'update', 'follow')]
actor = {
'displayName': domain,
'url': f'https://{domain}',
}
title = f'Bridgy Fed feed for {domain}'
extra = '<link rel="stylesheet" href="/static/feed.css" type="text/css" />'
if not as1_activities:
extra += '\n<p>Nothing yet. Follow more people, check back soon!</p>'
if format == 'html':
return microformats2.activities_to_html(as1_activities, extra=extra,
body_class='h-feed')
elif format == 'atom':
body = atom.activities_to_atom(as1_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,
feed_url=request.url)
return body, {'Content-Type': rss.CONTENT_TYPE}
@app.get('/responses') # deprecated
def recent_deprecated():
return redirect('/recent', code=301)

24
static/feed.css 100644
Wyświetl plik

@ -0,0 +1,24 @@
/** From Bridgy's mf2 handlers:
* https://github.com/snarfed/bridgy/blob/0b4b37cab61257510b45aee5b0678ba53af69d80/handlers.py#L43-L70
*
* Also see:
* https://github.com/kevinmarks/unmung/blob/master/styles/hfeed.css
* https://github.com/kevinmarks/unmung/blob/master/styles/mastoview.css
*/
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.p-uid {
display: none;
}
.u-photo {
max-width: 50px;
border-radius: 4px;
}
.e-content {
margin-top: 10px;
font-size: 1.3em;
}

Wyświetl plik

@ -11,6 +11,9 @@
<h3 style="display: inline">
| <a href="/user/{{ domain }}/followers">{{ followers }} follower{% if followers != '1' %}s{% endif %}</a>
| <a href="/user/{{ domain }}/following">following {{ following }}</a>
| <a href="/user/{{ domain }}/feed">HTML</a>,
<a href="/user/{{ domain }}/feed?format=atom">Atom</a>,
<a href="/user/{{ domain }}/feed?format=rss">RSS</a> feeds
</h3>
</div>

Wyświetl plik

@ -0,0 +1,69 @@
"""Unit tests for pages.py."""
from oauth_dropins.webutil import util
from oauth_dropins.webutil.util import json_dumps, json_loads
from granary import as2, atom, microformats2, rss
from models import Follower, Activity
from . import testutil
from .test_activitypub import LIKE, MENTION, NOTE, REPLY
def contents(activities):
return [a['object']['content'] for a in activities]
class PagesTest(testutil.TestCase):
EXPECTED = contents([as2.to_as1(REPLY), as2.to_as1(NOTE)])
@staticmethod
def add_activities():
Activity(id='a', domain=['foo.com'], direction='in',
source_as2=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()
def test_feed_html_empty(self):
got = self.client.get('/user/foo.com/feed')
self.assert_equals(200, got.status_code)
self.assert_equals([], microformats2.html_to_activities(got.text))
def test_feed_html(self):
self.add_activities()
got = self.client.get('/user/foo.com/feed')
self.assert_equals(200, got.status_code)
self.assert_equals(self.EXPECTED,
contents(microformats2.html_to_activities(got.text)))
def test_feed_atom_empty(self):
got = self.client.get('/user/foo.com/feed?format=atom')
self.assert_equals(200, got.status_code)
self.assert_equals([], atom.atom_to_activities(got.text))
def test_feed_atom(self):
self.add_activities()
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)))
def test_feed_rss_empty(self):
got = self.client.get('/user/foo.com/feed?format=rss')
self.assert_equals(200, got.status_code)
self.assert_equals([], rss.to_activities(got.text))
def test_feed_rss(self):
self.add_activities()
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)))

Wyświetl plik

@ -43,7 +43,7 @@ class RenderTest(testutil.TestCase):
<html>
<head><meta charset="utf-8">
<meta http-equiv="refresh" content="0;url=abc"></head>
<body>
<body class="">
<article class="h-entry">
<span class="p-uid">http://this/reply</span>
<a class="u-url" href="http://this/reply">http://this/reply</a>