kopia lustrzana https://github.com/tsileo/little-boxes
Initial commit
commit
d5afc0db09
|
@ -0,0 +1,7 @@
|
||||||
|
*.sw[op]
|
||||||
|
key_*.pem
|
||||||
|
|
||||||
|
*.egg-info
|
||||||
|
.pytest_cache
|
||||||
|
.mypy_cache/
|
||||||
|
__pycache__/
|
|
@ -0,0 +1,10 @@
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.6"
|
||||||
|
install:
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- pip install -r dev-requirements.txt
|
||||||
|
script:
|
||||||
|
- mypy --ignore-missing-imports .
|
||||||
|
- flake8 .
|
||||||
|
- python -m pytest -vv
|
|
@ -0,0 +1,15 @@
|
||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2018, Thomas Sileo
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Little Boxes
|
||||||
|
|
||||||
|
Tiny ActivityPub framework written in Python, both database and server agnostic.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
```python
|
||||||
|
from little_boxes import activitypub as ap
|
||||||
|
|
||||||
|
from mydb import db_client
|
||||||
|
|
||||||
|
|
||||||
|
class MyBackend(BaseBackend):
|
||||||
|
|
||||||
|
def __init__(self, db_connection):
|
||||||
|
self.db_connection = db_connection
|
||||||
|
|
||||||
|
def inbox_new(self, as_actor, activity):
|
||||||
|
# Save activity as "as_actor"
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
def post_to_remote_inbox(self, as_actor, payload, recipient):
|
||||||
|
# Send the activity to the remote actor
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
|
||||||
|
db_con = db_client()
|
||||||
|
my_backend = MyBackend(db_con)
|
||||||
|
|
||||||
|
ap.use_backend(my_backend)
|
||||||
|
|
||||||
|
me = ap.Person({}) # Init an actor
|
||||||
|
outbox = ap.Outbox(me)
|
||||||
|
|
||||||
|
follow = ap.Follow(actor=me, object='http://iri-i-want-follow')
|
||||||
|
outbox.post(follow)
|
||||||
|
```
|
|
@ -0,0 +1,3 @@
|
||||||
|
pytest
|
||||||
|
flake8
|
||||||
|
mypy
|
|
@ -0,0 +1,12 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def strtobool(s: str) -> bool:
|
||||||
|
if s in ['y', 'yes', 'true', 'on', '1']:
|
||||||
|
return True
|
||||||
|
if s in ['n', 'no', 'false', 'off', '0']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
raise ValueError(f'cannot convert {s} to bool')
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,55 @@
|
||||||
|
"""Errors raised by this package."""
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
"""HTTP-friendly base error, with a status code, a message and an optional payload."""
|
||||||
|
status_code = 400
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, message: str,
|
||||||
|
status_code: Optional[int] = None,
|
||||||
|
payload: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> None:
|
||||||
|
Exception.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
if status_code is not None:
|
||||||
|
self.status_code = status_code
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
rv = dict(self.payload or ())
|
||||||
|
rv['message'] = self.message
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'{self.__class__.__qualname__}({self.message!r}, payload={self.payload!r}, status_code={self.status_code})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ActorBlockedError(Error):
|
||||||
|
"""Raised when an activity from a blocked actor is received."""
|
||||||
|
|
||||||
|
|
||||||
|
class NotFromOutboxError(Error):
|
||||||
|
"""Raised when an activity targets an object from the inbox when an object from the oubox was expected."""
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityNotFoundError(Error):
|
||||||
|
"""Raised when an activity is not found."""
|
||||||
|
status_code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class BadActivityError(Error):
|
||||||
|
"""Raised when an activity could not be parsed/initialized."""
|
||||||
|
|
||||||
|
|
||||||
|
class RecursionLimitExceededError(BadActivityError):
|
||||||
|
"""Raised when the recursion limit for fetching remote object was exceeded (likely a collection)."""
|
||||||
|
|
||||||
|
|
||||||
|
class UnexpectedActivityTypeError(BadActivityError):
|
||||||
|
"""Raised when an another activty was expected."""
|
|
@ -0,0 +1,47 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import ipaddress
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from . import strtobool
|
||||||
|
from .errors import Error
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidURLError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def is_url_valid(url: str) -> bool:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
if parsed.scheme not in ['http', 'https']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# XXX in debug mode, we want to allow requests to localhost to test the federation with local instances
|
||||||
|
debug_mode = strtobool(os.getenv('MICROBLOGPUB_DEBUG', 'false'))
|
||||||
|
if debug_mode:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if parsed.hostname in ['localhost']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
ip_address = socket.getaddrinfo(parsed.hostname, parsed.port or 80)[0][4][0]
|
||||||
|
except socket.gaierror:
|
||||||
|
logger.exception(f'failed to lookup url {url}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if ipaddress.ip_address(ip_address).is_private:
|
||||||
|
logger.info(f'rejecting private URL {url}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_url(url: str) -> None:
|
||||||
|
if not is_url_valid(url):
|
||||||
|
raise InvalidURLError(f'"{url}" is invalid')
|
||||||
|
|
||||||
|
return None
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""Contains some ActivityPub related utils."""
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
from .errors import RecursionLimitExceededError
|
||||||
|
from .errors import UnexpectedActivityTypeError
|
||||||
|
|
||||||
|
|
||||||
|
def parse_collection(
|
||||||
|
payload: Optional[Dict[str, Any]] = None,
|
||||||
|
url: Optional[str] = None,
|
||||||
|
level: int = 0,
|
||||||
|
fetcher: Optional[Callable[[str], Dict[str, Any]]] = None,
|
||||||
|
) -> List[Any]:
|
||||||
|
"""Resolve/fetch a `Collection`/`OrderedCollection`."""
|
||||||
|
if not fetcher:
|
||||||
|
raise Exception('must provide a fetcher')
|
||||||
|
if level > 3:
|
||||||
|
raise RecursionLimitExceededError('recursion limit exceeded')
|
||||||
|
|
||||||
|
# Go through all the pages
|
||||||
|
out: List[Any] = []
|
||||||
|
if url:
|
||||||
|
payload = fetcher(url)
|
||||||
|
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, level=level+1, fetcher=fetcher))
|
||||||
|
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 = fetcher(n)
|
||||||
|
else:
|
||||||
|
raise UnexpectedActivityTypeError('unexpected activity type {}'.format(payload['type']))
|
||||||
|
|
||||||
|
return out
|
|
@ -0,0 +1,7 @@
|
||||||
|
requests
|
||||||
|
markdown
|
||||||
|
pyld
|
||||||
|
pycryptodome
|
||||||
|
html2text
|
||||||
|
mf2py
|
||||||
|
git+https://github.com/erikriver/opengraph.git
|
|
@ -0,0 +1,2 @@
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 120
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from distutils.core import setup
|
||||||
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='Little Boxes',
|
||||||
|
version='0.1.0',
|
||||||
|
description='Tiny ActivityPub framework written in Python, both database and server agnostic.',
|
||||||
|
author='Thomas Sileo',
|
||||||
|
author_email='t@a4.io',
|
||||||
|
url='https://github.com/tsileo/little-boxes',
|
||||||
|
packages=find_packages(),
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from little_boxes import activitypub as ap
|
||||||
|
|
||||||
|
|
||||||
|
def test_little_boxes_follow():
|
||||||
|
back = ap.BaseBackend()
|
||||||
|
ap.use_backend(back)
|
||||||
|
|
||||||
|
me = back.setup_actor('Thomas', 'tom')
|
||||||
|
|
||||||
|
other = back.setup_actor('Thomas', 'tom2')
|
||||||
|
|
||||||
|
outbox = ap.Outbox(me)
|
||||||
|
f = ap.Follow(
|
||||||
|
actor=me.id,
|
||||||
|
object=other.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
outbox.post(f)
|
||||||
|
|
||||||
|
assert back.followers(other) == [me.id]
|
||||||
|
assert back.following(other) == []
|
||||||
|
|
||||||
|
assert back.followers(me) == []
|
||||||
|
assert back.following(me) == [other.id]
|
Ładowanie…
Reference in New Issue