pull/1/head
Thomas Sileo 2018-06-12 19:57:40 +02:00
rodzic ce2d5a607a
commit 4f0ffc2445
14 zmienionych plików z 504 dodań i 394 usunięć

2
.gitignore vendored
Wyświetl plik

@ -1,6 +1,8 @@
*.sw[op] *.sw[op]
key_*.pem key_*.pem
.coverage
coverage.xml
*.egg-info *.egg-info
.pytest_cache .pytest_cache
.mypy_cache/ .mypy_cache/

Wyświetl plik

@ -7,4 +7,5 @@ install:
script: script:
# - mypy --ignore-missing-imports . # - mypy --ignore-missing-imports .
# - flake8 . # - flake8 .
- python -m pytest -vv - black --check
- python -m pytest -vv -cov=little_boxes

1
MANIFEST.in 100644
Wyświetl plik

@ -0,0 +1 @@
include README.md LICENSE

Wyświetl plik

@ -45,3 +45,9 @@ outbox.post(follow)
## Projects using Little Boxes ## Projects using Little Boxes
- [microblog.pub](http://github.com/tsileo/microblog.pub) (using MongoDB as a backend) - [microblog.pub](http://github.com/tsileo/microblog.pub) (using MongoDB as a backend)
## Contributions
TODO: document Mypy, flake8 and black.
PRs are welcome, please open an issue to start a discussion before your start any work.

Wyświetl plik

@ -1,3 +1,5 @@
pytest pytest
pytest-cov
black
flake8 flake8
mypy mypy

Wyświetl plik

@ -4,9 +4,9 @@ logger = logging.getLogger(__name__)
def strtobool(s: str) -> bool: def strtobool(s: str) -> bool:
if s in ['y', 'yes', 'true', 'on', '1']: if s in ["y", "yes", "true", "on", "1"]:
return True return True
if s in ['n', 'no', 'false', 'off', '0']: if s in ["n", "no", "false", "off", "0"]:
return False return False
raise ValueError(f'cannot convert {s} to bool') raise ValueError(f"cannot convert {s} to bool")

Wyświetl plik

@ -0,0 +1,3 @@
VERSION = (0, 1, 0)
__version__ = ".".join(map(str, VERSION))

Wyświetl plik

@ -6,10 +6,12 @@ from typing import Any
class Error(Exception): class Error(Exception):
"""HTTP-friendly base error, with a status code, a message and an optional payload.""" """HTTP-friendly base error, with a status code, a message and an optional payload."""
status_code = 400 status_code = 400
def __init__( def __init__(
self, message: str, self,
message: str,
status_code: Optional[int] = None, status_code: Optional[int] = None,
payload: Optional[Dict[str, Any]] = None, payload: Optional[Dict[str, Any]] = None,
) -> None: ) -> None:
@ -21,13 +23,11 @@ class Error(Exception):
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
rv = dict(self.payload or ()) rv = dict(self.payload or ())
rv['message'] = self.message rv["message"] = self.message
return rv return rv
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return f"{self.__class__.__qualname__}({self.message!r}, payload={self.payload!r}, status_code={self.status_code})"
f'{self.__class__.__qualname__}({self.message!r}, payload={self.payload!r}, status_code={self.status_code})'
)
class ActorBlockedError(Error): class ActorBlockedError(Error):
@ -40,6 +40,7 @@ class NotFromOutboxError(Error):
class ActivityNotFoundError(Error): class ActivityNotFoundError(Error):
"""Raised when an activity is not found.""" """Raised when an activity is not found."""
status_code = 404 status_code = 404

Wyświetl plik

@ -16,25 +16,25 @@ class InvalidURLError(Error):
def is_url_valid(url: str) -> bool: def is_url_valid(url: str) -> bool:
parsed = urlparse(url) parsed = urlparse(url)
if parsed.scheme not in ['http', 'https']: if parsed.scheme not in ["http", "https"]:
return False return False
# XXX in debug mode, we want to allow requests to localhost to test the federation with local instances # 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')) debug_mode = strtobool(os.getenv("MICROBLOGPUB_DEBUG", "false"))
if debug_mode: if debug_mode:
return True return True
if parsed.hostname in ['localhost']: if parsed.hostname in ["localhost"]:
return False return False
try: try:
ip_address = socket.getaddrinfo(parsed.hostname, parsed.port or 80)[0][4][0] ip_address = socket.getaddrinfo(parsed.hostname, parsed.port or 80)[0][4][0]
except socket.gaierror: except socket.gaierror:
logger.exception(f'failed to lookup url {url}') logger.exception(f"failed to lookup url {url}")
return False return False
if ipaddress.ip_address(ip_address).is_private: if ipaddress.ip_address(ip_address).is_private:
logger.info(f'rejecting private URL {url}') logger.info(f"rejecting private URL {url}")
return False return False
return True return True

Wyświetl plik

