kopia lustrzana https://github.com/snarfed/bridgy-fed
rename MagicKey => Domain, Response => Activity
the Python classes, but not (yet) the kinds in the datastore. maybe eventually.pull/280/head
rodzic
53a133d554
commit
4f3dc03a3e
|
@ -17,7 +17,7 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
|
||||||
from app import app, cache
|
from app import app, cache
|
||||||
import common
|
import common
|
||||||
from common import redirect_unwrap, redirect_wrap
|
from common import redirect_unwrap, redirect_wrap
|
||||||
from models import Follower, MagicKey
|
from models import Follower, Domain
|
||||||
from httpsig.requests_auth import HTTPSignatureAuth
|
from httpsig.requests_auth import HTTPSignatureAuth
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -58,7 +58,7 @@ def send(activity, inbox_url, user_domain):
|
||||||
# https://tools.ietf.org/html/draft-cavage-http-signatures-07
|
# https://tools.ietf.org/html/draft-cavage-http-signatures-07
|
||||||
# https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846
|
# https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846
|
||||||
key_id = request.host_url + user_domain
|
key_id = request.host_url + user_domain
|
||||||
key = MagicKey.get_or_create(user_domain)
|
key = Domain.get_or_create(user_domain)
|
||||||
auth = HTTPSignatureAuth(secret=key.private_pem(), key_id=key_id,
|
auth = HTTPSignatureAuth(secret=key.private_pem(), key_id=key_id,
|
||||||
algorithm='rsa-sha256', sign_header='signature',
|
algorithm='rsa-sha256', sign_header='signature',
|
||||||
headers=('Date', 'Digest', 'Host'))
|
headers=('Date', 'Digest', 'Host'))
|
||||||
|
@ -94,7 +94,7 @@ def actor(domain):
|
||||||
if not hcard:
|
if not hcard:
|
||||||
error(f"Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {mf2['url']}")
|
error(f"Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {mf2['url']}")
|
||||||
|
|
||||||
key = MagicKey.get_or_create(domain)
|
key = Domain.get_or_create(domain)
|
||||||
obj = common.postprocess_as2(
|
obj = common.postprocess_as2(
|
||||||
as2.from_as1(microformats2.json_to_object(hcard)), key=key)
|
as2.from_as1(microformats2.json_to_object(hcard)), key=key)
|
||||||
obj.update({
|
obj.update({
|
||||||
|
|
24
common.py
24
common.py
|
@ -14,7 +14,7 @@ from oauth_dropins.webutil.flask_util import error
|
||||||
import requests
|
import requests
|
||||||
from werkzeug.exceptions import BadGateway
|
from werkzeug.exceptions import BadGateway
|
||||||
|
|
||||||
from models import Response
|
from models import Activity
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -144,11 +144,11 @@ def content_type(resp):
|
||||||
return type.split(';')[0]
|
return type.split(';')[0]
|
||||||
|
|
||||||
|
|
||||||
def send_webmentions(activity_wrapped, proxy=None, **response_props):
|
def send_webmentions(activity_wrapped, proxy=None, **activity_props):
|
||||||
"""Sends webmentions for an incoming Salmon slap or ActivityPub inbox delivery.
|
"""Sends webmentions for an incoming Salmon slap or ActivityPub inbox delivery.
|
||||||
Args:
|
Args:
|
||||||
activity_wrapped: dict, AS1 activity
|
activity_wrapped: dict, AS1 activity
|
||||||
response_props: passed through to the newly created Responses
|
activity_props: passed through to the newly created Activity entities
|
||||||
"""
|
"""
|
||||||
activity = redirect_unwrap(activity_wrapped)
|
activity = redirect_unwrap(activity_wrapped)
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ def send_webmentions(activity_wrapped, proxy=None, **response_props):
|
||||||
if not targets:
|
if not targets:
|
||||||
error("Couldn't find any target URLs in inReplyTo, object, or mention tags")
|
error("Couldn't find any target URLs in inReplyTo, object, or mention tags")
|
||||||
|
|
||||||
# send webmentions and store Responses
|
# send webmentions and store Activitys
|
||||||
errors = [] # stores (code, body) tuples
|
errors = [] # stores (code, body) tuples
|
||||||
for target in targets:
|
for target in targets:
|
||||||
domain = util.domain_from_link(target, minimize=False)
|
domain = util.domain_from_link(target, minimize=False)
|
||||||
|
@ -194,10 +194,10 @@ def send_webmentions(activity_wrapped, proxy=None, **response_props):
|
||||||
logger.info(f'Skipping same-domain webmention from {source} to {target}')
|
logger.info(f'Skipping same-domain webmention from {source} to {target}')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
response = Response(source=source, target=target, direction='in',
|
activity = Activity(source=source, target=target, direction='in',
|
||||||
domain=domain, **response_props)
|
domain=domain, **activity_props)
|
||||||
response.put()
|
activity.put()
|
||||||
wm_source = (response.proxy_url()
|
wm_source = (activity.proxy_url()
|
||||||
if verb in ('follow', 'like', 'share') or proxy
|
if verb in ('follow', 'like', 'share') or proxy
|
||||||
else source)
|
else source)
|
||||||
logger.info(f'Sending webmention from {wm_source} to {target}')
|
logger.info(f'Sending webmention from {wm_source} to {target}')
|
||||||
|
@ -206,14 +206,14 @@ def send_webmentions(activity_wrapped, proxy=None, **response_props):
|
||||||
endpoint = webmention.discover(target).endpoint
|
endpoint = webmention.discover(target).endpoint
|
||||||
if endpoint:
|
if endpoint:
|
||||||
webmention.send(endpoint, wm_source, target)
|
webmention.send(endpoint, wm_source, target)
|
||||||
response.status = 'complete'
|
activity.status = 'complete'
|
||||||
logger.info('Success!')
|
logger.info('Success!')
|
||||||
else:
|
else:
|
||||||
response.status = 'ignored'
|
activity.status = 'ignored'
|
||||||
logger.info('Ignoring.')
|
logger.info('Ignoring.')
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
errors.append(util.interpret_http_exception(e))
|
errors.append(util.interpret_http_exception(e))
|
||||||
response.put()
|
activity.put()
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
msg = 'Errors: ' + ', '.join(f'{code} {body}' for code, body in errors)
|
msg = 'Errors: ' + ', '.join(f'{code} {body}' for code, body in errors)
|
||||||
|
@ -227,7 +227,7 @@ def postprocess_as2(activity, target=None, key=None):
|
||||||
activity: dict, AS2 object or activity
|
activity: dict, AS2 object or activity
|
||||||
target: dict, AS2 object, optional. The target of activity's inReplyTo or
|
target: dict, AS2 object, optional. The target of activity's inReplyTo or
|
||||||
Like/Announce/etc object, if any.
|
Like/Announce/etc object, if any.
|
||||||
key: :class:`models.MagicKey`, optional. populated into publicKey field
|
key: :class:`models.Domain`, optional. populated into publicKey field
|
||||||
if provided.
|
if provided.
|
||||||
"""
|
"""
|
||||||
type = activity.get('type')
|
type = activity.get('type')
|
||||||
|
|
26
models.py
26
models.py
|
@ -11,7 +11,7 @@ from oauth_dropins.webutil.models import StringIdModel
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MagicKey(StringIdModel):
|
class Domain(StringIdModel):
|
||||||
"""Stores a user's public/private key pair used for Magic Signatures.
|
"""Stores a user's public/private key pair used for Magic Signatures.
|
||||||
|
|
||||||
The key name is the domain.
|
The key name is the domain.
|
||||||
|
@ -28,17 +28,21 @@ class MagicKey(StringIdModel):
|
||||||
public_exponent = ndb.StringProperty(required=True)
|
public_exponent = ndb.StringProperty(required=True)
|
||||||
private_exponent = ndb.StringProperty(required=True)
|
private_exponent = ndb.StringProperty(required=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_kind(cls):
|
||||||
|
return 'MagicKey'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ndb.transactional()
|
@ndb.transactional()
|
||||||
def get_or_create(domain):
|
def get_or_create(domain):
|
||||||
"""Loads and returns a MagicKey. Creates it if necessary."""
|
"""Loads and returns a Domain. Creates it if necessary."""
|
||||||
key = MagicKey.get_by_id(domain)
|
key = Domain.get_by_id(domain)
|
||||||
|
|
||||||
if not key:
|
if not key:
|
||||||
# this uses urandom(), and does nontrivial math, so it can take a
|
# this uses urandom(), and does nontrivial math, so it can take a
|
||||||
# while depending on the amount of randomness available.
|
# while depending on the amount of randomness available.
|
||||||
pubexp, mod, privexp = magicsigs.generate()
|
pubexp, mod, privexp = magicsigs.generate()
|
||||||
key = MagicKey(id=domain, mod=mod, public_exponent=pubexp,
|
key = Domain(id=domain, mod=mod, public_exponent=pubexp,
|
||||||
private_exponent=privexp)
|
private_exponent=privexp)
|
||||||
key.put()
|
key.put()
|
||||||
|
|
||||||
|
@ -62,7 +66,7 @@ class MagicKey(StringIdModel):
|
||||||
return rsa.exportKey(format='PEM')
|
return rsa.exportKey(format='PEM')
|
||||||
|
|
||||||
|
|
||||||
class Response(StringIdModel):
|
class Activity(StringIdModel):
|
||||||
"""A reply, like, repost, or other interaction that we've relayed.
|
"""A reply, like, repost, or other interaction that we've relayed.
|
||||||
|
|
||||||
Key name is 'SOURCE_URL TARGET_URL', e.g. 'http://a/reply http://orig/post'.
|
Key name is 'SOURCE_URL TARGET_URL', e.g. 'http://a/reply http://orig/post'.
|
||||||
|
@ -86,16 +90,20 @@ class Response(StringIdModel):
|
||||||
created = ndb.DateTimeProperty(auto_now_add=True)
|
created = ndb.DateTimeProperty(auto_now_add=True)
|
||||||
updated = ndb.DateTimeProperty(auto_now=True)
|
updated = ndb.DateTimeProperty(auto_now=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_kind(cls):
|
||||||
|
return 'Response'
|
||||||
|
|
||||||
def __init__(self, source=None, target=None, **kwargs):
|
def __init__(self, source=None, target=None, **kwargs):
|
||||||
if source and target:
|
if source and target:
|
||||||
assert 'id' not in kwargs
|
assert 'id' not in kwargs
|
||||||
kwargs['id'] = self._id(source, target)
|
kwargs['id'] = self._id(source, target)
|
||||||
logger.info(f"Response id (source target): {kwargs['id']}")
|
logger.info(f"Activity id (source target): {kwargs['id']}")
|
||||||
super(Response, self).__init__(**kwargs)
|
super(Activity, self).__init__(**kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_or_create(cls, source=None, target=None, **kwargs):
|
def get_or_create(cls, source=None, target=None, **kwargs):
|
||||||
logger.info(f'Response source target: {source} {target}')
|
logger.info(f'Activity source target: {source} {target}')
|
||||||
return cls.get_or_insert(cls._id(source, target), **kwargs)
|
return cls.get_or_insert(cls._id(source, target), **kwargs)
|
||||||
|
|
||||||
def source(self):
|
def source(self):
|
||||||
|
@ -105,7 +113,7 @@ class Response(StringIdModel):
|
||||||
return self.key.id().split()[1]
|
return self.key.id().split()[1]
|
||||||
|
|
||||||
def proxy_url(self):
|
def proxy_url(self):
|
||||||
"""Returns the Bridgy Fed proxy URL to render this response as HTML."""
|
"""Returns the Bridgy Fed proxy URL to render this post as HTML."""
|
||||||
if self.source_mf2 or self.source_as2 or self.source_atom:
|
if self.source_mf2 or self.source_as2 or self.source_atom:
|
||||||
source, target = self.key.id().split(' ')
|
source, target = self.key.id().split(' ')
|
||||||
return f'{request.host_url}render?' + urllib.parse.urlencode({
|
return f'{request.host_url}render?' + urllib.parse.urlencode({
|
||||||
|
|
31
pages.py
31
pages.py
|
@ -1,4 +1,4 @@
|
||||||
"""Render recent responses and logs."""
|
"""UI pages."""
|
||||||
import calendar
|
import calendar
|
||||||
import datetime
|
import datetime
|
||||||
from itertools import islice
|
from itertools import islice
|
||||||
|
@ -12,7 +12,7 @@ from oauth_dropins.webutil.flask_util import error
|
||||||
|
|
||||||
from app import app, cache
|
from app import app, cache
|
||||||
import common
|
import common
|
||||||
from models import Follower, MagicKey, Response
|
from models import Follower, Domain, Activity
|
||||||
|
|
||||||
PAGE_SIZE = 20
|
PAGE_SIZE = 20
|
||||||
FOLLOWERS_UI_LIMIT = 999
|
FOLLOWERS_UI_LIMIT = 999
|
||||||
|
@ -28,14 +28,14 @@ def front_page():
|
||||||
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>')
|
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>')
|
||||||
@app.get(f'/responses/<regex("{common.DOMAIN_RE}"):domain>') # deprecated
|
@app.get(f'/responses/<regex("{common.DOMAIN_RE}"):domain>') # deprecated
|
||||||
def user(domain):
|
def user(domain):
|
||||||
if not MagicKey.get_by_id(domain):
|
if not Domain.get_by_id(domain):
|
||||||
return render_template('user_not_found.html', domain=domain), 404
|
return render_template('user_not_found.html', domain=domain), 404
|
||||||
|
|
||||||
query = Response.query(
|
query = Activity.query(
|
||||||
Response.status.IN(('new', 'complete', 'error')),
|
Activity.status.IN(('new', 'complete', 'error')),
|
||||||
Response.domain == domain,
|
Activity.domain == domain,
|
||||||
)
|
)
|
||||||
responses, before, after = fetch_page(query, Response)
|
activities, before, after = fetch_page(query, Activity)
|
||||||
|
|
||||||
followers = Follower.query(Follower.dest == domain)\
|
followers = Follower.query(Follower.dest == domain)\
|
||||||
.count(limit=FOLLOWERS_UI_LIMIT)
|
.count(limit=FOLLOWERS_UI_LIMIT)
|
||||||
|
@ -54,7 +54,10 @@ def user(domain):
|
||||||
|
|
||||||
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>/followers')
|
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>/followers')
|
||||||
def followers(domain):
|
def followers(domain):
|
||||||
if not MagicKey.get_by_id(domain):
|
# TODO:
|
||||||
|
# pull more info from last_follow, eg name, profile picture, url
|
||||||
|
# unify with following
|
||||||
|
if not Domain.get_by_id(domain):
|
||||||
return render_template('user_not_found.html', domain=domain), 404
|
return render_template('user_not_found.html', domain=domain), 404
|
||||||
|
|
||||||
query = Follower.query(
|
query = Follower.query(
|
||||||
|
@ -79,7 +82,7 @@ def followers(domain):
|
||||||
|
|
||||||
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>/following')
|
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>/following')
|
||||||
def following(domain):
|
def following(domain):
|
||||||
if not MagicKey.get_by_id(domain):
|
if not Domain.get_by_id(domain):
|
||||||
return render_template('user_not_found.html', domain=domain), 404
|
return render_template('user_not_found.html', domain=domain), 404
|
||||||
|
|
||||||
query = Follower.query(
|
query = Follower.query(
|
||||||
|
@ -105,9 +108,9 @@ def following(domain):
|
||||||
@app.get('/recent')
|
@app.get('/recent')
|
||||||
@app.get('/responses') # deprecated
|
@app.get('/responses') # deprecated
|
||||||
def recent():
|
def recent():
|
||||||
"""Renders recent Responses, with links to logs."""
|
"""Renders recent activities, with links to logs."""
|
||||||
query = Response.query(Response.status.IN(('new', 'complete', 'error')))
|
query = Activity.query(Activity.status.IN(('new', 'complete', 'error')))
|
||||||
responses, before, after = fetch_page(query, Response)
|
activities, before, after = fetch_page(query, Activity)
|
||||||
return render_template(
|
return render_template(
|
||||||
'recent.html',
|
'recent.html',
|
||||||
util=util,
|
util=util,
|
||||||
|
@ -186,8 +189,8 @@ def fetch_page(query, model_class):
|
||||||
def stats():
|
def stats():
|
||||||
return render_template(
|
return render_template(
|
||||||
'stats.html',
|
'stats.html',
|
||||||
users=KindStat.query(KindStat.kind_name == 'MagicKey').get().count,
|
users=KindStat.query(KindStat.kind_name == 'Domain').get().count,
|
||||||
responses=KindStat.query(KindStat.kind_name == 'Response').get().count,
|
activities=KindStat.query(KindStat.kind_name == 'Activity').get().count,
|
||||||
followers=KindStat.query(KindStat.kind_name == 'Follower').get().count,
|
followers=KindStat.query(KindStat.kind_name == 'Follower').get().count,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ from werkzeug.exceptions import abort
|
||||||
|
|
||||||
from app import app, cache
|
from app import app, cache
|
||||||
import common
|
import common
|
||||||
from models import MagicKey
|
from models import Domain
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -51,8 +51,8 @@ def redir(to):
|
||||||
util.domain_from_link(to, minimize=False),
|
util.domain_from_link(to, minimize=False),
|
||||||
urllib.parse.urlparse(to).hostname))
|
urllib.parse.urlparse(to).hostname))
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
if domain and MagicKey.get_by_id(domain):
|
if domain and Domain.get_by_id(domain):
|
||||||
logger.info(f'Found MagicKey for domain {domain}')
|
logger.info(f'Found Domain for domain {domain}')
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
logger.info(f'No user found for any of {domains}; returning 404')
|
logger.info(f'No user found for any of {domains}; returning 404')
|
||||||
|
|
26
render.py
26
render.py
|
@ -1,5 +1,5 @@
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
"""Renders mf2 proxy pages based on stored Responses."""
|
"""Renders mf2 proxy pages based on stored Activity entities."""
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
@ -10,7 +10,7 @@ from oauth_dropins.webutil.util import json_loads
|
||||||
|
|
||||||
from app import app, cache
|
from app import app, cache
|
||||||
import common
|
import common
|
||||||
from models import Response
|
from models import Activity
|
||||||
|
|
||||||
CACHE_TIME = datetime.timedelta(minutes=15)
|
CACHE_TIME = datetime.timedelta(minutes=15)
|
||||||
|
|
||||||
|
@ -18,23 +18,23 @@ CACHE_TIME = datetime.timedelta(minutes=15)
|
||||||
@app.get('/render')
|
@app.get('/render')
|
||||||
@flask_util.cached(cache, CACHE_TIME)
|
@flask_util.cached(cache, CACHE_TIME)
|
||||||
def render():
|
def render():
|
||||||
"""Fetches a stored Response and renders it as HTML."""
|
"""Fetches a stored Activity and renders it as HTML."""
|
||||||
source = flask_util.get_required_param('source')
|
source = flask_util.get_required_param('source')
|
||||||
target = flask_util.get_required_param('target')
|
target = flask_util.get_required_param('target')
|
||||||
|
|
||||||
id = f'{source} {target}'
|
id = f'{source} {target}'
|
||||||
resp = Response.get_by_id(id)
|
activity = Activity.get_by_id(id)
|
||||||
if not resp:
|
if not activity:
|
||||||
error(f'No stored response for {id}', status=404)
|
error(f'No stored activity for {id}', status=404)
|
||||||
|
|
||||||
if resp.source_mf2:
|
if activity.source_mf2:
|
||||||
as1 = microformats2.json_to_object(json_loads(resp.source_mf2))
|
as1 = microformats2.json_to_object(json_loads(activity.source_mf2))
|
||||||
elif resp.source_as2:
|
elif activity.source_as2:
|
||||||
as1 = as2.to_as1(json_loads(resp.source_as2))
|
as1 = as2.to_as1(json_loads(activity.source_as2))
|
||||||
elif resp.source_atom:
|
elif activity.source_atom:
|
||||||
as1 = atom.atom_to_activity(resp.source_atom)
|
as1 = atom.atom_to_activity(activity.source_atom)
|
||||||
else:
|
else:
|
||||||
error(f'Stored response for {id} has no data', status=404)
|
error(f'Stored activity for {id} has no data', status=404)
|
||||||
|
|
||||||
# add HTML meta redirect to source page. should trigger for end users in
|
# add HTML meta redirect to source page. should trigger for end users in
|
||||||
# browsers but not for webmention receivers (hopefully).
|
# browsers but not for webmention receivers (hopefully).
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!-- <table> -->
|
||||||
|
<!-- <tr><th>Source</th> <th>Target</th> <th>Protocol</th> <th>Status</th> <th>Time (click for log)</th></tr> -->
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{% for a in activities %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3">{{ util.pretty_link(a.source())|safe }}</div>
|
||||||
|
<div class="col-sm-3">{{ util.pretty_link(a.target())|safe }}</div>
|
||||||
|
<div class="col-sm-2">{{ a.protocol }}</div>
|
||||||
|
<div class="col-sm-2">{{ a.status }}</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
{% if a.log_url_path %}<a href="{{ a.log_url_path }}">{% endif %}
|
||||||
|
{{ a.updated.replace(microsecond=0) }}
|
||||||
|
{% if a.log_url_path %}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row">None</div>
|
||||||
|
{% endfor %}
|
||||||
|
<!-- </table> -->
|
||||||
|
|
||||||
|
{% include "paging.html" %}
|
|
@ -1,13 +1,13 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
{% if after %}
|
{% if after %}
|
||||||
<a href="?after={{ after }}#responses">← Newer</a>
|
<a href="?after={{ after }}">← Newer</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3 col-sm-offset-6">
|
<div class="col-sm-3 col-sm-offset-6">
|
||||||
{% if before %}
|
{% if before %}
|
||||||
<a href="?before={{ before }}#responses">Older →</a>
|
<a href="?before={{ before }}">Older →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,6 @@
|
||||||
|
|
||||||
<h2 class="row">Recent activity</h3>
|
<h2 class="row">Recent activity</h3>
|
||||||
|
|
||||||
{% include "responses.html" %}
|
{% include "activities.html" %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
<!-- <table> -->
|
|
||||||
<!-- <tr><th>Source</th> <th>Target</th> <th>Protocol</th> <th>Status</th> <th>Time (click for log)</th></tr> -->
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
{% for r in responses %}
|
|
||||||
<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 %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="row">None</div>
|
|
||||||
{% endfor %}
|
|
||||||
<!-- </table> -->
|
|
||||||
|
|
||||||
{% include "paging.html" %}
|
|
|
@ -12,7 +12,7 @@
|
||||||
<li><a href="https://snarfed.org/2017-10-22_bridgy-fed">Launched Octover 17, 2022</a>
|
<li><a href="https://snarfed.org/2017-10-22_bridgy-fed">Launched Octover 17, 2022</a>
|
||||||
<li>{{ users }} users</li>
|
<li>{{ users }} users</li>
|
||||||
<li>{{ followers }} fediverse followers</li>
|
<li>{{ followers }} fediverse followers</li>
|
||||||
<li>{{ responses }} activities handled</li>
|
<li>{{ activities }} activities handled</li>
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -16,6 +16,6 @@
|
||||||
|
|
||||||
<h3 class="row">Recent activity</h3>
|
<h3 class="row">Recent activity</h3>
|
||||||
|
|
||||||
{% include "responses.html" %}
|
{% include "activities.html" %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -14,7 +14,7 @@ from urllib3.exceptions import ReadTimeoutError
|
||||||
|
|
||||||
import activitypub
|
import activitypub
|
||||||
import common
|
import common
|
||||||
from models import Follower, MagicKey, Response
|
from models import Follower, Domain, Activity
|
||||||
from . import testutil
|
from . import testutil
|
||||||
|
|
||||||
REPLY_OBJECT = {
|
REPLY_OBJECT = {
|
||||||
|
@ -173,7 +173,7 @@ class ActivityPubTest(testutil.TestCase):
|
||||||
'publicKey': {
|
'publicKey': {
|
||||||
'id': 'http://localhost/foo.com',
|
'id': 'http://localhost/foo.com',
|
||||||
'owner': 'http://localhost/foo.com',
|
'owner': 'http://localhost/foo.com',
|
||||||
'publicKeyPem': MagicKey.get_by_id('foo.com').public_pem().decode(),
|
'publicKeyPem': Domain.get_by_id('foo.com').public_pem().decode(),
|
||||||
},
|
},
|
||||||
}, got.json)
|
}, got.json)
|
||||||
|
|
||||||
|
@ -237,7 +237,7 @@ class ActivityPubTest(testutil.TestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
resp = Response.get_by_id('http://this/reply http://orig/post')
|
resp = Activity.get_by_id('http://this/reply http://orig/post')
|
||||||
self.assertEqual('orig', resp.domain)
|
self.assertEqual('orig', resp.domain)
|
||||||
self.assertEqual('in', resp.direction)
|
self.assertEqual('in', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
@ -257,7 +257,7 @@ class ActivityPubTest(testutil.TestCase):
|
||||||
self.assert_req(mock_head, 'http://this', allow_redirects=True)
|
self.assert_req(mock_head, 'http://this', allow_redirects=True)
|
||||||
mock_get.assert_not_called()
|
mock_get.assert_not_called()
|
||||||
mock_post.assert_not_called()
|
mock_post.assert_not_called()
|
||||||
self.assertEqual(0, Response.query().count())
|
self.assertEqual(0, Activity.query().count())
|
||||||
|
|
||||||
def test_inbox_mention_object(self, *mocks):
|
def test_inbox_mention_object(self, *mocks):
|
||||||
self._test_inbox_mention(MENTION_OBJECT, *mocks)
|
self._test_inbox_mention(MENTION_OBJECT, *mocks)
|
||||||
|
@ -286,7 +286,7 @@ class ActivityPubTest(testutil.TestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
resp = Response.get_by_id('http://this/mention http://target/')
|
resp = Activity.get_by_id('http://this/mention http://target/')
|
||||||
self.assertEqual('target', resp.domain)
|
self.assertEqual('target', resp.domain)
|
||||||
self.assertEqual('in', resp.direction)
|
self.assertEqual('in', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
@ -319,7 +319,7 @@ class ActivityPubTest(testutil.TestCase):
|
||||||
'target': 'http://orig/post',
|
'target': 'http://orig/post',
|
||||||
}, kwargs['data'])
|
}, kwargs['data'])
|
||||||
|
|
||||||
resp = Response.get_by_id('http://this/like__ok http://orig/post')
|
resp = Activity.get_by_id('http://this/like__ok http://orig/post')
|
||||||
self.assertEqual('orig', resp.domain)
|
self.assertEqual('orig', resp.domain)
|
||||||
self.assertEqual('in', resp.direction)
|
self.assertEqual('in', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
@ -358,7 +358,7 @@ class ActivityPubTest(testutil.TestCase):
|
||||||
'target': 'https://www.realize.be/',
|
'target': 'https://www.realize.be/',
|
||||||
}, kwargs['data'])
|
}, kwargs['data'])
|
||||||
|
|
||||||
resp = Response.get_by_id('https://mastodon.social/6d1a https://www.realize.be/')
|
resp = Activity.get_by_id('https://mastodon.social/6d1a https://www.realize.be/')
|
||||||
self.assertEqual('www.realize.be', resp.domain)
|
self.assertEqual('www.realize.be', resp.domain)
|
||||||
self.assertEqual('in', resp.direction)
|
self.assertEqual('in', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
@ -443,7 +443,7 @@ class ActivityPubTest(testutil.TestCase):
|
||||||
got = self.client.post('/foo.com/inbox', json=LIKE)
|
got = self.client.post('/foo.com/inbox', json=LIKE)
|
||||||
self.assertEqual(200, got.status_code)
|
self.assertEqual(200, got.status_code)
|
||||||
|
|
||||||
resp = Response.get_by_id('http://this/like__ok http://orig/post')
|
resp = Activity.get_by_id('http://this/like__ok http://orig/post')
|
||||||
self.assertEqual('orig', resp.domain)
|
self.assertEqual('orig', resp.domain)
|
||||||
self.assertEqual('in', resp.direction)
|
self.assertEqual('in', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
"""Unit tests for models.py."""
|
"""Unit tests for models.py."""
|
||||||
from app import app
|
from app import app
|
||||||
from models import MagicKey, Response
|
from models import Domain, Activity
|
||||||
from . import testutil
|
from . import testutil
|
||||||
|
|
||||||
|
|
||||||
class MagicKeyTest(testutil.TestCase):
|
class DomainTest(testutil.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(MagicKeyTest, self).setUp()
|
super(DomainTest, self).setUp()
|
||||||
self.key = MagicKey.get_or_create('y.z')
|
self.key = Domain.get_or_create('y.z')
|
||||||
|
|
||||||
def test_magic_key_get_or_create(self):
|
def test_magic_key_get_or_create(self):
|
||||||
assert self.key.mod
|
assert self.key.mod
|
||||||
assert self.key.public_exponent
|
assert self.key.public_exponent
|
||||||
assert self.key.private_exponent
|
assert self.key.private_exponent
|
||||||
|
|
||||||
same = MagicKey.get_or_create('y.z')
|
same = Domain.get_or_create('y.z')
|
||||||
self.assertEqual(same, self.key)
|
self.assertEqual(same, self.key)
|
||||||
|
|
||||||
def test_href(self):
|
def test_href(self):
|
||||||
|
@ -36,25 +36,25 @@ class MagicKeyTest(testutil.TestCase):
|
||||||
self.assertTrue(pem.decode().endswith('-----END RSA PRIVATE KEY-----'), pem)
|
self.assertTrue(pem.decode().endswith('-----END RSA PRIVATE KEY-----'), pem)
|
||||||
|
|
||||||
|
|
||||||
class ResponseTest(testutil.TestCase):
|
class ActivityTest(testutil.TestCase):
|
||||||
|
|
||||||
def test_constructor(self):
|
def test_constructor(self):
|
||||||
resp = Response('abc', 'xyz')
|
resp = Activity('abc', 'xyz')
|
||||||
self.assertEqual('abc xyz', resp.key.id())
|
self.assertEqual('abc xyz', resp.key.id())
|
||||||
|
|
||||||
resp = Response('abc#1', 'xyz#Z')
|
resp = Activity('abc#1', 'xyz#Z')
|
||||||
self.assertEqual('abc__1 xyz__Z', resp.key.id())
|
self.assertEqual('abc__1 xyz__Z', resp.key.id())
|
||||||
|
|
||||||
def test_get_or_create(self):
|
def test_get_or_create(self):
|
||||||
resp = Response.get_or_create('abc', 'xyz')
|
resp = Activity.get_or_create('abc', 'xyz')
|
||||||
self.assertEqual('abc xyz', resp.key.id())
|
self.assertEqual('abc xyz', resp.key.id())
|
||||||
|
|
||||||
resp = Response.get_or_create('abc#1', 'xyz#Z')
|
resp = Activity.get_or_create('abc#1', 'xyz#Z')
|
||||||
self.assertEqual('abc__1 xyz__Z', resp.key.id())
|
self.assertEqual('abc__1 xyz__Z', resp.key.id())
|
||||||
|
|
||||||
def test_proxy_url(self):
|
def test_proxy_url(self):
|
||||||
with app.test_request_context('/'):
|
with app.test_request_context('/'):
|
||||||
resp = Response.get_or_create('abc', 'xyz')
|
resp = Activity.get_or_create('abc', 'xyz')
|
||||||
self.assertIsNone(resp.proxy_url())
|
self.assertIsNone(resp.proxy_url())
|
||||||
|
|
||||||
resp.source_as2 = 'as2'
|
resp.source_as2 = 'as2'
|
||||||
|
|
|
@ -6,7 +6,7 @@ from unittest.mock import patch
|
||||||
from oauth_dropins.webutil.testutil import requests_response
|
from oauth_dropins.webutil.testutil import requests_response
|
||||||
|
|
||||||
import common
|
import common
|
||||||
from models import MagicKey
|
from models import Domain
|
||||||
from .test_webmention import REPOST_HTML, REPOST_AS2
|
from .test_webmention import REPOST_HTML, REPOST_AS2
|
||||||
from . import testutil
|
from . import testutil
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class RedirectTest(testutil.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
MagicKey.get_or_create('foo.com')
|
Domain.get_or_create('foo.com')
|
||||||
|
|
||||||
def test_redirect(self):
|
def test_redirect(self):
|
||||||
got = self.client.get('/r/https://foo.com/bar?baz=baj&biff')
|
got = self.client.get('/r/https://foo.com/bar?baz=baj&biff')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"""Unit tests for render.py."""
|
"""Unit tests for render.py."""
|
||||||
from oauth_dropins.webutil.util import json_dumps
|
from oauth_dropins.webutil.util import json_dumps
|
||||||
|
|
||||||
from models import Response
|
from models import Activity
|
||||||
import render
|
import render
|
||||||
from . import testutil
|
from . import testutil
|
||||||
|
|
||||||
|
@ -61,31 +61,31 @@ class RenderTest(testutil.TestCase):
|
||||||
resp = self.client.get(f'/render?source={source}&target={target}')
|
resp = self.client.get(f'/render?source={source}&target={target}')
|
||||||
self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))
|
self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))
|
||||||
|
|
||||||
# no Response
|
# no Activity
|
||||||
resp = self.client.get('/render?source=abc&target=xyz')
|
resp = self.client.get('/render?source=abc&target=xyz')
|
||||||
self.assertEqual(404, resp.status_code)
|
self.assertEqual(404, resp.status_code)
|
||||||
|
|
||||||
# no source data
|
# no source data
|
||||||
Response(id='abc xyz').put()
|
Activity(id='abc xyz').put()
|
||||||
resp = self.client.get('/render?source=abc&target=xyz')
|
resp = self.client.get('/render?source=abc&target=xyz')
|
||||||
self.assertEqual(404, resp.status_code)
|
self.assertEqual(404, resp.status_code)
|
||||||
|
|
||||||
def test_render_as2(self):
|
def test_render_as2(self):
|
||||||
Response(id='abc xyz', source_as2=json_dumps(self.as2)).put()
|
Activity(id='abc xyz', source_as2=json_dumps(self.as2)).put()
|
||||||
resp = self.client.get('/render?source=abc&target=xyz')
|
resp = self.client.get('/render?source=abc&target=xyz')
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
self.assert_multiline_equals(self.html, resp.get_data(as_text=True),
|
self.assert_multiline_equals(self.html, resp.get_data(as_text=True),
|
||||||
ignore_blanks=True)
|
ignore_blanks=True)
|
||||||
|
|
||||||
def test_render_mf2(self):
|
def test_render_mf2(self):
|
||||||
Response(id='abc xyz', source_mf2=json_dumps(self.mf2)).put()
|
Activity(id='abc xyz', source_mf2=json_dumps(self.mf2)).put()
|
||||||
resp = self.client.get('/render?source=abc&target=xyz')
|
resp = self.client.get('/render?source=abc&target=xyz')
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
self.assert_multiline_equals(self.html, resp.get_data(as_text=True),
|
self.assert_multiline_equals(self.html, resp.get_data(as_text=True),
|
||||||
ignore_blanks=True)
|
ignore_blanks=True)
|
||||||
|
|
||||||
def test_render_atom(self):
|
def test_render_atom(self):
|
||||||
Response(id='abc xyz', source_atom=self.atom).put()
|
Activity(id='abc xyz', source_atom=self.atom).put()
|
||||||
resp = self.client.get('/render?source=abc&target=xyz')
|
resp = self.client.get('/render?source=abc&target=xyz')
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
self.assert_multiline_equals(self.html, resp.get_data(as_text=True),
|
self.assert_multiline_equals(self.html, resp.get_data(as_text=True),
|
||||||
|
|
|
@ -12,7 +12,7 @@ from oauth_dropins.webutil.testutil import requests_response, UrlopenResult
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import common
|
import common
|
||||||
from models import MagicKey, Response
|
from models import Domain, Activity
|
||||||
from . import testutil
|
from . import testutil
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class SalmonTest(testutil.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.key = MagicKey.get_or_create('alice')
|
self.key = Domain.get_or_create('alice')
|
||||||
|
|
||||||
def send_slap(self, mock_urlopen, mock_head, mock_get, mock_post, atom_slap):
|
def send_slap(self, mock_urlopen, mock_head, mock_get, mock_post, atom_slap):
|
||||||
# salmon magic key discovery. first host-meta, then webfinger
|
# salmon magic key discovery. first host-meta, then webfinger
|
||||||
|
@ -89,8 +89,8 @@ class SalmonTest(testutil.TestCase):
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
headers={'Accept': '*/*'})
|
headers={'Accept': '*/*'})
|
||||||
|
|
||||||
# check stored response
|
# check stored post
|
||||||
resp = Response.get_by_id('https://my/reply http://orig/post')
|
resp = Activity.get_by_id('https://my/reply http://orig/post')
|
||||||
self.assertEqual('orig', resp.domain)
|
self.assertEqual('orig', resp.domain)
|
||||||
self.assertEqual('in', resp.direction)
|
self.assertEqual('in', resp.direction)
|
||||||
self.assertEqual('ostatus', resp.protocol)
|
self.assertEqual('ostatus', resp.protocol)
|
||||||
|
@ -124,8 +124,8 @@ class SalmonTest(testutil.TestCase):
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
headers={'Accept': '*/*'})
|
headers={'Accept': '*/*'})
|
||||||
|
|
||||||
# check stored response
|
# check stored post
|
||||||
resp = Response.get_by_id('https://my/like http://orig/post')
|
resp = Activity.get_by_id('https://my/like http://orig/post')
|
||||||
self.assertEqual('orig', resp.domain)
|
self.assertEqual('orig', resp.domain)
|
||||||
self.assertEqual('in', resp.direction)
|
self.assertEqual('in', resp.direction)
|
||||||
self.assertEqual('ostatus', resp.protocol)
|
self.assertEqual('ostatus', resp.protocol)
|
||||||
|
|
|
@ -32,7 +32,7 @@ class WebfingerTest(testutil.TestCase):
|
||||||
</a>
|
</a>
|
||||||
</body>
|
</body>
|
||||||
"""
|
"""
|
||||||
self.key = models.MagicKey.get_or_create('foo.com')
|
self.key = models.Domain.get_or_create('foo.com')
|
||||||
self.expected_webfinger = {
|
self.expected_webfinger = {
|
||||||
'subject': 'acct:foo.com@foo.com',
|
'subject': 'acct:foo.com@foo.com',
|
||||||
'aliases': [
|
'aliases': [
|
||||||
|
|
|
@ -26,7 +26,7 @@ from common import (
|
||||||
CONTENT_TYPE_HTML,
|
CONTENT_TYPE_HTML,
|
||||||
CONTENT_TYPE_MAGIC_ENVELOPE,
|
CONTENT_TYPE_MAGIC_ENVELOPE,
|
||||||
)
|
)
|
||||||
from models import Follower, MagicKey, Response
|
from models import Follower, Domain, Activity
|
||||||
import webmention
|
import webmention
|
||||||
from . import testutil
|
from . import testutil
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ REPOST_AS2 = {
|
||||||
class WebmentionTest(testutil.TestCase):
|
class WebmentionTest(testutil.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.key = MagicKey.get_or_create('a')
|
self.key = Domain.get_or_create('a')
|
||||||
|
|
||||||
self.orig_html_as2 = requests_response("""\
|
self.orig_html_as2 = requests_response("""\
|
||||||
<html>
|
<html>
|
||||||
|
@ -284,7 +284,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
mock_get.side_effect = ValueError('foo bar')
|
mock_get.side_effect = ValueError('foo bar')
|
||||||
got = self.client.post('/webmention', data={'source': 'bad'})
|
got = self.client.post('/webmention', data={'source': 'bad'})
|
||||||
self.assertEqual(400, got.status_code)
|
self.assertEqual(400, got.status_code)
|
||||||
self.assertEqual(0, Response.query().count())
|
self.assertEqual(0, Activity.query().count())
|
||||||
|
|
||||||
def test_no_source_entry(self, mock_get, mock_post):
|
def test_no_source_entry(self, mock_get, mock_post):
|
||||||
mock_get.return_value = requests_response("""
|
mock_get.return_value = requests_response("""
|
||||||
|
@ -299,7 +299,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
'target': 'https://fed.brid.gy/',
|
'target': 'https://fed.brid.gy/',
|
||||||
})
|
})
|
||||||
self.assertEqual(400, got.status_code)
|
self.assertEqual(400, got.status_code)
|
||||||
self.assertEqual(0, Response.query().count())
|
self.assertEqual(0, Activity.query().count())
|
||||||
|
|
||||||
mock_get.assert_has_calls((self.req('http://a/post'),))
|
mock_get.assert_has_calls((self.req('http://a/post'),))
|
||||||
|
|
||||||
|
@ -316,7 +316,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
'target': 'https://fed.brid.gy/',
|
'target': 'https://fed.brid.gy/',
|
||||||
})
|
})
|
||||||
self.assertEqual(200, got.status_code)
|
self.assertEqual(200, got.status_code)
|
||||||
self.assertEqual(0, Response.query().count())
|
self.assertEqual(0, Activity.query().count())
|
||||||
|
|
||||||
mock_get.assert_has_calls((self.req('http://a/post'),))
|
mock_get.assert_has_calls((self.req('http://a/post'),))
|
||||||
|
|
||||||
|
@ -328,7 +328,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
|
|
||||||
got = self.client.post('/webmention', data={'source': 'http://a/post'})
|
got = self.client.post('/webmention', data={'source': 'http://a/post'})
|
||||||
self.assertEqual(400, got.status_code)
|
self.assertEqual(400, got.status_code)
|
||||||
self.assertEqual(0, Response.query().count())
|
self.assertEqual(0, Activity.query().count())
|
||||||
|
|
||||||
def test_source_fetch_fails(self, mock_get, mock_post):
|
def test_source_fetch_fails(self, mock_get, mock_post):
|
||||||
mock_get.side_effect = (
|
mock_get.side_effect = (
|
||||||
|
@ -350,7 +350,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
)
|
)
|
||||||
got = self.client.post('/webmention', data={'source': 'http://a/post'})
|
got = self.client.post('/webmention', data={'source': 'http://a/post'})
|
||||||
self.assertEqual(502, got.status_code)
|
self.assertEqual(502, got.status_code)
|
||||||
self.assertEqual(0, Response.query().count())
|
self.assertEqual(0, Activity.query().count())
|
||||||
|
|
||||||
def test_no_backlink(self, mock_get, mock_post):
|
def test_no_backlink(self, mock_get, mock_post):
|
||||||
mock_get.return_value = requests_response(
|
mock_get.return_value = requests_response(
|
||||||
|
@ -362,7 +362,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
'target': 'https://fed.brid.gy/',
|
'target': 'https://fed.brid.gy/',
|
||||||
})
|
})
|
||||||
self.assertEqual(400, got.status_code)
|
self.assertEqual(400, got.status_code)
|
||||||
self.assertEqual(0, Response.query().count())
|
self.assertEqual(0, Activity.query().count())
|
||||||
|
|
||||||
mock_get.assert_has_calls((self.req('http://a/post'),))
|
mock_get.assert_has_calls((self.req('http://a/post'),))
|
||||||
|
|
||||||
|
@ -405,7 +405,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||||
self.assertEqual(self.key.private_pem(), rsa_key.exportKey())
|
self.assertEqual(self.key.private_pem(), rsa_key.exportKey())
|
||||||
|
|
||||||
resp = Response.get_by_id('http://a/reply http://orig/as2')
|
resp = Activity.get_by_id('http://a/reply http://orig/as2')
|
||||||
self.assertEqual('a', resp.domain)
|
self.assertEqual('a', resp.domain)
|
||||||
self.assertEqual('out', resp.direction)
|
self.assertEqual('out', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
@ -419,7 +419,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
# self.assertEqual(['abc xyz'], resp.responses)
|
# self.assertEqual(['abc xyz'], resp.responses)
|
||||||
|
|
||||||
def test_activitypub_update_reply(self, mock_get, mock_post):
|
def test_activitypub_update_reply(self, mock_get, mock_post):
|
||||||
Response(id='http://a/reply http://orig/as2', status='complete').put()
|
Activity(id='http://a/reply http://orig/as2', status='complete').put()
|
||||||
|
|
||||||
mock_get.side_effect = self.activitypub_gets
|
mock_get.side_effect = self.activitypub_gets
|
||||||
mock_post.return_value = requests_response('abc xyz')
|
mock_post.return_value = requests_response('abc xyz')
|
||||||
|
@ -436,7 +436,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
|
|
||||||
def test_activitypub_skip_update_if_content_unchanged(self, mock_get, mock_post):
|
def test_activitypub_skip_update_if_content_unchanged(self, mock_get, mock_post):
|
||||||
"""https://github.com/snarfed/bridgy-fed/issues/78"""
|
"""https://github.com/snarfed/bridgy-fed/issues/78"""
|
||||||
Response(id='http://a/reply http://orig/as2', status='complete',
|
Activity(id='http://a/reply http://orig/as2', status='complete',
|
||||||
source_mf2=json_dumps(self.reply_mf2)).put()
|
source_mf2=json_dumps(self.reply_mf2)).put()
|
||||||
|
|
||||||
mock_get.side_effect = self.activitypub_gets
|
mock_get.side_effect = self.activitypub_gets
|
||||||
|
@ -508,7 +508,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||||
self.assertEqual(self.key.private_pem(), rsa_key.exportKey())
|
self.assertEqual(self.key.private_pem(), rsa_key.exportKey())
|
||||||
|
|
||||||
resp = Response.get_by_id('http://a/repost http://orig/as2')
|
resp = Activity.get_by_id('http://a/repost http://orig/as2')
|
||||||
self.assertEqual('a', resp.domain)
|
self.assertEqual('a', resp.domain)
|
||||||
self.assertEqual('out', resp.direction)
|
self.assertEqual('out', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
@ -605,12 +605,12 @@ class WebmentionTest(testutil.TestCase):
|
||||||
mock_get.side_effect = [self.create, self.actor]
|
mock_get.side_effect = [self.create, self.actor]
|
||||||
mock_post.return_value = requests_response('abc xyz')
|
mock_post.return_value = requests_response('abc xyz')
|
||||||
|
|
||||||
Response(id='http://orig/post https://skipped/inbox', domain='orig',
|
Activity(id='http://orig/post https://skipped/inbox', domain='orig',
|
||||||
status='complete', source_mf2=json_dumps(self.create_mf2)).put()
|
status='complete', source_mf2=json_dumps(self.create_mf2)).put()
|
||||||
|
|
||||||
different_create_mf2 = copy.deepcopy(self.create_mf2)
|
different_create_mf2 = copy.deepcopy(self.create_mf2)
|
||||||
different_create_mf2['items'][0]['properties']['content'][0]['value'] += ' different'
|
different_create_mf2['items'][0]['properties']['content'][0]['value'] += ' different'
|
||||||
Response(id='http://orig/post https://updated/inbox', domain='orig',
|
Activity(id='http://orig/post https://updated/inbox', domain='orig',
|
||||||
status='complete', direction='out', protocol='activitypub',
|
status='complete', direction='out', protocol='activitypub',
|
||||||
source_mf2=json_dumps(different_create_mf2)).put()
|
source_mf2=json_dumps(different_create_mf2)).put()
|
||||||
|
|
||||||
|
@ -672,7 +672,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
self.update_as2 if inbox == 'https://updated/inbox' else self.create_as2,
|
self.update_as2 if inbox == 'https://updated/inbox' else self.create_as2,
|
||||||
json_loads(call[1]['data']))
|
json_loads(call[1]['data']))
|
||||||
|
|
||||||
resp = Response.get_by_id('http://orig/post %s' % inbox)
|
resp = Activity.get_by_id('http://orig/post %s' % inbox)
|
||||||
self.assertEqual('orig', resp.domain)
|
self.assertEqual('orig', resp.domain)
|
||||||
self.assertEqual('out', resp.direction, inbox)
|
self.assertEqual('out', resp.direction, inbox)
|
||||||
self.assertEqual('activitypub', resp.protocol, inbox)
|
self.assertEqual('activitypub', resp.protocol, inbox)
|
||||||
|
@ -733,7 +733,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||||
self.assertEqual(self.key.private_pem(), rsa_key.exportKey())
|
self.assertEqual(self.key.private_pem(), rsa_key.exportKey())
|
||||||
|
|
||||||
resp = Response.get_by_id('http://a/follow http://followee/')
|
resp = Activity.get_by_id('http://a/follow http://followee/')
|
||||||
self.assertEqual('a', resp.domain)
|
self.assertEqual('a', resp.domain)
|
||||||
self.assertEqual('out', resp.direction)
|
self.assertEqual('out', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
@ -776,7 +776,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||||
self.assertEqual(self.key.private_pem(), rsa_key.exportKey())
|
self.assertEqual(self.key.private_pem(), rsa_key.exportKey())
|
||||||
|
|
||||||
resp = Response.get_by_id('http://a/follow http://followee/')
|
resp = Activity.get_by_id('http://a/follow http://followee/')
|
||||||
self.assertEqual('a', resp.domain)
|
self.assertEqual('a', resp.domain)
|
||||||
self.assertEqual('out', resp.direction)
|
self.assertEqual('out', resp.direction)
|
||||||
self.assertEqual('activitypub', resp.protocol)
|
self.assertEqual('activitypub', resp.protocol)
|
||||||
|
@ -821,7 +821,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
<a href="http://localhost/"></a>""",
|
<a href="http://localhost/"></a>""",
|
||||||
entry.content[0]['value'])
|
entry.content[0]['value'])
|
||||||
|
|
||||||
resp = Response.get_by_id('http://a/reply http://orig/post')
|
resp = Activity.get_by_id('http://a/reply http://orig/post')
|
||||||
self.assertEqual('a', resp.domain)
|
self.assertEqual('a', resp.domain)
|
||||||
self.assertEqual('out', resp.direction)
|
self.assertEqual('out', resp.direction)
|
||||||
self.assertEqual('ostatus', resp.protocol)
|
self.assertEqual('ostatus', resp.protocol)
|
||||||
|
@ -855,7 +855,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
}, entry['links'])
|
}, entry['links'])
|
||||||
self.assertEqual('http://orig/post', entry['activity_object'])
|
self.assertEqual('http://orig/post', entry['activity_object'])
|
||||||
|
|
||||||
resp = Response.get_by_id('http://a/like http://orig/post')
|
resp = Activity.get_by_id('http://a/like http://orig/post')
|
||||||
self.assertEqual('a', resp.domain)
|
self.assertEqual('a', resp.domain)
|
||||||
self.assertEqual('out', resp.direction)
|
self.assertEqual('out', resp.direction)
|
||||||
self.assertEqual('ostatus', resp.protocol)
|
self.assertEqual('ostatus', resp.protocol)
|
||||||
|
@ -907,7 +907,7 @@ class WebmentionTest(testutil.TestCase):
|
||||||
self.assertIn('Target post http://orig/url has no Atom link',
|
self.assertIn('Target post http://orig/url has no Atom link',
|
||||||
got.get_data(as_text=True))
|
got.get_data(as_text=True))
|
||||||
|
|
||||||
resp = Response.get_by_id('http://a/reply http://orig/url')
|
resp = Activity.get_by_id('http://a/reply http://orig/url')
|
||||||
self.assertEqual('a', resp.domain)
|
self.assertEqual('a', resp.domain)
|
||||||
self.assertEqual('out', resp.direction)
|
self.assertEqual('out', resp.direction)
|
||||||
self.assertEqual('ostatus', resp.protocol)
|
self.assertEqual('ostatus', resp.protocol)
|
||||||
|
|
|
@ -59,7 +59,7 @@ class User(flask_util.XrdOrJrd):
|
||||||
error(f"didn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {resp.url}")
|
error(f"didn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {resp.url}")
|
||||||
|
|
||||||
logger.info(f'Generating WebFinger data for {domain}')
|
logger.info(f'Generating WebFinger data for {domain}')
|
||||||
key = models.MagicKey.get_or_create(domain)
|
key = models.Domain.get_or_create(domain)
|
||||||
props = hcard.get('properties', {})
|
props = hcard.get('properties', {})
|
||||||
urls = util.dedupe_urls(props.get('url', []) + [resp.url])
|
urls = util.dedupe_urls(props.get('url', []) + [resp.url])
|
||||||
canonical_url = urls[0]
|
canonical_url = urls[0]
|
||||||
|
|
|
@ -25,7 +25,7 @@ from werkzeug.exceptions import BadGateway
|
||||||
import activitypub
|
import activitypub
|
||||||
from app import app
|
from app import app
|
||||||
import common
|
import common
|
||||||
from models import Follower, MagicKey, Response
|
from models import Follower, Domain, Activity
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class Webmention(View):
|
||||||
if not targets:
|
if not targets:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
key = MagicKey.get_or_create(self.source_domain)
|
key = Domain.get_or_create(self.source_domain)
|
||||||
error = None
|
error = None
|
||||||
last_success = None
|
last_success = None
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ class Webmention(View):
|
||||||
|
|
||||||
def _activitypub_targets(self):
|
def _activitypub_targets(self):
|
||||||
"""
|
"""
|
||||||
Returns: list of (Response, string inbox URL)
|
Returns: list of (Activity, string inbox URL)
|
||||||
"""
|
"""
|
||||||
# if there's in-reply-to, like-of, or repost-of, they're the targets.
|
# if there's in-reply-to, like-of, or repost-of, they're the targets.
|
||||||
# otherwise, it's all followers' inboxes.
|
# otherwise, it's all followers' inboxes.
|
||||||
|
@ -178,7 +178,7 @@ class Webmention(View):
|
||||||
inboxes.add(actor.get('endpoints', {}).get('sharedInbox') or
|
inboxes.add(actor.get('endpoints', {}).get('sharedInbox') or
|
||||||
actor.get('publicInbox')or
|
actor.get('publicInbox')or
|
||||||
actor.get('inbox'))
|
actor.get('inbox'))
|
||||||
return [(Response.get_or_create(
|
return [(Activity.get_or_create(
|
||||||
source=self.source_url, target=inbox, domain=self.source_domain,
|
source=self.source_url, target=inbox, domain=self.source_domain,
|
||||||
direction='out', protocol='activitypub',
|
direction='out', protocol='activitypub',
|
||||||
source_mf2=json_dumps(self.source_mf2)),
|
source_mf2=json_dumps(self.source_mf2)),
|
||||||
|
@ -200,7 +200,7 @@ class Webmention(View):
|
||||||
raise
|
raise
|
||||||
target_url = self.target_resp.url or target
|
target_url = self.target_resp.url or target
|
||||||
|
|
||||||
resp = Response.get_or_create(
|
resp = Activity.get_or_create(
|
||||||
source=self.source_url, target=target_url, domain=self.source_domain,
|
source=self.source_url, target=target_url, domain=self.source_domain,
|
||||||
direction='out', protocol='activitypub',
|
direction='out', protocol='activitypub',
|
||||||
source_mf2=json_dumps(self.source_mf2))
|
source_mf2=json_dumps(self.source_mf2))
|
||||||
|
@ -266,7 +266,7 @@ class Webmention(View):
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
if status:
|
if status:
|
||||||
Response(source=self.source_url, target=target, status=status,
|
Activity(source=self.source_url, target=target, status=status,
|
||||||
domain=self.source_domain, direction='out',
|
domain=self.source_domain, direction='out',
|
||||||
protocol = 'ostatus',
|
protocol = 'ostatus',
|
||||||
source_mf2=json_dumps(self.source_mf2)).put()
|
source_mf2=json_dumps(self.source_mf2)).put()
|
||||||
|
@ -351,7 +351,7 @@ class Webmention(View):
|
||||||
|
|
||||||
# sign reply and wrap in magic envelope
|
# sign reply and wrap in magic envelope
|
||||||
domain = urllib.parse.urlparse(self.source_url).netloc
|
domain = urllib.parse.urlparse(self.source_url).netloc
|
||||||
key = MagicKey.get_or_create(domain)
|
key = Domain.get_or_create(domain)
|
||||||
logger.info(f'Using key for {domain}: {key}')
|
logger.info(f'Using key for {domain}: {key}')
|
||||||
magic_envelope = magicsigs.magic_envelope(
|
magic_envelope = magicsigs.magic_envelope(
|
||||||
entry, common.CONTENT_TYPE_ATOM, key).decode()
|
entry, common.CONTENT_TYPE_ATOM, key).decode()
|
||||||
|
|
Ładowanie…
Reference in New Issue