kopia lustrzana https://github.com/Aonrud/ila-microblog.pub
				
				
				
			First shot at parsing replies tree
							rodzic
							
								
									baceb6be6c
								
							
						
					
					
						commit
						b3cbf1f6db
					
				|  | @ -17,6 +17,10 @@ AS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public" | |||
| ACTOR_TYPES = ["Application", "Group", "Organization", "Person", "Service"] | ||||
| 
 | ||||
| 
 | ||||
| class ObjectIsGoneError(Exception): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class VisibilityEnum(str, enum.Enum): | ||||
|     PUBLIC = "public" | ||||
|     UNLISTED = "unlisted" | ||||
|  | @ -108,6 +112,11 @@ def fetch(url: str, params: dict[str, Any] | None = None) -> dict[str, Any]: | |||
|         params=params, | ||||
|         follow_redirects=True, | ||||
|     ) | ||||
| 
 | ||||
|     # Special handling for deleted object | ||||
|     if resp.status_code == 410: | ||||
|         raise ObjectIsGoneError(f"{url} is gone") | ||||
| 
 | ||||
|     resp.raise_for_status() | ||||
|     try: | ||||
|         return resp.json() | ||||
|  |  | |||
|  | @ -244,7 +244,7 @@ def admin_actions_new( | |||
|     files: list[UploadFile], | ||||
|     content: str = Form(), | ||||
|     redirect_url: str = Form(), | ||||
|     in_reply_to: str | None = Form(), | ||||
|     in_reply_to: str | None = Form(None), | ||||
|     csrf_check: None = Depends(verify_csrf_token), | ||||
|     db: Session = Depends(get_db), | ||||
| ) -> RedirectResponse: | ||||
|  |  | |||
|  | @ -53,7 +53,7 @@ class Object: | |||
|         return ap.object_visibility(self.ap_object) | ||||
| 
 | ||||
|     @property | ||||
|     def context(self) -> str | None: | ||||
|     def ap_context(self) -> str | None: | ||||
|         return self.ap_object.get("context") or self.ap_object.get("conversation") | ||||
| 
 | ||||
