kopia lustrzana https://github.com/tsileo/little-boxes
Start the Litepub support
rodzic
9724059d40
commit
04a68aaed9
38
README.md
38
README.md
|
@ -5,6 +5,8 @@
|
|||
<a href="https://github.com/tsileo/little-boxes/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-ISC-red.svg?style=flat" alt="License"></a>
|
||||
<a href="https://github.com/ambv/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
||||
|
||||
/!\ [Litepub](https://litepub.social/litepub/) support in progress
|
||||
|
||||
Tiny [ActivityPub](https://activitypub.rocks/) framework written in Python, both database and server agnostic.
|
||||
|
||||
**Still in early development, and not published on PyPI yet.**
|
||||
|
@ -23,45 +25,9 @@ Until a first version is released, the main goal of this framework is to power t
|
|||
- with helpers for parsing hashtags and linkify content
|
||||
- Key (RSA) helper
|
||||
- HTTP signature helper
|
||||
- JSON-LD signature helper
|
||||
- Webfinger helper
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
```python
|
||||
from little_boxes import activitypub as ap
|
||||
|
||||
from mydb import db_client
|
||||
|
||||
|
||||
class MyBackend(ap.Backend):
|
||||
|
||||
def __init__(self, db_connection):
|
||||
self.db_connection = db_connection
|
||||
|
||||
def inbox_new(self, as_actor: ap.Person, activity: ap.Activity) -> None:
|
||||
# Save activity as "as_actor"
|
||||
# [...]
|
||||
|
||||
def post_to_remote_inbox(self, as_actor: ap.Person, payload: ap.ObjectType, recipient: str) -> None:
|
||||
# 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.id, object='http://iri-i-want-follow')
|
||||
outbox.post(follow)
|
||||
```
|
||||
|
||||
|
||||
## Projects using Little Boxes
|
||||
|
||||
- [microblog.pub](http://github.com/tsileo/microblog.pub) (using MongoDB as a backend)
|
||||
|
|
|
@ -31,25 +31,12 @@ ActorType = Union["Person", "Application", "Group", "Organization", "Service"]
|
|||
ObjectOrIDType = Union[str, ObjectType]
|
||||
|
||||
CTX_AS = "https://www.w3.org/ns/activitystreams"
|
||||
CTX_SECURITY = "https://w3id.org/security/v1"
|
||||
AS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
DEFAULT_CTX = COLLECTION_CTX = [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
# AS ext
|
||||
"Hashtag": "as:Hashtag",
|
||||
"sensitive": "as:sensitive",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
# toot
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"featured": "toot:featured",
|
||||
# schema
|
||||
"schema": "http://schema.org#",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"value": "schema:value",
|
||||
},
|
||||
"https://pleroma.site/schemas/litepub-0.1.jsonld",
|
||||
{"@language": "und"},
|
||||
]
|
||||
|
||||
# Will be used to keep track of all the defined activities
|
||||
|
@ -333,13 +320,12 @@ class BaseActivity(object, metaclass=_ActivityMeta):
|
|||
# @context check
|
||||
if not isinstance(self._data["@context"], list):
|
||||
self._data["@context"] = [self._data["@context"]]
|
||||
if CTX_SECURITY not in self._data["@context"]:
|
||||
self._data["@context"].append(CTX_SECURITY)
|
||||
if isinstance(self._data["@context"][-1], dict):
|
||||
self._data["@context"][-1]["Hashtag"] = "as:Hashtag"
|
||||
self._data["@context"][-1]["sensitive"] = "as:sensitive"
|
||||
self._data["@context"][-1]["toot"] = "http://joinmastodon.org/ns#"
|
||||
self._data["@context"][-1]["featured"] = "toot:featured"
|
||||
self._data["@context"][-1]["@language"] = "und"
|
||||
else:
|
||||
self._data["@context"].append(
|
||||
{
|
||||
|
@ -347,6 +333,7 @@ class BaseActivity(object, metaclass=_ActivityMeta):
|
|||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"featured": "toot:featured",
|
||||
"@language": "und",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
import base64
|
||||
import hashlib
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from pyld import jsonld
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .key import Key # noqa: type checking
|
||||
|
||||
|
||||
# cache the downloaded "schemas", otherwise the library is super slow
|
||||
# (https://github.com/digitalbazaar/pyld/issues/70)
|
||||
_CACHE: Dict[str, Any] = {}
|
||||
LOADER = jsonld.requests_document_loader()
|
||||
|
||||
|
||||
def _caching_document_loader(url: str) -> Any:
|
||||
if url in _CACHE:
|
||||
return _CACHE[url]
|
||||
resp = LOADER(url)
|
||||
_CACHE[url] = resp
|
||||
return resp
|
||||
|
||||
|
||||
jsonld.set_document_loader(_caching_document_loader)
|
||||
|
||||
|
||||
def _options_hash(doc):
|
||||
doc = dict(doc["signature"])
|
||||
for k in ["type", "id", "signatureValue"]:
|
||||
if k in doc:
|
||||
del doc[k]
|
||||
doc["@context"] = "https://w3id.org/identity/v1"
|
||||
normalized = jsonld.normalize(
|
||||
doc, {"algorithm": "URDNA2015", "format": "application/nquads"}
|
||||
)
|
||||
h = hashlib.new("sha256")
|
||||
h.update(normalized.encode("utf-8"))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _doc_hash(doc):
|
||||
doc = dict(doc)
|
||||
if "signature" in doc:
|
||||
del doc["signature"]
|
||||
normalized = jsonld.normalize(
|
||||
doc, {"algorithm": "URDNA2015", "format": "application/nquads"}
|
||||
)
|
||||
h = hashlib.new("sha256")
|
||||
h.update(normalized.encode("utf-8"))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def verify_signature(doc, key: "Key"):
|
||||
to_be_signed = _options_hash(doc) + _doc_hash(doc)
|
||||
signature = doc["signature"]["signatureValue"]
|
||||
signer = PKCS1_v1_5.new(key.pubkey or key.privkey) # type: ignore
|
||||
digest = SHA256.new()
|
||||
digest.update(to_be_signed.encode("utf-8"))
|
||||
return signer.verify(digest, base64.b64decode(signature)) # type: ignore
|
||||
|
||||
|
||||
def generate_signature(doc, key: "Key"):
|
||||
options = {
|
||||
"type": "RsaSignature2017",
|
||||
"creator": doc["actor"] + "#main-key",
|
||||
"created": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
|
||||
}
|
||||
doc["signature"] = options
|
||||
to_be_signed = _options_hash(doc) + _doc_hash(doc)
|
||||
if not key.privkey:
|
||||
raise ValueError(f"missing privkey on key {key!r}")
|
||||
|
||||
signer = PKCS1_v1_5.new(key.privkey)
|
||||
digest = SHA256.new()
|
||||
digest.update(to_be_signed.encode("utf-8"))
|
||||
sig = base64.b64encode(signer.sign(digest)) # type: ignore
|
||||
options["signatureValue"] = sig.decode("utf-8")
|
2
setup.py
2
setup.py
|
@ -26,7 +26,6 @@ REQUIRED = [
|
|||
"requests",
|
||||
"markdown",
|
||||
"bleach",
|
||||
"pyld",
|
||||
"pycryptodome",
|
||||
"html2text",
|
||||
"mdx_linkify",
|
||||
|
@ -70,7 +69,6 @@ setup(
|
|||
"Development Status :: 3 - Alpha",
|
||||
"License :: OSI Approved :: ISC License (ISCL)",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
from little_boxes import linked_data_sig
|
||||
from little_boxes.key import Key
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
DOC = """{"type": "Create", "actor": "https://microblog.pub", "object": {"type": "Note", "sensitive": false, "cc": ["https://microblog.pub/followers"], "to": ["https://www.w3.org/ns/activitystreams#Public"], "content": "<p>Hello world!</p>", "tag": [], "source": {"mediaType": "text/markdown", "content": "Hello world!"}, "attributedTo": "https://microblog.pub", "published": "2018-05-21T15:51:59Z", "id": "https://microblog.pub/outbox/988179f13c78b3a7/activity", "url": "https://microblog.pub/note/988179f13c78b3a7", "replies": {"type": "OrderedCollection", "totalItems": 0, "first": "https://microblog.pub/outbox/988179f13c78b3a7/replies?page=first", "id": "https://microblog.pub/outbox/988179f13c78b3a7/replies"}, "likes": {"type": "OrderedCollection", "totalItems": 2, "first": "https://microblog.pub/outbox/988179f13c78b3a7/likes?page=first", "id": "https://microblog.pub/outbox/988179f13c78b3a7/likes"}, "shares": {"type": "OrderedCollection", "totalItems": 3, "first": "https://microblog.pub/outbox/988179f13c78b3a7/shares?page=first", "id": "https://microblog.pub/outbox/988179f13c78b3a7/shares"}}, "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {"Hashtag": "as:Hashtag", "sensitive": "as:sensitive"}], "published": "2018-05-21T15:51:59Z", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://microblog.pub/followers"], "id": "https://microblog.pub/outbox/988179f13c78b3a7"}""" # noqa: E501
|
||||
|
||||
|
||||
def test_linked_data_sig():
|
||||
doc = json.loads(DOC)
|
||||
|
||||
k = Key("https://lol.com")
|
||||
k.new()
|
||||
|
||||
linked_data_sig.generate_signature(doc, k)
|
||||
assert linked_data_sig.verify_signature(doc, k)
|
Ładowanie…
Reference in New Issue