DevChatter / Smilodon

Mastodon-compatible Fediverse Application written in C# with .NET 7
GNU Affero General Public License v3.0
49 stars 5 forks source link

Begin implementing ActivityPub library #28

Open kirkbrauer opened 1 year ago

kirkbrauer commented 1 year ago

Is your feature request related to a problem? Please describe. We should use this issue as a place to start planning the implementation of the ActivityPub library. I just finished reading the spec, it appears that we'll need some interesting data models and maybe some special JSON parsing setup to handle the multiple types of requests/responses specified by ActivityPub. We will also need extension methods to support configuring this with minimal APIs and the WebApplicationBuilder.

Describe the solution you'd like I think the first step to implementing this will be to move everything into a project named ActivityPub with a structure similar to the following:

ActivityPub
├── Objects
│   ├── Object.cs
│   ├── Context.cs
│   └── Identifier.cs
├── Actors
│   └── Actor.cs
├── Collections
│   ├── Collection.cs
│   ├── OrderedCollection.cs
│   └── CollectionTypes.cs
└── Activities
    ├── Activity.cs
    ├── ActivityTypes.cs
    └── Types
        ├── CreateActivity.cs
        ├── UpdateActivity.cs
        ├── DeleteActivity.cs
        ├── FollowActivity.cs
        ├── AddActivity.cs
        ├── RemoveActvitiy.cs
        ├── LikeActivity.cs
        ├── BlockActivity.cs
        ├── UndoActivity.cs
        ├── AcceptActivity.cs
        ├── RejectActivity.cs
        └── AnnounceActivity.cs

Describe alternatives you've considered Another option would be to create separate projects for the ActivityPub models and the AspNetCore configuration. Right now, we just need the models, so maybe start out with that project and decide how to evolve in the future.

benrick commented 1 year ago

Looks good. I'd originally just put in placeholders for the models. Breaking out a library for this could be good.

This does bring up a question. If we use these internally, do we use project references internally and separately pack a nuget library for other projects to use if they want?

kirkbrauer commented 1 year ago

@benrick I think the way I've seen it done with monorepos is for us to reference the library as a project reference and then publish a version that is synchronized across the project. So if we publish multiple NuGet packages, they all get published as version 1.0.0.

Here is how HotChocolate GraphQL does it

benrick commented 1 year ago

Yep, that's exactly what I was thinking we'd do is the monorepo like that, rather than breaking into separate ones, which would just overcomplicate changes to the references packages.

kirkbrauer commented 1 year ago

I took a stab at implementing this, I'll make a draft PR when I can. There are a lot of decisions to make with regards to how we're going to serialize/deserialize the objects. The schema is kinda complex and is backed by JSON-LD. I know there is a library for that, but it's Newtonsoft JSON and doesn't directly serialize into .NET types, it appears to just operate on JObjects. The good news is that there appears to be type discriminators in many places, so we can determine which type of object to deserialize with the new System.Text.Json features in .NET 7.

kirkbrauer commented 1 year ago

I have looked more into the specification, and I found that the vast majority of the ActivityPub functionality is derived from the ActivityStreams spec. In fact, it appears that ActivityPub is a superset of ActivityStreams and both are supersets of JSON-LD documents.

I think implementing a Smilodon.ActivityStreams library first might be the easiest thing to do since it would provide a good foundation for all the other modules to be build on top of like Smilodon.ActivityPub. It appears that we don't have to worry about JSON-LD (other than the @context property) because both specs prefer the human-readable JSON representations of the document.

However, this does lead to a few decisions that need to be made with regards to how we're going to serialize/deserialize ActivityStreams and later ActivityPub JSON objects. It appears that there are two core types Object and Link, which form a disjoint relationship. I think the best way to represent this would be to have an interface called IObjectOrLink, which is implemented by both and has all common properties (e.g. @context, type, id, etc.). There are a few more instances like this where we could use a disjoint interface to represent the difference. In C#, pattern matching will allow us to unwrap this after deserialization. When serializing, we can use the type property as a type discriminator to allow us to determine which object type we are dealing with.

The other question is how we should deal with Links to other objects. In the spec, there appears to be both a long and short form (either using a full Link object or just a string URI. Even though the JSON may serialize the Link as a URI string, I think when deserializing, we should always produce the most verbose and explicit representation, so all URIs become Link objects. This would make parsing an ActivityPub object super easy as it will always be in the exact same format on our end.

When serializing back to JSON, we can maybe use custom JsonConverter<T>s to automatically decide which form to use based on some sort of config and whether the Link has properties besides href, @context, and type. For developer experience, this can be eased by maybe providing a select number of implicit cast operators that will allow you to type a Uri or string directly and have it automatically wrapped in a Link object.

We also need to decide which .NET types we should be mapping some of the JSON values to. It appears that there are a lot if xsd:anyURIs in the spec, which could be either a Uri or string in our representation. There are also a few special types like xsd:duration, which could be mapped to a TimeSpan or a string. The most interesting type is xsd:string | rdf:langString, which specifies an optionally i18n string. This might be a good use case for a custom LangString class, which has constructors for both a single string and IDictionary<string, string> as well as implicit casts from string to make it easier for developers to write. This would also probably need methods to fetch the text for the invariant culture, or a specific language (if available).