kopia lustrzana https://github.com/tsileo/little-boxes
Cleanup
rodzic
ce2d5a607a
commit
4f0ffc2445
|
@ -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/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
include README.md LICENSE
|
|
@ -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.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
pytest
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
black
|
||||||
flake8
|
flake8
|
||||||
mypy
|
mypy
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -4,4 +4,3 @@ pyld
|
||||||
pycryptodome
|
pycryptodome
|
||||||
html2text
|
html2text
|
||||||
mf2py
|
mf2py
|
||||||
git+https://github.com/erikriver/opengraph.git
|
|
||||||
|
|
68
setup.py
68
setup.py
|
@ -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",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -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) == []
|
||||||
|
|
Ładowanie…
Reference in New Issue