diff --git a/README.md b/README.md index 707ef17c04fff8e45da98cca814c6f7b8564c01d..26434bf071c552938994289ca14b040af0bd7e4b 100644 --- a/README.md +++ b/README.md @@ -1,269 +1,93 @@ -# Building an ActivityPub Library for Python Developers -## Introduction -ActivityPub is a protocol for social networking that is decentralized enabling the interchange of data between different social media systems. Thus, its aim of this library is to create the ActivityPub protocol in the form of Python language to the benefit of python programmers who wish to incorporate ActivityPub in their Apps. With that in mind, it is clear that ActivityPub would transform our online interactions for the better as it encourages users to leave the centralized, client-server model for more decentralized, federated networks. This library will appeal to developer of decentralized social networks, enthusiastic studying the federated protocols and all those who would like to develop social applications. - -## Approach -To build a robust and reliable ActivityPub library, we will follow these steps: - -1. **Model Definition** -- Define Python classes for ActivityPub entities (e.g., Actor, Object, Activity) using Pydantic for data validation and serialization. - -**Example Model Definition** - -```python -from pydantic import BaseModel, HttpUrl - from typing import List, Optional - - class Actor(BaseModel): - id: HttpUrl - type: str - name: str - inbox: HttpUrl - outbox: HttpUrl - - class Object(BaseModel): - id: HttpUrl - type: str - attributedTo: Actor - to: List[Actor] = [] - content: str - - class Activity(BaseModel): - id: HttpUrl - type: str - actor: Actor - object: Object - to: List[Actor] = [] - context: Optional[str] = "https://www.w3.org/ns/activitystreams" - - actor = Actor( - id="https://chatty.example/ben/", - type="Person", - name="Ben", - inbox="https://chatty.example/ben/inbox", - outbox="https://chatty.example/ben/outbox" - ) - obj = Object( - id="https://example.com/message", - type="Note", - attributedTo=actor, - to=[actor], - content="Hello, World!" - ) -... -``` +# pyfed -2. **Serialization and Deserialization** -- Implement methods to convert Pydantic models to and from JSON-LD format, which is used by ActivityStreams. -- Pydantic provides built-in methods for serialization and deserialization, simplifying the process. -```python - from pydantic import BaseModel - from typing import Any - class JsonSerializer: - def serialize(self, obj: BaseModel) -> str: - return obj.json(indent=4) - - def deserialize(self, json_str: str, model: BaseModel) -> BaseModel: - return model.parse_raw(json_str) +## Getting started - serializer = JsonSerializer() +To make it easy for you to get started with GitLab, here's a list of recommended next steps. - # Serialize an activity - serialized_activity = serializer.serialize(activity) +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - # Deserialize back to an Activity object - deserialized_activity = serializer.deserialize(serialized_activity, Activity) -``` +## Add your files -3. **Server Endpoints** -- Design a client class for sending ActivityPub requests (e.g., `ActivityPubClient`). -- Implement server endpoints for receiving ActivityPub requests (e.g., `ActivityPubServer`). -- Use a library like `aiohttp` for HTTP client functionality. -**Example Server Endpoint** - -```python -from aiohttp import web - - class ActivityPubServer: - async def handle_inbox_request(self, request: web.Request) -> web.Response: - # This method will parse the incoming JSON-LD, - # validate the sender's signature, and route the activity to the appropriate handler. - data = await request.json() - activity = Activity.parse_obj(data) # Validate and parse incoming data - # Process the incoming activity... - ... - - async def handle_outbox_request(self, request: web.Request) -> web.Response: - # This method will parse the outgoing activity and prepare it for delivery. - data = await request.json() - activity = Activity.parse_obj(data) # Validate and parse incoming data - # Prepare and send the outgoing activity... - ... - -app = web.Application() -app.add_routes([web.post("/inbox", ActivityPubServer().handle_inbox_request)]) -app.add_routes([web.post("/outbox", ActivityPubServer().handle_outbox_request)]) -web.run_app(app) +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +``` +cd existing_repo +git remote add origin https://dev.funkwhale.audio/funkwhale/pyfed.git +git branch -M main +git push -uf origin main ``` -4. **Activity Handling** - - Implement activity handling logic, including: - - Activity creation and validation. - - Activity forwarding and delivery. - - Activity processing and updating. +## Integrate with your tools - **Example Activity Handling** +- [ ] [Set up project integrations](https://dev.funkwhale.audio/funkwhale/pyfed/-/settings/integrations) - ```python - class ActivityHandler: - def create_activity(self, actor: Actor, object: Object) -> Activity: - # Logic to create an activity - return Activity(id="some_id", type="Create", actor=actor, object=object, to=[actor]) +## Collaborate with your team - def forward_activity(self, activity: Activity) -> None: - # Logic to forward an activity - ... +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - def process_activity(self, activity: Activity) -> None: - # Logic to process an activity - ... +## Test and Deploy +Use the built-in continuous integration in GitLab. - class CreateActivityHandler(ActivityHandler): - def process_activity(self, activity: Activity) -> None: - # Logic specific to processing create activities - ... +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - class FollowActivityHandler(ActivityHandler): - def process_activity(self, activity: Activity) -> None: - # Logic specific to processing follow activities - ... +*** - class LikeActivityHandler(ActivityHandler): - def process_activity(self, activity: Activity) -> None: - # Logic specific to processing like activities - ... - # Handlers would be selected dynamically based on activity type. - ``` +# Editing this README -**Example Use Cases** -- Sending a message from one actor to another -- Creating a new activity and forwarding it to other actors -- Processing incoming activities and updating the local state +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. -**Library Structure** -The library will be structured as follows: +## Suggestions for a good README -``` -pyfed/ -│ -├── __init__.py -│ -├── models/ # Contains data models for ActivityPub entities -│ ├── __init__.py -│ ├── actor.py -│ ├── object.py -│ └── activity.py -│ -├── serializers/ # Handles serialization and deserialization of models -│ ├── __init__.py -│ ├── json_serializer.py -│ ├── client.py -│ └── server.py -│ -├── utils.py # Common utility functions and validation helpers -│ -├── security/ # Implement security features like HTTP signatures -│ ├── __init__.py -│ └── http_signatures.py -│ -├── tests/ # Test cases for models, serializers, client, and server -│ ├── __init__.py -│ ├── test_models.py -│ ├── test_serializers.py -│ ├── test_client.py -│ └── test_server.py -│ -├── CONTRIBUTING.md -└── README.md -``` +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. -## Installation and Usage Instructions -To install the library, run +## Name +Choose a self-explaining name for your project. -```bash -pip install pyfed -``` +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. -**Basic Usage Example** -Here’s a simple example of how to instantiate the server, create an actor and an object, and invoke an endpoint: - -```python -from activitypub import ActivityPubServer, Actor, Object - -# Initialize server -server = ActivityPubServer() - -# Create an actor and an object -actor = Actor( - id="https://chatty.example/ben/", - type="Person", - name="Ben", - inbox="https://chatty.example/ben/inbox", - outbox="https://chatty.example/ben/outbox" -) -obj = Object( - id="https://example.com/message", - type="Note", - attributedTo=actor, - to=[actor], - content="Hello, World!" -) - -# Create and serialize an activity -activity = server.create_activity(actor, obj) -serialized_activity = server.serialize(activity) - -# (In a real scenario, you would send this serialized activity via a request) -``` +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. -## Contribution Guidelines -We welcome contributions to the library. Please see the `CONTRIBUTING.md` file for guidelines on how to contribute. You can report issues or request features on the [GitHub issues page](https://github.com/yourusername/pyfed/issues). +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. -## Dependencies -The library depends on the following major dependencies: -- `aiohttp` (version >=3.7.4) for HTTP client functionality. -- `pydantic` (version >=1.8.0) for data validation and serialization. -- `pytest` (version >=6.0) for testing. +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. -## Future Plans -We plan to implement additional features, including: -- Support for more ActivityPub endpoints (e.g., followers, following). -- Improved security and authentication mechanisms. -- Enhanced activity handling logic. -- Better error handling and debugging tools. +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. -For a detailed roadmap of the project, see the `ROADMAP.md` file. +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. -## Additional Resources -For more information on ActivityPub and its ecosystem, please see the following resources: +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. -- [ActivityPub specification](https://www.w3.org/TR/activitypub/) -- [ActivityStreams standard](https://www.w3.org/TR/activitystreams-core/) -- [JSON-LD specification](https://www.w3.org/TR/json-ld/) +## Contributing +State if you are open to contributions and what your requirements are for accepting them. -## License -This project is licensed under the MIT License. See the `LICENSE` file for details. +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. -## Timeline -**Review of Library by October** -**Completion by December** +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. ---- +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. -This is just a starting point, and the approach may evolve as we progress with the implementation. We welcome contributions and feedback from the community to help shape the library into a robust and reliable implementation of the ActivityPub protocol. +## License +For open source projects, say how it is licensed. - \ No newline at end of file +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/flow.drawio.png b/flow.drawio.png deleted file mode 100644 index 3eea5386b6a1991be6a4b4a562e867579719b429..0000000000000000000000000000000000000000 Binary files a/flow.drawio.png and /dev/null differ diff --git a/models/__init__.py b/models/__init__.py deleted file mode 100644 index 660a3ab580574a610e0f495b7290d1706a49a630..0000000000000000000000000000000000000000 --- a/models/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .actors import Actor -from .objects import Object -from .activities import Activity - -__all__ = ["Actor", "Object", "Activity"] diff --git a/models/activities.py b/models/activities.py deleted file mode 100644 index 2a5d7a1d0bbf4b4e7f208ba02c56bed5892761d5..0000000000000000000000000000000000000000 --- a/models/activities.py +++ /dev/null @@ -1,131 +0,0 @@ -from pydantic import Field, root_validator -from typing import List, Union, Optional -from models.objects import APObject -from .fields import ( - ActorField, - InstrumentField, - ObjectField, - OriginField, - ResultField, - TargetField, - TypeField, - AnyOfField, - ClosedField, - OneOfField -) - - -class APActivity(APObject): - actor: Union[ActorField, List[ActorField]] - object: Union[ObjectField, List[ObjectField]] - target: Union[TargetField, List[TargetField]] - origin: Union[OriginField, List[OriginField]] - result: Union[ResultField, List[ResultField]] - instrument: Union[InstrumentField, List[InstrumentField]] - -class APIntransitiveActivity(APActivity): - object = None - -class APIgnore(APActivity): - type = TypeField = Field("Ignore", const=True) - -class APOffer(ActorField): - type = TypeField = Field("Offer", const=True) - -class APAccept(APActivity): - type: TypeField = Field("Accept", const=True) - -class APAdd(APActivity): - type: TypeField = Field("Add", const=True) - -class APAnnounce(APActivity): - type: TypeField = Field("Announce", const=True) - -class APArrivve(APIntransitiveActivity): - type: TypeField = Field("Arrive", const=True) - -class APBlock(APIgnore): - type: TypeField = Field("Block", const=True) - -class APCreate(APActivity): - type: TypeField = Field("Create", const=True) - -class APDelete(APActivity): - type: TypeField = Field("Delete", const=True) - -class APDislike(APActivity): - type: TypeField = Field("Dislike", const=True) - -class APFlag(APActivity): - type: TypeField = Field("Flag", const=True) - -class APFollow(APActivity): - type: TypeField = Field("Follow", const=True) - -class APInvite(APOffer): - type: TypeField = Field("Invite", const=True) - -class APJoin(APActivity): - type: TypeField = Field("Join", const=True) - -class APLeave(APActivity): - type: TypeField = Field("Leave", const=True) - -class APLike(APActivity): - type: TypeField = Field("Like", const=True) - -class APListen(APActivity): - type: TypeField = Field("Listen", const=True) - -class APMove(APActivity): - type: TypeField = Field("Move", const=True) - -class APRead(APActivity): - type: TypeField = Field("Read", const=True) - -class APReject(APActivity): - type: TypeField = Field("Reject", const=True) - -class APRemove(APActivity): - type: TypeField = Field("Remove", const=True) - -class APTentativeAccept(APActivity): - type: TypeField = Field("TentativeAccept", const=True) - -class APTentativeReject(APActivity): - type: TypeField = Field("TentativeReject", const=True) - -class APTravel(APActivity): - type: TypeField = Field("Travel", const=True) - -class APUndo(APActivity): - type: TypeField = Field("Undo", const=True) - -class APUpdate(APActivity): - type: TypeField = Field("Update", const=True) - -class APView(APActivity): - type: TypeField = Field("View", const=True) - -class Question(APIntransitiveActivity): - type: TypeField = Field("Question", const=True) - oneOf: Optional[List[OneOfField]] = None - anyOf: Optional[List[AnyOfField]] = None - closed: Optional[ClosedField] = None - - @root_validator(pre=True) - def check_any_of_or_one_of(cls, values): - """Ensure that either 'anyOf' or 'oneOf' is used, but not both.""" - if values.get('anyOf') and values.get('oneOf'): - raise ValueError("A Question object must not have both 'anyOf' and 'oneOf'.") - return values - -class APQuestionAnyOf(Question): - oneOf: None - -class APQuestionOneOf(Question): - anyOf: None - -class APQuestionNone(Question): - oneOf: None - anyOf: None diff --git a/models/actors.py b/models/actors.py deleted file mode 100644 index ce7875ec2eb25b5b53bda433b5df8dc923d92601..0000000000000000000000000000000000000000 --- a/models/actors.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations -from pydantic import Field -from typing import Optional -from .objects import APObject -from .fields import ( - FollowersField, - FollowingField, - InboxField, - LikedField, - OutboxField, - PreferredUsernameMapField, - PreferredUsernameField, - StreamsField, - EndpointsField, - TypeField -) - -class APActor(APObject): - """ - Base class for ActivityPub actors. - """ - inbox: InboxField - outbox: OutboxField - following: Optional[FollowingField] = None - followers: Optional[FollowersField] = None - liked: Optional[LikedField] = None - streams: Optional[StreamsField] = None - preferred_username: Optional[PreferredUsernameField] = None - preferred_username_map: Optional[PreferredUsernameMapField] = None - endpoints: Optional[EndpointsField] = None - -class APPerson(APActor): - """ - Represents a Person actor in ActivityPub. - """ - type: TypeField = Field("Person", const=True) - -class APGroup(APActor): - """ - Represents a Group actor in ActivityPub. - """ - type: TypeField = Field("Group", const=True) - -class APOrganization(APActor): - """ - Represents an Organization actor in ActivityPub. - """ - type: TypeField = Field("Organization", const=True) - -class APApplication(APActor): - """ - Represents an Application actor in ActivityPub. - """ - type: TypeField = Field("Application", const=True) - -class APService(APActor): - """ - Represents a Service actor in ActivityPub. - """ - type: TypeField = Field("Service", const=True) \ No newline at end of file diff --git a/models/collections.py b/models/collections.py deleted file mode 100644 index 651d40c94f8ea453b446ca5134e37f2d12c3e762..0000000000000000000000000000000000000000 --- a/models/collections.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations -from typing import Optional, List, Union -from pydantic import Field -from .fields import ( - CollectionCurrentField, - CollectionFirstField, CollectionItemsField, - CollectionLastField, - TotalItemsField, - CollectionPageNextField, - CollectionPagePartOfField, - CollectionPagePrevField, - StartIndexField, - OrderedCollectionItemsField, - TypeField -) -from .objects import APObject - -class APCollection(APObject): - """ - Collection object, inheriting from Object - - """ - type: TypeField = Field("Collection", const=True) - items: List[CollectionItemsField] - total_items: Optional[TotalItemsField] = None - first: Optional[CollectionFirstField] = None - last: Optional[CollectionLastField] = None - current: Optional[CollectionCurrentField] = None - -class APCollectionPage(APCollection): - type: TypeField = Field("CollectionPage", const=True) - part_of: Optional[CollectionPagePartOfField] = None - next: Optional[CollectionPageNextField] = None - prev: Optional[CollectionPagePrevField] = None - -class APOrderedCollection(APCollectionPage): - type: TypeField = Field("OrderedCollectionPage", const=True) - ordered_items: OrderedCollectionItemsField - -class APOrderedCollectionPage(APCollection, APCollectionPage): - type: TypeField = Field("OrderedCollectionPage", const=True) - start_index: StartIndexField \ No newline at end of file diff --git a/models/fields.py b/models/fields.py deleted file mode 100644 index 06fa729d50a6a1f3d67d8f4db48f0ba26374b5d0..0000000000000000000000000000000000000000 --- a/models/fields.py +++ /dev/null @@ -1,368 +0,0 @@ -from pydantic import BaseModel, HttpUrl, Field, validator -from typing import Union, Dict, Optional, Literal, List -from datetime import datetime -from .links import APLink, APMention -from .collections import ( - APCollection, - APOrderedCollection, - APOrderedCollectionPage, - APCollectionPage -) -from .objects import ( - APObject, - APEvent, - APPlace, - APRelationship, - APProfile, - APTombstone, - APArticle, - APAudio, - APDocument, - APImage, - APNote, - APPage, - APVideo - ) - -class AnyCollection(BaseModel): - __root__: Union[APCollection, APOrderedCollection] - -# AnyAPObject is a union of many APObject types, including AnyCollection -class AnyAPObject(BaseModel): - __root__: Union[ - APObject, - APArticle, - APAudio, - APDocument, - APEvent, - APImage, - APNote, - APPage, - APPlace, - APProfile, - APRelationship, - APTombstone, - APVideo, - AnyCollection - ] -class DateTime(BaseModel): - __root__: Union[str | datetime] - -class LanguageTag(BaseModel): - __root__: str - -class MediaType(BaseModel): - __root__: str - - @validator("__root__") - def validate_media_type(cls, value): - """ - Validation helper for MIME types. - This validates the type of media file in use - it recognizes "image/jpeg", "image/png", "application/json", "text/html" - - """ - allowed_mimes = ["image/jpeg", "image/png", "application/json", "text/html"] - if value and value not in allowed_mimes: - raise ValueError(f"Invalid MIME type: {value}") - return value - -class Duration(BaseModel): - __root__: str - -class ContextField(BaseModel): - __root__: Union[str, Dict[str, str]] = Field(alias="@context") - -class UrlField(BaseModel): - __root__: Union[str, APLink] - -class IdField(BaseModel): - __root__: str - -class TypeField(BaseModel): - __root__: str - -class DurationField(BaseModel): - __root__: Duration - -class ContentField(BaseModel): - __root__: str - -class ContentMapField(BaseModel): - __root__: Dict[str, ContentField] - -class MediaTypeField(BaseModel): - __root__: MediaType - -class NameField(BaseModel): - __root__: str - -class NameMapField(BaseModel): - __root__: Dict[str, NameField] - -class SummaryField(BaseModel): - __root__: str - -class SummaryMapField(BaseModel): - __root__: Dict[str, SummaryField] - -class EndTimeField(BaseModel): - __root__: DateTime - -class StartTimeField(BaseModel): - __root__: DateTime - -class PublishedField(BaseModel): - __root__: DateTime - -class UpdatedField(BaseModel): - __root__: DateTime - -class RepliesField(BaseModel): - __root__: Union[str, AnyCollection] - -class RelationshipField(BaseModel): - __root__: Union[str, AnyAPObject] - -class IconField(BaseModel): - __root__: Union[str, APImage, APLink] - -class ImageField(BaseModel): - __root__: Union[str, APImage, APLink] - -class AttachmentField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class AudienceField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class InReplyToField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class LocationField(BaseModel): - __root__: Union[str, APPlace, APLink] - -class PreviewField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class ToField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class BtoField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class CcField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class BccField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class GeneratorField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class ObjectField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class AttributedToField(BaseModel): - __root__: Union[str, AnyAPObject, APLink, APMention] - -class TagField(BaseModel): - __root__: Union[str, AnyAPObject, APLink, APMention] - -class ActorField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class TargetField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class ResultField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class OriginField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class InstrumentField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class OneOfField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class AnyOfField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class SubjectField(BaseModel): - __root__: Union[str, AnyAPObject, APLink] - -class AccuracyField(BaseModel): - __root__: float - -class AltitudeField(BaseModel): - __root__: float - -class LatitudeField(BaseModel): - __root__: float - -class LongitudeField(BaseModel): - __root__: float - -class RadiusField(BaseModel): - __root__: float = Field(..., ge=0.0) - -class UnitsField(BaseModel): - __root__: Union[Literal['cm', 'feet', 'inches', 'km', 'm', 'miles'], str] - -class ClosedField(BaseModel): - __root__: Union[str, AnyAPObject, APLink, DateTime, bool] - -class FormerTypeField(BaseModel): - __root__: str - -class DeletedField(BaseModel): - __root__: DateTime - -class DescribesField(BaseModel): - __root__: Union[str, AnyAPObject] - -class HrefField(BaseModel): - __root__: HttpUrl - - -class HreflangField(BaseModel): - __root__: LanguageTag - -class RelField(BaseModel): - __root__: List[str] - -class HeightField(BaseModel): - __root__: int = Field(..., ge=0) - -class WidthField(BaseModel): - __root__: int = Field(..., ge=0) - -class TotalItemsField(BaseModel): - __root__: int = Field(..., ge=0) - -class CollectionCurrentField(BaseModel): - __root__: Union[str, APCollectionPage, APLink] - -class CollectionFirstField(BaseModel): - __root__: Union[str, APCollectionPage, APLink] - -class CollectionLastField(BaseModel): - __root__: Union[str, APCollectionPage, APLink] - - -class CollectionItemsField(BaseModel): - __root__: Union[str, APCollectionPage, APLink] - - -class CollectionPagePartOfField(BaseModel): - __root__: Union[str, APCollection, APLink] - - -class CollectionPageNextField(BaseModel): - __root__: Union[str, APCollection, APLink] - - -class CollectionPagePrevField(BaseModel): - __root__: Union[str, APCollection, APLink] - - -class OrderedCollectionCurrentField(BaseModel): - __root__: Union[str, APOrderedCollectionPage, APLink] - - -class OrderedCollectionFirstField(BaseModel): - __root__: Union[str, APOrderedCollectionPage, APLink] - - -class OrderedCollectionLastField(BaseModel): - __root__: Union[str, APOrderedCollectionPage, APLink] - - -class OrderedCollectionItemsField(BaseModel): - __root__: Union[str, APOrderedCollectionPage, APLink] - - -class OrderedCollectionPagePartOfField(BaseModel): - __root__: Union[str, APOrderedCollection, APLink] - - -class OrderedCollectionPageNextField(BaseModel): - __root__: Union[str, APOrderedCollection, APLink] - - -class OrderedCollectionPagePrevField(BaseModel): - __root__: Union[str, APOrderedCollection, APLink] - - -class StartIndexField(BaseModel): - __root__: int = Field(..., ge=0) - -class SourceField(BaseModel): - content: str - media_type: MediaType - -class InboxField(BaseModel): - __root__: Union[str, APOrderedCollection, APLink] - -class OutboxField(BaseModel): - __root__: Union[str, APOrderedCollection, APLink] - -class FollowingField(BaseModel): - __root__: Union[str, APCollection, APOrderedCollection, APLink] - -class FollowersField(BaseModel): - __root__: Union[str, APCollection, APOrderedCollection, APLink] - -class LikedField(BaseModel): - __root__: Union[str, APCollection, APOrderedCollection, APLink] - -class LikesField(BaseModel): - __root__: Union[str, APCollection, APOrderedCollection, APLink] - - -class SharesField(BaseModel): - __root__: Union[str, APCollection, APOrderedCollection, APLink] - -class StreamsField(BaseModel): - __root__: Union[str, APCollection, APOrderedCollection, APLink] - -class PreferredUsernameField(BaseModel): - __root__: str - - -class PreferredUsernameMapField(BaseModel): - __root__: Dict[str, str] - - -class ProxyUrlField(BaseModel): - __root__: str - - -class OauthAuthorizationEndpointField(BaseModel): - __root__: str - - -class OauthTokenEndpointField(BaseModel): - __root__: str - - -class ProvideClientKeyField(BaseModel): - __root__: str - - -class SignClientKeyField(BaseModel): - __root__: str - - -class SharedInboxField(BaseModel): - __root__: str - -class EndpointsField(BaseModel): - proxy_url: Optional[ProxyUrlField] = None - oauth_authorization_endpoint: Optional[OauthAuthorizationEndpointField] = None - oauth_token_endpoint: Optional[OauthTokenEndpointField] = None - provide_client_key: Optional[ProvideClientKeyField] = None - sign_client_key: Optional[SignClientKeyField] = None - shared_inbox: Optional[SharedInboxField] = None diff --git a/models/links.py b/models/links.py deleted file mode 100644 index a56ea27a136af1a0256c01b5fccbe1c925227e77..0000000000000000000000000000000000000000 --- a/models/links.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations -from pydantic import Field -from typing import Optional, List, Union -from serializers.json_serializer import ActivityPubModel -from .fields import ( - HeightField, - HreflangField, - HrefField, - MediaTypeField, - NameMapField, - NameField, PreviewField, - RelField, TypeField, WidthField, - ContextField, -) - -class APLink(ActivityPubModel): - """ - Base class for links - - """ - context: Optional[ContextField] = Field(alias="@context") - type: Union[TypeField, List[TypeField]] - name: Optional[NameField] = None - name_map: Optional[NameMapField] = None - href: HrefField - hreflang: HreflangField - rel: Optional[Union[RelField, List[RelField]]] = None - height: Optional[HeightField] = None - width: Optional[WidthField] = None - preview: Optional[PreviewField] = None - media_type: Optional[MediaTypeField] - -class APMention(APLink): - """ - """ - type: TypeField = Field("Mention", const=True) \ No newline at end of file diff --git a/models/objects.py b/models/objects.py deleted file mode 100644 index 2bc03d2ba2daab474d841910f8662acd5d314a9a..0000000000000000000000000000000000000000 --- a/models/objects.py +++ /dev/null @@ -1,183 +0,0 @@ -from __future__ import annotations -from pydantic import HttpUrl, Field, AnyUrl, PositiveInt, validator -from typing import Optional -from serializers.json_serializer import ActivityPubModel - -from .fields import ( - UrlField, - IconField, - ImageField, - AttachmentField, - AudienceField, - InReplyToField, - LocationField, - PreviewField, - ToField, - BtoField, - CcField, - BccField, - AttributedToField, - TagField, - GeneratorField, - IdField, - TypeField, - ContentField, - ContentMapField, - MediaTypeField, - NameField, - NameMapField, - EndTimeField, - StartTimeField, - PublishedField, - RepliesField, - SummaryField, - SummaryMapField, - UpdatedField, - DurationField, - ContextField, - AccuracyField, - AltitudeField, - LatitudeField, - LongitudeField, - RadiusField, - UnitsField, - DescribesField, - SubjectField, - ObjectField, - RelationshipField, - FormerTypeField, - DeletedField, - SourceField, - LikesField, - SharesField, -) - -class _APObjectBaseModel(ActivityPubModel): - """ - Base class for all objects in the ActivityStreams vocabulary - - """ - id: Optional[IdField] = None - type: TypeField - context: Optional[ContextField] = Field(alias="@context") - name: Optional[NameField] = None - -# class Image(_MediaObject): -# pass - -class APObject(_APObjectBaseModel): - """ - The Object class represents any object in the vocabulary. - https://www.w3.org/TR/activitystreams-core/#object - https://www.w3.org/TR/activitystreams-vocabulary/#object-types - - """ - attachment: Optional[AttachmentField] = None - attributed_to: Optional[AttributedToField] = None - audience: Optional[AudienceField] = None - bcc: Optional[BccField] = None - bto: Optional[BtoField] = None - cc: Optional[CcField] = None - content: Optional[ContentField] = None - content_map: Optional[ContentMapField] = None - generator: Optional[GeneratorField] = None - icon: Optional[IconField] = None - image: Optional[ImageField] = None - in_reply_to: Optional[InReplyToField] = None - location: Optional[LocationField] = None - end_time: Optional[EndTimeField] = None - preview: Optional[PreviewField] = None - published: Optional[PublishedField] = None - replies: Optional[RepliesField] = None - start_time: Optional[StartTimeField] = None - summary: Optional[SummaryField] = None - summary_map: Optional[SummaryMapField] = None - name: Optional[NameField] = None - name_map: Optional[NameMapField] = None - tag: Optional[TagField] = None - updated: Optional[UpdatedField] = None - url: Optional[UrlField] = None - to: Optional[ToField] = None - duration: Optional[DurationField] = None - media_type: Optional[MediaTypeField] = None - - # added these field based on the typescript definitions, - # TODO yet to verify if this should be included or not - source: Optional[SourceField] = None - likes: Optional[LikesField] = None - shares: Optional[SharesField] = None - - - @validator("end_time", "start_time") - def validate_time_constraints(cls, v, values, field): - if field.name == "end_time" and v and values.get("start_time") and v < values["start_time"]: - raise ValueError("end_time cannot be before start_time") - return v - -class APEvent(APObject): - """ - Represents any kind of event. - - """ - type: TypeField = Field("Event", const=True) - -class APPlace(APObject): - """ - Represents a logical or physical location. - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place - https://www.w3.org/TR/activitystreams-vocabulary/#places - - """ - type: TypeField = Field("Place", const=True) - accuracy: AccuracyField - altitude: AltitudeField - latitude: LatitudeField - logitude: LongitudeField - raduis: RadiusField - units: UnitsField - - @validator("units") - def validate_unit(cls, value): - allowed_units = ["cm", " feet", " inches", " km", " m", " miles" ] - if value and value not in allowed_units: - raise ValueError(f"Invalid units type: {value}") - return value - -class APProfile(APObject): - """ - Represents Profile - """ - type: TypeField = Field("Profile", const=True) - describes: DescribesField - -class APRelationship(APObject): - type: TypeField = Field("Relationship", const=True) - subject: SubjectField - object: ObjectField - relationship: RelationshipField - -class APTombstone(APObject): - type: TypeField = Field("Tombstone", const=True) - former_type: FormerTypeField - deleted: DeletedField - -class APArticle(APObject): - type: TypeField = Field("Article", const=True) - -class APAudio(APObject): - type: TypeField = Field("Audio", const=True) - -class APDocument(APObject): - type: TypeField = Field("Document", const=True) - -class APImage(APObject): - type: TypeField = Field("Image", const=True) - -class APNote(APObject): - type: TypeField = Field("Note", const=True) - -class APPage(APObject): - type: TypeField = Field("Page", const=True) - -class APVideo(APObject): - type: TypeField = Field("Video", const=True) \ No newline at end of file diff --git a/serializers/__init__.py b/serializers/__init__.py deleted file mode 100644 index 4548553568ebd1d22a3d5d5e4fad1a66266b5dc2..0000000000000000000000000000000000000000 --- a/serializers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .json_serializer import ActivityPubModel - -__all__ = ["ActivityPubModel"] \ No newline at end of file diff --git a/serializers/json_serializer.py b/serializers/json_serializer.py deleted file mode 100644 index ca25b47cb2f6246936528863d0127c2297f88cee..0000000000000000000000000000000000000000 --- a/serializers/json_serializer.py +++ /dev/null @@ -1,92 +0,0 @@ -import warnings -from pydantic import BaseModel, root_validator -from typing import Dict, Union, Generator, Callable, Any - -def to_camel_case(snake_str: str) -> str: - """ - Converts snake_case to camelCase - """ - pascal_string = "".join(x.capitalize() for x in snake_str.lower().split("_")) - return snake_str[0].lower() + pascal_string[1:] - -class ActivityPubModel(BaseModel): - """ - A robust class combining serialization, deserialization, and object type handling - for ActivityPub models. - """ - - _subtypes_: Dict[str, type] = dict() - - class Config: - alias_generator = to_camel_case - - @root_validator(pre=True) - @classmethod - def set_object_type(cls, values: Dict[str, str]) -> Dict[str, str]: - """ - Reads the type of an object from its class and adds the class to the value - so that it appears in the serialized representation. - """ - values["type"] = cls.__name__ - return values - - def __init_subclass__(cls) -> None: - if cls.__name__ in cls._subtypes_: - warnings.warn( - f"A subclass called {cls.__name__} is already initialized. Unexpected behavior might occur." - ) - cls._subtypes_[cls.__name__] = cls - - @classmethod - def __get_validators__(cls) -> Generator[Callable[..., None], None, None]: - yield cls._convert_to_real_type - - @classmethod - def _convert_to_real_type(cls, data: Union[str, Dict[str, Any]]) -> Any: - """ - Ascertains the data type of a deserialized object. This ensures the correct object is created. - """ - if isinstance(data, str): - raise NotImplementedError("Actor needs dereferencing, not yet supported") - - if isinstance(data, dict): - data_type = data.get("type") - if data_type is None: - raise ValueError("Missing 'type' in ActivityPub Model") - - sub = cls._subtypes_.get(data_type) - if sub is None: - raise TypeError(f"Unsupported sub-type: {data_type}") - - return sub(**data) - - @classmethod - def parse_obj(cls, obj: Union[str, Dict[str, Any]]) -> Any: - """ - Deserializes a string into a Python object. - """ - return cls._convert_to_real_type(obj) - - def dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - """ - Converts an object to a Python dict with by_alias=True and exclude_none=True by default. - """ - if "by_alias" not in kwargs: - kwargs.update(by_alias=True) - if "exclude_none" not in kwargs: - kwargs.update(exclude_none=True) - return super().dict(*args, **kwargs) - - @classmethod - def serialize(cls, obj: BaseModel) -> str: - """ - Serializes an object into a JSON string. - """ - return obj.json(indent=4) - - @classmethod - def deserialize(cls, json_str: str, model: BaseModel) -> BaseModel: - """ - Deserializes a JSON string into the appropriate model object. - """ - return model.parse_raw(json_str)