pull/1/head
Thomas Sileo 2018-06-11 22:50:02 +02:00
commit d5afc0db09
14 zmienionych plików z 1391 dodań i 0 usunięć

7
.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1,7 @@
*.sw[op]
key_*.pem
*.egg-info
.pytest_cache
.mypy_cache/
__pycache__/

10
.travis.yml 100644
Wyświetl plik

@ -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

15
LICENSE 100644
Wyświetl plik

@ -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.

37
README.md 100644
Wyświetl plik

@ -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)
```

Wyświetl plik

@ -0,0 +1,3 @@
pytest
flake8
mypy

Wyświetl plik

@ -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

Wyświetl plik

@ -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."""

Wyświetl plik

@ -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

Wyświetl plik

@ -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

7
requirements.txt 100644
Wyświetl plik

@ -0,0 +1,7 @@
requests
markdown
pyld
pycryptodome
html2text
mf2py
git+https://github.com/erikriver/opengraph.git

2
setup.cfg 100644
Wyświetl plik

@ -0,0 +1,2 @@
[flake8]
max-line-length = 120

14
setup.py 100644
Wyświetl plik

@ -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(),
)

Wyświetl plik

@ -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]