@ -18,43 +18,45 @@ def parse_collection(
) -> List[Any]: ) -> List[Any]:
"""Resolve/fetch a `Collection`/`OrderedCollection`.""" """Resolve/fetch a `Collection`/`OrderedCollection`."""
if not fetcher: if not fetcher:
raise Exception('must provide a fetcher') raise Exception("must provide a fetcher")
if level > 3: if level > 3:
raise RecursionLimitExceededError('recursion limit exceeded') raise RecursionLimitExceededError("recursion limit exceeded")
# Go through all the pages # Go through all the pages
out: List[Any] = [] out: List[Any] = []
if url: if url:
payload = fetcher(url) payload = fetcher(url)
if not payload: if not payload:
raise ValueError('must at least prove a payload or an URL') raise ValueError("must at least prove a payload or an URL")
if payload['type'] in ['Collection', 'OrderedCollection']: if payload["type"] in ["Collection", "OrderedCollection"]:
if 'orderedItems' in payload: if "orderedItems" in payload:
return payload['orderedItems'] return payload["orderedItems"]
if 'items' in payload: if "items" in payload:
return payload['items'] return payload["items"]
if 'first' in payload: if "first" in payload:
if 'orderedItems' in payload['first']: if "orderedItems" in payload["first"]:
out.extend(payload['first']['orderedItems']) out.extend(payload["first"]["orderedItems"])
if 'items' in payload['first']: if "items" in payload["first"]:
out.extend(payload['first']['items']) out.extend(payload["first"]["items"])
n = payload['first'].get('next') n = payload["first"].get("next")
if n: if n:
out.extend(parse_collection(url=n, level=level+1, fetcher=fetcher)) out.extend(parse_collection(url=n, level=level + 1, fetcher=fetcher))
return out return out
while payload: while payload:
if payload['type'] in ['CollectionPage', 'OrderedCollectionPage']: if payload["type"] in ["CollectionPage", "OrderedCollectionPage"]:
if 'orderedItems' in payload: if "orderedItems" in payload:
out.extend(payload['orderedItems']) out.extend(payload["orderedItems"])
if 'items' in payload: if "items" in payload:
out.extend(payload['items']) out.extend(payload["items"])
n = payload.get('next') n = payload.get("next")
if n is None: if n is None:
break break
payload = fetcher(n) payload = fetcher(n)
else: else:
raise UnexpectedActivityTypeError('unexpected activity type {}'.format(payload['type'])) raise UnexpectedActivityTypeError(
"unexpected activity type {}".format(payload["type"])
)
return out return out

Wyświetl plik

@ -4,4 +4,3 @@ pyld
pycryptodome pycryptodome
html2text html2text
mf2py mf2py
git+https://github.com/erikriver/opengraph.git

Wyświetl plik

