kopia lustrzana https://github.com/tsileo/little-boxes
Cleanup
rodzic
ce2d5a607a
commit
4f0ffc2445
|
@ -1,6 +1,8 @@
|
|||
*.sw[op]
|
||||
key_*.pem
|
||||
|
||||
.coverage
|
||||
coverage.xml
|
||||
*.egg-info
|
||||
.pytest_cache
|
||||
.mypy_cache/
|
||||
|
|
|
@ -7,4 +7,5 @@ install:
|
|||
script:
|
||||
# - mypy --ignore-missing-imports .
|
||||
# - flake8 .
|
||||
- python -m pytest -vv
|
||||
- black --check
|
||||
- python -m pytest -vv -cov=little_boxes
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
include README.md LICENSE
|
|
@ -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.
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
pytest
|
||||
pytest-cov
|
||||
black
|
||||
flake8
|
||||
mypy
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
VERSION = (0, 1, 0)
|
||||
|
||||
__version__ = ".".join(map(str, VERSION))
|
Plik diff jest za duży
Load Diff
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,4 +4,3 @@ pyld
|
|||
pycryptodome
|
||||
html2text
|
||||
mf2py
|
||||
git+https://github.com/erikriver/opengraph.git
|
||||
|
|
68
setup.py
68
setup.py
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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) == []
|
||||
|
|
Ładowanie…
Reference in New Issue