|     @property | ||||
|  |  | |||
							
								
								
									
										10
									
								
								app/boxes.py
								
								
								
								
							
							
						
						
									
										10
									
								
								app/boxes.py
								
								
								
								
							|  | @ -49,7 +49,7 @@ def save_outbox_object( | |||
|         public_id=public_id, | ||||
|         ap_type=ra.ap_type, | ||||
|         ap_id=ra.ap_id, | ||||
|         ap_context=ra.context, | ||||
|         ap_context=ra.ap_context, | ||||
|         ap_object=ra.ap_object, | ||||
|         visibility=ra.visibility, | ||||
|         og_meta=ra.og_meta, | ||||
|  | @ -233,9 +233,9 @@ def send_create( | |||
|         in_reply_to_object = get_anybox_object_by_ap_id(db, in_reply_to) | ||||
|         if not in_reply_to_object: | ||||
|             raise ValueError(f"Invalid in reply to {in_reply_to=}") | ||||
|         if not in_reply_to_object.context: | ||||
|         if not in_reply_to_object.ap_context: | ||||
|             raise ValueError("Object has no context") | ||||
|         context = in_reply_to_object.context | ||||
|         context = in_reply_to_object.ap_context | ||||
| 
 | ||||
|     for (upload, filename) in uploads: | ||||
|         attachments.append(upload_to_attachment(upload, filename)) | ||||
|  | @ -544,7 +544,7 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None: | |||
|         ap_actor_id=actor.ap_id, | ||||
|         ap_type=ra.ap_type, | ||||
|         ap_id=ra.ap_id, | ||||
|         ap_context=ra.context, | ||||
|         ap_context=ra.ap_context, | ||||
|         ap_published_at=ap_published_at, | ||||
|         ap_object=ra.ap_object, | ||||
|         visibility=ra.visibility, | ||||
|  | @ -651,7 +651,7 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None: | |||
|                     ap_actor_id=announced_actor.ap_id, | ||||
|                     ap_type=announced_object.ap_type, | ||||
|                     ap_id=announced_object.ap_id, | ||||
|                     ap_context=announced_object.context, | ||||
|                     ap_context=announced_object.ap_context, | ||||
|                     ap_published_at=announced_object.ap_published_at, | ||||
|                     ap_object=announced_object.ap_object, | ||||
|                     visibility=announced_object.visibility, | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ from Crypto.Hash import SHA256 | |||
| from Crypto.Signature import PKCS1_v1_5 | ||||
| from loguru import logger | ||||
| 
 | ||||
| from app import activitypub as ap | ||||
| from app import config | ||||
| from app.key import Key | ||||
| from app.key import get_key | ||||
|  | @ -63,6 +64,7 @@ def _body_digest(body: bytes) -> str: | |||
| 
 | ||||
| @lru_cache(32) | ||||
| def _get_public_key(key_id: str) -> Key: | ||||
|     # TODO: use DB to use cache actor | ||||
|     from app import activitypub as ap | ||||
| 
 | ||||
|     actor = ap.fetch(key_id) | ||||
|  | @ -110,6 +112,9 @@ async def httpsig_checker( | |||
| 
 | ||||
|     try: | ||||
|         k = _get_public_key(hsig["keyId"]) | ||||
|     except ap.ObjectIsGoneError: | ||||
|         logger.info("Actor is gone") | ||||
|         return HTTPSigInfo(has_valid_signature=False) | ||||
|     except Exception: | ||||
|         logger.exception(f'Failed to fetch HTTP sig key {hsig["keyId"]}') | ||||
|         return HTTPSigInfo(has_valid_signature=False) | ||||
|  |  | |||
							
								
								
									
										78
									
								
								app/main.py
								
								
								
								
							
							
						
						
									
										78
									
								
								app/main.py
								
								
								
								
							|  | @ -2,6 +2,8 @@ import base64 | |||
| import os | ||||
| import sys | ||||
| import time | ||||
| from collections import defaultdict | ||||
| from dataclasses import dataclass | ||||
| from datetime import datetime | ||||
| from io import BytesIO | ||||
| from typing import Any | ||||
|  | @ -27,6 +29,7 @@ from starlette.responses import JSONResponse | |||
| 
 | ||||
| from app import activitypub as ap | ||||
| from app import admin | ||||
| from app import boxes | ||||
| from app import config | ||||
| from app import httpsig | ||||
| from app import models | ||||
|  | @ -368,6 +371,14 @@ def outbox( | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class ReplyTreeNode: | ||||
|     ap_object: boxes.AnyboxObject | ||||
|     children: list["ReplyTreeNode"] | ||||
|     is_requested: bool = False | ||||
|     is_root: bool = False | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/o/{public_id}") | ||||
| def outbox_by_public_id( | ||||
|     public_id: str, | ||||
|  | @ -385,7 +396,7 @@ def outbox_by_public_id( | |||
|         ) | ||||
|         .filter( | ||||
|             models.OutboxObject.public_id == public_id, | ||||
|             # models.OutboxObject.is_deleted.is_(False), | ||||
|             models.OutboxObject.is_deleted.is_(False), | ||||
|         ) | ||||
|         .one_or_none() | ||||
|     ) | ||||
|  | @ -395,6 +406,66 @@ def outbox_by_public_id( | |||
|     if is_activitypub_requested(request): | ||||
|         return ActivityPubResponse(maybe_object.ap_object) | ||||
| 
 | ||||
|     # TODO: handle visibility | ||||
|     tree_nodes: list[boxes.AnyboxObject] = [maybe_object] | ||||
|     tree_nodes.extend( | ||||
|         db.query(models.InboxObject) | ||||
|         .filter( | ||||
|             models.InboxObject.ap_context == maybe_object.ap_context, | ||||
|         ) | ||||
|         .all() | ||||
|     ) | ||||
|     tree_nodes.extend( | ||||
|         db.query(models.OutboxObject) | ||||
|         .filter( | ||||
|             models.OutboxObject.ap_context == maybe_object.ap_context, | ||||
|             models.OutboxObject.is_deleted.is_(False), | ||||
|             models.OutboxObject.id != maybe_object.id, | ||||
|         ) | ||||
|         .all() | ||||
|     ) | ||||
|     logger.info(f"root={maybe_object.ap_id}") | ||||
|     nodes_by_in_reply_to = defaultdict(list) | ||||
|     for node in tree_nodes: | ||||
|         nodes_by_in_reply_to[node.in_reply_to].append(node) | ||||
|         logger.info(f"in_reply_to={node.in_reply_to}") | ||||
|     logger.info(nodes_by_in_reply_to) | ||||
| 
 | ||||
|     # TODO: get oldest if we cannot get to root? | ||||
|     if len(nodes_by_in_reply_to.get(None, [])) != 1: | ||||
|         raise ValueError("Failed to compute replies tree") | ||||
| 
 | ||||
|     def _get_reply_node_children( | ||||
|         node: ReplyTreeNode, | ||||
|         index: defaultdict[str | None, list[boxes.AnyboxObject]], | ||||
|     ) -> list[ReplyTreeNode]: | ||||
|         children = [] | ||||
|         for child in index.get(node.ap_object.ap_id, []):  # type: ignore | ||||
|             logger.info(f"{child=}") | ||||
|             child_node = ReplyTreeNode( | ||||
|                 ap_object=child, | ||||
|                 is_requested=child.ap_id == maybe_object.ap_id,  # type: ignore | ||||
|                 children=[], | ||||
|             ) | ||||
|             child_node.children = _get_reply_node_children(child_node, index) | ||||
|             children.append(child_node) | ||||
| 
 | ||||
|         return sorted( | ||||
|             children, | ||||
|             key=lambda node: node.ap_object.ap_published_at,  # type: ignore | ||||
|         ) | ||||
| 
 | ||||
|     root_node = ReplyTreeNode( | ||||
|         ap_object=nodes_by_in_reply_to[None][0], | ||||
|         # ap_object=maybe_object, | ||||
|         is_root=True, | ||||
|         is_requested=nodes_by_in_reply_to[None][0].ap_id == maybe_object.ap_id, | ||||
|         children=[], | ||||
|     ) | ||||
|     root_node.children = _get_reply_node_children(root_node, nodes_by_in_reply_to) | ||||
|     logger.info(root_node.ap_object.ap_id) | ||||
|     logger.info(root_node) | ||||
| 
 | ||||
|     return templates.render_template( | ||||
|         db, | ||||
|         request, | ||||
|  | @ -414,7 +485,10 @@ def outbox_activity_by_public_id( | |||
|     # TODO: ACL? | ||||
|     maybe_object = ( | ||||
|         db.query(models.OutboxObject) | ||||
|         .filter(models.OutboxObject.public_id == public_id) | ||||
|         .filter( | ||||
|             models.OutboxObject.public_id == public_id, | ||||
|             models.OutboxObject.is_deleted.is_(False), | ||||
|         ) | ||||
|         .one_or_none() | ||||
|     ) | ||||
|     if not maybe_object: | ||||
|  |  | |||
|  | @ -156,7 +156,7 @@ class OutboxObjectFactory(factory.alchemy.SQLAlchemyModelFactory): | |||
|             public_id=public_id, | ||||
|             ap_type=ro.ap_type, | ||||
|             ap_id=ro.ap_id, | ||||
|             ap_context=ro.context, | ||||
|             ap_context=ro.ap_context, | ||||
|             ap_object=ro.ap_object, | ||||
|             visibility=ro.visibility, | ||||
|             og_meta=ro.og_meta, | ||||
|  | @ -194,7 +194,7 @@ class InboxObjectFactory(factory.alchemy.SQLAlchemyModelFactory): | |||
|             ap_actor_id=actor.ap_id, | ||||
|             ap_type=ro.ap_type, | ||||
|             ap_id=ro.ap_id, | ||||
|             ap_context=ro.context, | ||||
|             ap_context=ro.ap_context, | ||||
|             ap_published_at=ap_published_at, | ||||
|             ap_object=ro.ap_object, | ||||
|             visibility=ro.visibility, | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Thomas Sileo
						Thomas Sileo