Start migration to little_boxes
This commit is contained in:
1312
activitypub.py
1312
activitypub.py
File diff suppressed because it is too large
Load Diff
26
config.py
26
config.py
@@ -6,10 +6,9 @@ import requests
|
||||
from itsdangerous import JSONWebSignatureSerializer
|
||||
from datetime import datetime
|
||||
|
||||
from utils import strtobool
|
||||
from utils.key import Key, KEY_DIR, get_secret_key
|
||||
from utils.actor_service import ActorService
|
||||
from utils.object_service import ObjectService
|
||||
from little_boxes.utils import strtobool
|
||||
from utils.key import KEY_DIR, get_key, get_secret_key
|
||||
|
||||
|
||||
def noop():
|
||||
pass
|
||||
@@ -58,8 +57,6 @@ USER_AGENT = (
|
||||
f'(microblog.pub/{VERSION}; +{BASE_URL})'
|
||||
)
|
||||
|
||||
# TODO(tsileo): use 'mongo:27017;
|
||||
# mongo_client = MongoClient(host=['mongo:27017'])
|
||||
mongo_client = MongoClient(
|
||||
host=[os.getenv('MICROBLOGPUB_MONGODB_HOST', 'localhost:27017')],
|
||||
)
|
||||
@@ -67,23 +64,26 @@ mongo_client = MongoClient(
|
||||
DB_NAME = '{}_{}'.format(USERNAME, DOMAIN.replace('.', '_'))
|
||||
DB = mongo_client[DB_NAME]
|
||||
|
||||
|
||||
def _drop_db():
|
||||
if not DEBUG_MODE:
|
||||
return
|
||||
|
||||
mongo_client.drop_database(DB_NAME)
|
||||
|
||||
KEY = Key(USERNAME, DOMAIN, create=True)
|
||||
|
||||
KEY = get_key(ID, USERNAME, DOMAIN)
|
||||
|
||||
|
||||
JWT_SECRET = get_secret_key('jwt')
|
||||
JWT = JSONWebSignatureSerializer(JWT_SECRET)
|
||||
|
||||
|
||||
def _admin_jwt_token() -> str:
|
||||
return JWT.dumps({'me': 'ADMIN', 'ts': datetime.now().timestamp()}).decode('utf-8') # type: ignore
|
||||
|
||||
ADMIN_API_KEY = get_secret_key('admin_api_key', _admin_jwt_token)
|
||||
|
||||
ADMIN_API_KEY = get_secret_key('admin_api_key', _admin_jwt_token)
|
||||
|
||||
ME = {
|
||||
"@context": [
|
||||
@@ -107,13 +107,5 @@ ME = {
|
||||
"type": "Image",
|
||||
"url": ICON_URL,
|
||||
},
|
||||
"publicKey": {
|
||||
"id": ID+"#main-key",
|
||||
"owner": ID,
|
||||
"publicKeyPem": KEY.pubkey_pem,
|
||||
},
|
||||
"publicKey": KEY.to_dict(),
|
||||
}
|
||||
print(ME)
|
||||
|
||||
ACTOR_SERVICE = ActorService(USER_AGENT, DB.actors_cache, ID, ME, DB.instances)
|
||||
OBJECT_SERVICE = ObjectService(USER_AGENT, DB.objects_cache, DB.inbox, DB.outbox, DB.instances)
|
||||
|
@@ -1,37 +0,0 @@
|
||||
# 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)
|
||||
```
|
@@ -1,12 +0,0 @@
|
||||
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')
|
File diff suppressed because it is too large
Load Diff
@@ -1,55 +0,0 @@
|
||||
"""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."""
|
@@ -1,47 +0,0 @@
|
||||
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
|
@@ -1,60 +0,0 @@
|
||||
"""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
|
@@ -2,21 +2,19 @@ libsass
|
||||
gunicorn
|
||||
piexif
|
||||
requests
|
||||
markdown
|
||||
python-u2flib-server
|
||||
Flask
|
||||
Flask-WTF
|
||||
Celery
|
||||
pymongo
|
||||
pyld
|
||||
timeago
|
||||
bleach
|
||||
pycryptodome
|
||||
html2text
|
||||
feedgen
|
||||
itsdangerous
|
||||
bcrypt
|
||||
mf2py
|
||||
passlib
|
||||
pyyaml
|
||||
git+https://github.com/erikriver/opengraph.git
|
||||
git+https://github.com/tsileo/little-boxes.git
|
||||
pyyaml
|
||||
|
@@ -1,26 +0,0 @@
|
||||
from little_boxes.activitypub import use_backend
|
||||
from little_boxes.activitypub import BaseBackend
|
||||
from little_boxes.activitypub import Outbox
|
||||
from little_boxes.activitypub import Person
|
||||
from little_boxes.activitypub import Follow
|
||||
|
||||
def test_little_boxes_follow():
|
||||
back = BaseBackend()
|
||||
use_backend(back)
|
||||
|
||||
me = back.setup_actor('Thomas', 'tom')
|
||||
|
||||
other = back.setup_actor('Thomas', 'tom2')
|
||||
|
||||
outbox = Outbox(me)
|
||||
f = 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]
|
45
utils/key.py
45
utils/key.py
@@ -1,22 +1,23 @@
|
||||
import os
|
||||
import binascii
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from typing import Callable
|
||||
|
||||
KEY_DIR = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), '..', 'config'
|
||||
)
|
||||
from little_boxes.key import Key
|
||||
|
||||
KEY_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "config")
|
||||
|
||||
|
||||
def _new_key() -> str:
|
||||
return binascii.hexlify(os.urandom(32)).decode('utf-8')
|
||||
return binascii.hexlify(os.urandom(32)).decode("utf-8")
|
||||
|
||||
|
||||
def get_secret_key(name: str, new_key: Callable[[], str] = _new_key) -> str:
|
||||
key_path = os.path.join(KEY_DIR, f'{name}.key')
|
||||
"""Loads or generates a cryptographic key."""
|
||||
key_path = os.path.join(KEY_DIR, f"{name}.key")
|
||||
if not os.path.exists(key_path):
|
||||
k = new_key()
|
||||
with open(key_path, 'w+') as f:
|
||||
with open(key_path, "w+") as f:
|
||||
f.write(k)
|
||||
return k
|
||||
|
||||
@@ -24,23 +25,19 @@ def get_secret_key(name: str, new_key: Callable[[], str] = _new_key) -> str:
|
||||
return f.read()
|
||||
|
||||
|
||||
class Key(object):
|
||||
DEFAULT_KEY_SIZE = 2048
|
||||
def __init__(self, user: str, domain: str, create: bool = True) -> None:
|
||||
user = user.replace('.', '_')
|
||||
domain = domain.replace('.', '_')
|
||||
key_path = os.path.join(KEY_DIR, f'key_{user}_{domain}.pem')
|
||||
def get_key(owner: str, user: str, domain: str) -> Key:
|
||||
""""Loads or generates an RSA key."""
|
||||
k = Key(owner)
|
||||
user = user.replace(".", "_")
|
||||
domain = domain.replace(".", "_")
|
||||
key_path = os.path.join(KEY_DIR, f"key_{user}_{domain}.pem")
|
||||
if os.path.isfile(key_path):
|
||||
with open(key_path) as f:
|
||||
self.privkey_pem = f.read()
|
||||
self.privkey = RSA.importKey(self.privkey_pem)
|
||||
self.pubkey_pem = self.privkey.publickey().exportKey('PEM').decode('utf-8')
|
||||
privkey_pem = f.read()
|
||||
k.load(privkey_pem)
|
||||
else:
|
||||
if not create:
|
||||
raise Exception('must init private key first')
|
||||
k = RSA.generate(self.DEFAULT_KEY_SIZE)
|
||||
self.privkey_pem = k.exportKey('PEM').decode('utf-8')
|
||||
self.pubkey_pem = k.publickey().exportKey('PEM').decode('utf-8')
|
||||
with open(key_path, 'w') as f:
|
||||
f.write(self.privkey_pem)
|
||||
self.privkey = k
|
||||
k.new()
|
||||
with open(key_path, "w") as f:
|
||||
f.write(k.privkey_pem)
|
||||
|
||||
return k
|
||||
|
Reference in New Issue
Block a user