forked from forks/microblog.pub
Cleanup, improve the collection resolver
This commit is contained in:
parent
888410d646
commit
25a75a9cef
6 changed files with 99 additions and 41 deletions
|
@ -43,6 +43,14 @@
|
||||||
- Manually tested against [Mastodon](https://github.com/tootsuite/mastodon)
|
- Manually tested against [Mastodon](https://github.com/tootsuite/mastodon)
|
||||||
- Project is running an up-to-date instance
|
- Project is running an up-to-date instance
|
||||||
|
|
||||||
|
## ActivityPub
|
||||||
|
|
||||||
|
microblog.pub implements an [ActivityPub](http://activitypub.rocks/) server, it implements both the client to server API and the federated server to server API.
|
||||||
|
|
||||||
|
Compatible with [Mastodon](https://github.com/tootsuite/mastodon) (which is not following the spec closely), but will drop OStatus messages.
|
||||||
|
|
||||||
|
Activities are verified using HTTP Signatures or by fetching the content on the remote server directly.
|
||||||
|
|
||||||
## Running your instance
|
## Running your instance
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
|
@ -4,13 +4,13 @@ import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
import requests
|
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
from html2text import html2text
|
from html2text import html2text
|
||||||
from feedgen.feed import FeedGenerator
|
from feedgen.feed import FeedGenerator
|
||||||
|
|
||||||
from utils.linked_data_sig import generate_signature
|
from utils.linked_data_sig import generate_signature
|
||||||
from utils.actor_service import NotAnActorError
|
from utils.actor_service import NotAnActorError
|
||||||
|
from utils import activitypub_utils
|
||||||
from config import USERNAME, BASE_URL, ID
|
from config import USERNAME, BASE_URL, ID
|
||||||
from config import CTX_AS, CTX_SECURITY, AS_PUBLIC
|
from config import CTX_AS, CTX_SECURITY, AS_PUBLIC
|
||||||
from config import KEY, DB, ME, ACTOR_SERVICE
|
from config import KEY, DB, ME, ACTOR_SERVICE
|
||||||
|
@ -936,46 +936,7 @@ def parse_collection(payload: Optional[Dict[str, Any]] = None, url: Optional[str
|
||||||
return [doc['remote_actor'] for doc in DB.following.find()]
|
return [doc['remote_actor'] for doc in DB.following.find()]
|
||||||
|
|
||||||
# Go through all the pages
|
# Go through all the pages
|
||||||
out: List[str] = []
|
return activitypub_utils.parse_collection(payload, url)
|
||||||
if url:
|
|
||||||
resp = requests.get(url, headers={'Accept': 'application/activity+json'})
|
|
||||||
resp.raise_for_status()
|
|
||||||
payload = resp.json()
|
|
||||||
|
|
||||||
if not payload:
|
|
||||||
raise ValueError('must at least prove a payload or an URL')
|
|
||||||
|
|
||||||
if payload['type'] in ['Collection', 'OrderedCollection']:
|
|
||||||
if 'orderedItems' in payload:
|
|
||||||
return payload['orderedItems']
|
|
||||||
if 'items' in payload:
|
|
||||||
return payload['items']
|
|
||||||
if 'first' in payload:
|
|
||||||
if 'orderedItems' in payload['first']:
|
|
||||||
out.extend(payload['first']['orderedItems'])
|
|
||||||
if 'items' in payload['first']:
|
|
||||||
out.extend(payload['first']['items'])
|
|
||||||
n = payload['first'].get('next')
|
|
||||||
if n:
|
|
||||||
out.extend(parse_collection(url=n))
|
|
||||||
return out
|
|
||||||
|
|
||||||
while payload:
|
|
||||||
if payload['type'] in ['CollectionPage', 'OrderedCollectionPage']:
|
|
||||||
if 'orderedItems' in payload:
|
|
||||||
out.extend(payload['orderedItems'])
|
|
||||||
if 'items' in payload:
|
|
||||||
out.extend(payload['items'])
|
|
||||||
n = payload.get('next')
|
|
||||||
if n is None:
|
|
||||||
break
|
|
||||||
resp = requests.get(n, headers={'Accept': 'application/activity+json'})
|
|
||||||
resp.raise_for_status()
|
|
||||||
payload = resp.json()
|
|
||||||
else:
|
|
||||||
raise Exception('unexpected activity type {}'.format(payload['type']))
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def build_ordered_collection(col, q=None, cursor=None, map_func=None, limit=50, col_name=None):
|
def build_ordered_collection(col, q=None, cursor=None, map_func=None, limit=50, col_name=None):
|
||||||
|
|
|
@ -4,6 +4,7 @@ import yaml
|
||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from utils import strtobool
|
||||||
from utils.key import Key
|
from utils.key import Key
|
||||||
from utils.actor_service import ActorService
|
from utils.actor_service import ActorService
|
||||||
from utils.object_service import ObjectService
|
from utils.object_service import ObjectService
|
||||||
|
@ -20,6 +21,9 @@ except ModuleNotFoundError:
|
||||||
|
|
||||||
VERSION = subprocess.check_output(['git', 'describe', '--always']).split()[0].decode('utf-8')
|
VERSION = subprocess.check_output(['git', 'describe', '--always']).split()[0].decode('utf-8')
|
||||||
|
|
||||||
|
DEBUG_MODE = strtobool(os.getenv('MICROBLOGPUB_DEBUG', 'false'))
|
||||||
|
|
||||||
|
|
||||||
CTX_AS = 'https://www.w3.org/ns/activitystreams'
|
CTX_AS = 'https://www.w3.org/ns/activitystreams'
|
||||||
CTX_SECURITY = 'https://w3id.org/security/v1'
|
CTX_SECURITY = 'https://w3id.org/security/v1'
|
||||||
AS_PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'
|
AS_PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'
|
||||||
|
|
|
@ -53,6 +53,11 @@ class Instance(object):
|
||||||
|
|
||||||
return resp.json()['first']['orderedItems']
|
return resp.json()['first']['orderedItems']
|
||||||
|
|
||||||
|
def outbox(self):
|
||||||
|
resp = self.session.get(f'{self.host_url}/following', headers={'Accept': 'application/activity+json'})
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
def test_federation():
|
def test_federation():
|
||||||
"""Ensure the homepage is accessible."""
|
"""Ensure the homepage is accessible."""
|
||||||
|
|
65
utils/activitypub_utils.py
Normal file
65
utils/activitypub_utils.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
from typing import Optional, Dict, List, Any
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .errors import RecursionLimitExceededError
|
||||||
|
from .errors import UnexpectedActivityTypeError
|
||||||
|
|
||||||
|
|
||||||
|
def _do_req(url: str, headers: Dict[str, str]) -> Dict[str, Any]:
|
||||||
|
resp = requests.get(url, headers=headers)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_collection(
|
||||||
|
payload: Optional[Dict[str, Any]] = None,
|
||||||
|
url: Optional[str] = None,
|
||||||
|
user_agent: Optional[str] = None,
|
||||||
|
level: int = 0,
|
||||||
|
do_req: Any = _do_req,
|
||||||
|
) -> List[str]:
|
||||||
|
"""Resolve/fetch a `Collection`/`OrderedCollection`."""
|
||||||
|
if level > 3:
|
||||||
|
raise RecursionLimitExceededError('recursion limit exceeded')
|
||||||
|
|
||||||
|
# Go through all the pages
|
||||||
|
headers = {'Accept': 'application/activity+json'}
|
||||||
|
if user_agent:
|
||||||
|
headers['User-Agent'] = user_agent
|
||||||
|
|
||||||
|
out: List[str] = []
|
||||||
|
if url:
|
||||||
|
payload = do_req(url, headers)
|
||||||
|
if not payload:
|
||||||
|
raise ValueError('must at least prove a payload or an URL')
|
||||||
|
|
||||||
|
if payload['type'] in ['Collection', 'OrderedCollection']:
|
||||||
|
if 'orderedItems' in payload:
|
||||||
|
return payload['orderedItems']
|
||||||
|
if 'items' in payload:
|
||||||
|
return payload['items']
|
||||||
|
if 'first' in payload:
|
||||||
|
if 'orderedItems' in payload['first']:
|
||||||
|
out.extend(payload['first']['orderedItems'])
|
||||||
|
if 'items' in payload['first']:
|
||||||
|
out.extend(payload['first']['items'])
|
||||||
|
n = payload['first'].get('next')
|
||||||
|
if n:
|
||||||
|
out.extend(parse_collection(url=n, user_agent=user_agent, level=level+1, do_req=do_req))
|
||||||
|
return out
|
||||||
|
|
||||||
|
while payload:
|
||||||
|
if payload['type'] in ['CollectionPage', 'OrderedCollectionPage']:
|
||||||
|
if 'orderedItems' in payload:
|
||||||
|
out.extend(payload['orderedItems'])
|
||||||
|
if 'items' in payload:
|
||||||
|
out.extend(payload['items'])
|
||||||
|
n = payload.get('next')
|
||||||
|
if n is None:
|
||||||
|
break
|
||||||
|
payload = do_req(n, headers)
|
||||||
|
else:
|
||||||
|
raise UnexpectedActivityTypeError('unexpected activity type {}'.format(payload['type']))
|
||||||
|
|
||||||
|
return out
|
15
utils/errors.py
Normal file
15
utils/errors.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadActivityError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RecursionLimitExceededError(BadActivityError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnexpectedActivityTypeError(BadActivityError):
|
||||||
|
pass
|
Loading…
Reference in a new issue