@ -1,14 +1,66 @@
#!/usr/bin/env python #!/usr/bin/env python
import io
import os
from distutils.core import setup from distutils.core import setup
from setuptools import find_packages from setuptools import find_packages
setup( here = os.path.abspath(os.path.dirname(__file__))
name='Little Boxes',
version='0.1.0',
description='Tiny ActivityPub framework written in Python, both database and server agnostic.', # Package meta-data.
author='Thomas Sileo', NAME = "little_boxes"
author_email='t@a4.io', DESCRIPTION = (
url='https://github.com/tsileo/little-boxes', "Tiny ActivityPub framework written in Python, both database and server agnostic."
packages=find_packages(), )
URL = "https://github.com/tsileo/little-boxes"
EMAIL = "t@a4.io"
AUTHOR = "Thomas Sileo"
REQUIRES_PYTHON = ">=3.6.0"
VERSION = None
REQUIRED = ["requests", "markdown", "pyld", "pycryptodome", "html2text"]
DEPENDENCY_LINKS = []
# Load the package's __version__.py module as a dictionary.
about = {}
if not VERSION:
with open(os.path.join(here, NAME, "__version__.py")) as f:
exec(f.read(), about)
else:
about["__version__"] = VERSION
# Import the README and use it as the long-description.
with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
long_description = "\n" + f.read()
setup(
name=NAME,
version=about["__version__"],
description=DESCRIPTION,
long_description=long_description,
long_description_content_type="text/markdown",
author=AUTHOR,
author_email=EMAIL,
python_requires=REQUIRES_PYTHON,
url=URL,
packages=find_packages(),
install_requires=REQUIRED,
dependency_links=DEPENDENCY_LINKS,
license="ISC",
classifiers=[
# Trove classifiers
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: ISC License (ISCL)",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
) )

Wyświetl plik

@ -2,6 +2,7 @@ from unittest import mock
from little_boxes import activitypub as ap from little_boxes import activitypub as ap
def _assert_eq(val, other): def _assert_eq(val, other):
assert val == other assert val == other
@ -10,89 +11,85 @@ def test_little_boxes_follow():
back = ap.BaseBackend() back = ap.BaseBackend()
ap.use_backend(back) ap.use_backend(back)
me = back.setup_actor('Thomas', 'tom') me = back.setup_actor("Thomas", "tom")
other = back.setup_actor('Thomas', 'tom2') other = back.setup_actor("Thomas", "tom2")
outbox = ap.Outbox(me) outbox = ap.Outbox(me)
f = ap.Follow( f = ap.Follow(actor=me.id, object=other.id)
actor=me.id,
object=other.id,
)
outbox.post(f) outbox.post(f)
back.assert_called_methods( back.assert_called_methods(
me, me,
( (
'follow is saved in the actor inbox', "follow is saved in the actor inbox",
'outbox_new', "outbox_new",
lambda as_actor: _assert_eq(as_actor.id, me.id), lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.id, f.id) lambda activity: _assert_eq(activity.id, f.id),
), ),
( (
'follow is sent to the remote followee inbox', "follow is sent to the remote followee inbox",
'post_to_remote_inbox', "post_to_remote_inbox",
lambda as_actor: _assert_eq(as_actor.id, me.id), lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda payload: None, lambda payload: None,
lambda recipient: _assert_eq(recipient, other.inbox), lambda recipient: _assert_eq(recipient, other.inbox),
), ),
( (
'receiving an accept, ensure we check the actor is not blocked', "receiving an accept, ensure we check the actor is not blocked",
'outbox_is_blocked', "outbox_is_blocked",
lambda as_actor: _assert_eq(as_actor.id, me.id), lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda remote_actor: _assert_eq(remote_actor, other.id), lambda remote_actor: _assert_eq(remote_actor, other.id),
), ),
( (
'receiving the accept response from the follow', "receiving the accept response from the follow",
'inbox_new', "inbox_new",
lambda as_actor: _assert_eq(as_actor.id, me.id), lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.get_object().id, f.id), lambda activity: _assert_eq(activity.get_object().id, f.id),
), ),
( (
'the new_following hook is called', "the new_following hook is called",
'new_following', "new_following",
lambda as_actor: _assert_eq(as_actor.id, me.id), lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.id, f.id), lambda activity: _assert_eq(activity.id, f.id),
), ),
) )
back.assert_called_methods( back.assert_called_methods(
other, other,
( (
'receiving the follow, ensure we check the actor is not blocked', "receiving the follow, ensure we check the actor is not blocked",
'outbox_is_blocked', "outbox_is_blocked",
lambda as_actor: _assert_eq(as_actor.id, other.id), lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda remote_actor: _assert_eq(remote_actor, me.id), lambda remote_actor: _assert_eq(remote_actor, me.id),
), ),
( (
'receiving the follow activity', "receiving the follow activity",
'inbox_new', "inbox_new",
lambda as_actor: _assert_eq(as_actor.id, other.id), lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.id, f.id), lambda activity: _assert_eq(activity.id, f.id),
), ),
( (
'posting an accept in response to the follow', "posting an accept in response to the follow",
'outbox_new', "outbox_new",
lambda as_actor: _assert_eq(as_actor.id, other.id), lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.get_object().id, f.id), lambda activity: _assert_eq(activity.get_object().id, f.id),
), ),
( (
'post the accept to the remote follower inbox', "post the accept to the remote follower inbox",
'post_to_remote_inbox', "post_to_remote_inbox",
lambda as_actor: _assert_eq(as_actor.id, other.id), lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda payload: None, lambda payload: None,
lambda recipient: _assert_eq(recipient, me.inbox), lambda recipient: _assert_eq(recipient, me.inbox),
), ),
( (
'the new_follower hook is called', "the new_follower hook is called",
'new_follower', "new_follower",
lambda as_actor: _assert_eq(as_actor.id, other.id), lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.id, f.id), lambda activity: _assert_eq(activity.id, f.id),
), ),
) )
assert back.followers(other) == [me.id] assert back.followers(other) == [me.id]
assert back.following(other) == [] assert back.following(other) == []
@ -104,15 +101,12 @@ def test_little_boxes_follow_unfollow():
back = ap.BaseBackend() back = ap.BaseBackend()
ap.use_backend(back) ap.use_backend(back)
me = back.setup_actor('Thomas', 'tom') me = back.setup_actor("Thomas", "tom")
other = back.setup_actor('Thomas', 'tom2') other = back.setup_actor("Thomas", "tom2")
outbox = ap.Outbox(me) outbox = ap.Outbox(me)
f = ap.Follow( f = ap.Follow(actor=me.id, object=other.id)
actor=me.id,
object=other.id,
)
outbox.post(f) outbox.post(f)
@ -128,4 +122,4 @@ def test_little_boxes_follow_unfollow():
# assert back.following(other) == [] # assert back.following(other) == []
# assert back.followers(me) == [] # assert back.followers(me) == []
# assert back.following(me) == [] # assert back.following(me) == []