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]
key_*.pem
.coverage
coverage.xml
*.egg-info
.pytest_cache
.mypy_cache/

Wyświetl plik

@ -7,4 +7,5 @@ install:
script:
# - mypy --ignore-missing-imports .
# - 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
- [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-cov
black
flake8
mypy

Wyświetl plik

@ -4,9 +4,9 @@ logger = logging.getLogger(__name__)
def strtobool(s: str) -> bool:
if s in ['y', 'yes', 'true', 'on', '1']:
if s in ["y", "yes", "true", "on", "1"]:
return True
if s in ['n', 'no', 'false', 'off', '0']:
if s in ["n", "no", "false", "off", "0"]:
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):
"""HTTP-friendly base error, with a status code, a message and an optional payload."""
status_code = 400
def __init__(
self, message: str,
self,
message: str,
status_code: Optional[int] = None,
payload: Optional[Dict[str, Any]] = None,
) -> None:
@ -21,13 +23,11 @@ class Error(Exception):
def to_dict(self) -> Dict[str, Any]:
rv = dict(self.payload or ())
rv['message'] = self.message
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})'
)
return f"{self.__class__.__qualname__}({self.message!r}, payload={self.payload!r}, status_code={self.status_code})"
class ActorBlockedError(Error):
@ -40,6 +40,7 @@ class NotFromOutboxError(Error):
class ActivityNotFoundError(Error):
"""Raised when an activity is not found."""
status_code = 404

Wyświetl plik

@ -16,25 +16,25 @@ class InvalidURLError(Error):
def is_url_valid(url: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ['http', 'https']:
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'))
debug_mode = strtobool(os.getenv("MICROBLOGPUB_DEBUG", "false"))
if debug_mode:
return True
if parsed.hostname in ['localhost']:
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}')
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}')
logger.info(f"rejecting private URL {url}")
return False
return True

Wyświetl plik

@ -18,43 +18,45 @@ def parse_collection(
) -> List[Any]:
"""Resolve/fetch a `Collection`/`OrderedCollection`."""
if not fetcher:
raise Exception('must provide a fetcher')
raise Exception("must provide a fetcher")
if level > 3:
raise RecursionLimitExceededError('recursion limit exceeded')
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')
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 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))
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 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']))
raise UnexpectedActivityTypeError(
"unexpected activity type {}".format(payload["type"])
)
return out

Wyświetl plik

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

Wyświetl plik

@ -1,14 +1,66 @@
#!/usr/bin/env python
import io
import os
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(),
here = os.path.abspath(os.path.dirname(__file__))
# Package meta-data.
NAME = "little_boxes"
DESCRIPTION = (
"Tiny ActivityPub framework written in Python, both database and server agnostic."
)
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
def _assert_eq(val, other):
assert val == other
@ -10,89 +11,85 @@ def test_little_boxes_follow():
back = ap.BaseBackend()
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)
f = ap.Follow(
actor=me.id,
object=other.id,
)
f = ap.Follow(actor=me.id, object=other.id)
outbox.post(f)
back.assert_called_methods(
me,
(
'follow is saved in the actor inbox',
'outbox_new',
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.id, f.id)
),
(
'follow is sent to the remote followee inbox',
'post_to_remote_inbox',
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda payload: None,
lambda recipient: _assert_eq(recipient, other.inbox),
),
(
'receiving an accept, ensure we check the actor is not blocked',
'outbox_is_blocked',
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda remote_actor: _assert_eq(remote_actor, other.id),
),
(
'receiving the accept response from the follow',
'inbox_new',
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.get_object().id, f.id),
),
(
'the new_following hook is called',
'new_following',
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.id, f.id),
),
me,
(
"follow is saved in the actor inbox",
"outbox_new",
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.id, f.id),
),
(
"follow is sent to the remote followee inbox",
"post_to_remote_inbox",
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda payload: None,
lambda recipient: _assert_eq(recipient, other.inbox),
),
(
"receiving an accept, ensure we check the actor is not blocked",
"outbox_is_blocked",
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda remote_actor: _assert_eq(remote_actor, other.id),
),
(
"receiving the accept response from the follow",
"inbox_new",
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.get_object().id, f.id),
),
(
"the new_following hook is called",
"new_following",
lambda as_actor: _assert_eq(as_actor.id, me.id),
lambda activity: _assert_eq(activity.id, f.id),
),
)
back.assert_called_methods(
other,
(
'receiving the follow, ensure we check the actor is not blocked',
'outbox_is_blocked',
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda remote_actor: _assert_eq(remote_actor, me.id),
),
(
'receiving the follow activity',
'inbox_new',
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.id, f.id),
),
(
'posting an accept in response to the follow',
'outbox_new',
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.get_object().id, f.id),
),
(
'post the accept to the remote follower inbox',
'post_to_remote_inbox',
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda payload: None,
lambda recipient: _assert_eq(recipient, me.inbox),
),
(
'the new_follower hook is called',
'new_follower',
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.id, f.id),
),
other,
(
"receiving the follow, ensure we check the actor is not blocked",
"outbox_is_blocked",
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda remote_actor: _assert_eq(remote_actor, me.id),
),
(
"receiving the follow activity",
"inbox_new",
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.id, f.id),
),
(
"posting an accept in response to the follow",
"outbox_new",
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.get_object().id, f.id),
),
(
"post the accept to the remote follower inbox",
"post_to_remote_inbox",
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda payload: None,
lambda recipient: _assert_eq(recipient, me.inbox),
),
(
"the new_follower hook is called",
"new_follower",
lambda as_actor: _assert_eq(as_actor.id, other.id),
lambda activity: _assert_eq(activity.id, f.id),
),
)
assert back.followers(other) == [me.id]
assert back.following(other) == []
@ -104,15 +101,12 @@ def test_little_boxes_follow_unfollow():
back = ap.BaseBackend()
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)
f = ap.Follow(
actor=me.id,
object=other.id,
)
f = ap.Follow(actor=me.id, object=other.id)
outbox.post(f)
@ -128,4 +122,4 @@ def test_little_boxes_follow_unfollow():
# assert back.following(other) == []
# assert back.followers(me) == []
# assert back.following(me) == []
# assert back.following(me) == []