warriordog / activitypubsharp Goto Github PK
View Code? Open in Web Editor NEWModular implementation of ActivityPub in C#
Home Page: https://warriordog.github.io/ActivityPubSharp/
License: Mozilla Public License 2.0
Modular implementation of ActivityPub in C#
Home Page: https://warriordog.github.io/ActivityPubSharp/
License: Mozilla Public License 2.0
Various tweaks and small refactors to improve compatibility between AS and .NET. Depends on changes in #8.
Create new types:
ASUri
(use ASUriConverter
)LinkRel
(use LinkRelConverter
)Implement these implicit casts between AS / CLR types:
ASUri
<-> string
, Uri
ASLink
<-> ASUri
, Uri
MentionLink
<-> ASUri
, Uri
, string
Linkable<T>
<-- ASLink
, ASUri
, Uri
, string
, T
Linkable<T>
--> ASLink?
, T?
LinkRel
<-> string
ASCollection
<-- List<T>
, T
ASOrderedCollection
<-- List<T>
, T
ASCollectionPage
<-- List<T>
, T
ASOrderedCollectionPage
<-- List<T>
, T
Implement these custom JsonConverters:
ASUriConverter
<-> JsonTokenType.String
LinkRelConverter
<-> string
Change the type of these properties:
ASLink.HRef
to type ASUri
ASLink.Rel
to type LinkRel
REMOVE these implicit casts:
NaturalLanguageString
<-> string
(this will be redesigned in #12)Additionally, add unit tests for all new types and behavior.
For some APIs, we have to derive an instance of JsonNodeOptions
from the provided JsonSerializerOptions
. This is currently done once for every call to Write
, but that may be avoidable. Factory-style JSON converters (including ASTypeConverter
) should be bound to a particular options instance that can be passed from the non-generic factory. If that options never changes, then we can also avoid changing the node options. Both can be cached as readonly fields in the class.
Extensions are a critical and actively used part of the ActivityPub spec, but they're also remarkably difficult to implement in C#. This poses a research challenge - we need to find an implementation that meets all these requirements simultaneously:
Deserialization:
Serialization:
@context
propertyFor more information, see previous discussions in #2.
Do to a bad merge, the main branch does not currently build or compile. It needs to be restored to a working state, even if that means removing code that seems to support JSON-LD. Whatever is there doesn't work and will be replaced anyway with #2.
Finalize the library architecture for the upper packages. This needs to be worked out while its still early and things are flexible. The API shape of Client and Server packages will likely shape the entire rest of the library.
We have these goals to balance:
For now, lets go with this:
ActivityPub.Types
- type model and JSON conversionActivityPub.Common
- core utilities, services, and configActivityPub.Client
- client services and use casesActivityPub.Server
- server services and use cases (including federation)ActivityPub.Server.AspNetCore
- integration with ASP.NET CoreActivityPub.Extension.Mastodon
- implementation of Mastodon extensionsRelated: #117
We don't want to implement many at this stage, but it would be good to have the test project(s) created in the solution.
See README.md for a list of package names
Implement IEnumerable<T>
to iterate through Items
. This will allow ForEach over a collection.
Just a convenience. We can still add all our custom logic, but collection initializers and other goodies will be available.
Output is meant to be pretty-printed. This was working, but broke during refactors. Depends on #25.
In the meantime, a simple workaround is to put this somewhere in Program.cs
. It must precede the first JSON call, or an exception will be thrown!
var serializer = host.Services.GetRequiredService<IJsonLdSerializer>();
serializer.SerializerOptions.WriteIndented = true;
This is to implement a naive MVP implementation of JSON-LD, just to get the project unblocked. Easy version is this:
-[ ] Start with the System.Text.Json
fork
-[ ] Map all fields to their basic names. hope for no conflicts lol
Needs to split into one of these based on properties:
Ignore ASIntransitiveActivity as it has an entirely different AS type
this is important for things like pretty printing.
This is really just a DI upgrade. IJsonLdSerializer already exposes a SerializerOptions
property with a configurable instance. We need to run a callback or something after the service collection is built, then we can adjust it.
All the DevOps-y parts of OSS. Configure the github repo, create a build pipeline, establish contributor guidelines and technical standards, etc.
Phase 1 - Groundwork:
PROGRESS.md
into GitHub issuesPhase 2 - MVP:
Phase 3 - Best Practices:
SimpleClient is in a really bad state after rushing to get it workable. It needs (at least) the following work:
_focus
, mostly) into a stateful singleton serviceResult
type for non-exception errors?help
commandThis should allow us to remove all the Equals() stuff. Need unit tests to ensure everything behaves the same. Also check if records support inheritance - that's important for JsonLDTerm and JsonLDExpandedTerm.
AS types should be compared with exact case. An older version of this code was incorrectly case-ignorant, and some vestiges of that still remain. Everything should be fixed to be case-sensitive, with tests to verify this behavior.
Currently, ASObject.Url
is typed as ASLink | null
, but it should be List<ASLink> | ASLink | null
. List -> T conversion is already handled by the globally-registered LinkableConverter
, so this change will likely be as simple as changing the type to List<ASLink>?
. Tests are needed to ensure that everything works as expected.
This would allow for better DX in some edge cases. There are several important requirements:
We need a polymorphic JSON converter for ASType. It should read the "Type" field and select the appropriate subclass to construct. Missed part of #6.
During the push to implement functional JSON, many internal
types were switched to public
as a quick-fix. Additionally, private / internal types were dropped just anywhere instead of in the correct location. This needs to be fixed.
The Range type is clunky and results in bad UX. Its barely used anywhere so replace it with overloaded properties for each type, with backing fields.
The JSON-LD model, as currently designed and implemented, will not and can not work due to this open issue in System.Text.Json
: dotnet/runtime#63791. Until that feature gap is resolved, we will need to fall back to a lower-level implementation. This is the proposed fallback:
System.Text.Json
as usual.Serialize
and Deserialize
methods, which will work with JsonNode/JsonElement respectively.Read
and Write
. Read will populate an existing object from a reader, and write will write properties into a pre-initialized JsonObject. These will allow serialization logic to be "inherited" by calling into the base class members.This rewrite will be completed in the sample-app
branch. Work will be considered done when the sample app is able to query a simple object from a remote instance.
The HasOnlyHRef
property does not account for ASType.UnknownJsonProperties
. If that dictionary contains any elements, then HasOnlyHRef
should return false to force object-mode serialization.
Instead of SingeString
/ LanguageMap
mode, base it on Lang->Variant->Value
map with a default fallback. Use BCP47 Language-Tags and possibly an existing library since this is a lot to deal with.
API should look something like this:
public class NaturalLanguageString
{
/// <summary>
/// The language-agnostic value of this string.
/// Will be used as a fallback if the requested language is not available, or if no user preference is available.
/// </summary>
public string? DefaultValue { get; set; }
/// <summary>
/// Gets or sets the value of a string for a given language.
/// If no language tags are specified, then this will access <see cref="DefaultValue" /> instead.
/// </summary>
/// <seealso cref="GetFor" />
/// <seealso cref="SetFor" />
public string? this[params string[] language]
{
get => GetFor(language);
set => SetFor(value, language);
}
/// <summary>
/// Sets the value of the string for a given language.
/// If no language tags are specified, then sets the value of <see cref="DefaultValue" /> instead.
/// </summary>
/// <remarks>
/// The value must be provided <em>first</em> due to the use of <see langword="params" />.
/// </remarks>
public void SetFor(string value, params string[] language);
/// <summary>
/// Gets the value of the string for a given language.
/// If no language tags are specified, then returns the value of <see cref="DefaultValue" /> instead.
/// </summary>
/// <seealso cref="TryGetFor" />
/// <seealso cref="SetFor" />
public string? GetFor(params string[] language);
/// <summary>
/// Attempts to get the value of the string for a given language.
/// Returns true on success, or false if no matching string was found.
/// This method does <em>not</em> consider <see cref="DefaultValue" />.
/// </summary>
/// <seealso cref="GetFor" />
public bool TryGetFor(params string[] language, [NotNullWhen(true)] out string? value);
/// <summary>
/// Returns this string as a map of language tags to values.
/// The default value will be mapped to the <see langword="null" /> key.
/// </summary>
public IDictionary<string?, string> ToDictionary();
}
After scanning, assemblies should be tracked in some kind of list or hashset. If already in the list, then scanning should be skipped. This removes a negative performance hit if something mistakenly calls RegisterAssembly
or RegisterAllAssemblies
multiple times.
This is used in every object (ASType.MediaType
) and could benefit from a more specific type (at least for some use cases). If one exists in the framework, then we should use that. Otherwise we should create our own. In either case, we will need a custom converter. Make sure to update the property to use the new type.
This doesn't happen with base ActivityStreams, but the spec allows for it. The resulting type is essentially A & B
. C# can't model that so we'll offload most of that to extensions (#36), but we still need to support the types list. Its possible for something like this to occur:
{
"@context" {
"https://www.w3.org/ns/activitystreams",
"https://example.com/some-funky-extension"
},
"types": [
"CustomType",
"AnotherCustomType",
"TypeWeDon'tEvenCareAbout",
"PersonActor"
]
}
In the above scenario, we need to skip over the first three types and convert to PersonActor. The other types should be preserved in ASType.Types
.
Object | Link
) but the type is not an AS typeWe could substitute them like this:
Linkable<T>
-> ASType
LinkableList<T>
-> List<ASType>
Pros:
MentionLink | ASObject
Cons:
ASLink | PersonActor
- user would need to do something like if (asType is ASLink link) {}
or if (asType is PersonActor person) {}
ActorEndpoints
- it does not derive from ASObject
so ASType
can't be used as a base@context
only exists in the document root. We need to strip it from nested objects. Although technically, we should probably compare nested contexts against the root context and generate a scoped context for the specific property. But that's out of scope for now. TODO: create a separate item for that
Currently, you have to type "push Value" or similar which is both annoying and extremely unintuitive. We should unwrap this automatically.
This will involve three steps:
ASType.JsonLdContexts
to support the object formASUri
to resolve shortened forms given a specific contextDepends on #14
This computed property is used to determine whether an ASLink
is serialized as a string or object. The current implementation is a hack and won't work with extensions or subtypes. It should be replaced with something more durable and extendable by subtypes.
IActor
/ ASActor
do not exist as types in the ActivityPub spec. Instead, they are shapes that any object can implement. The description of these is extremely lacking and types have been largely inferred. Hopefully - there is a technical spec somewhere that be referenced.
This issue is to find such a spec, or if no such spec exists, then to study what existing implementations do. Use that knowledge to verify our model.
Use this as a starting point: https://www.w3.org/TR/activitypub/#actor-objects
Implement a type to model a BCP47 Language-Tag. These are used in NaturalLanguageString
, ASLink.HRefLang
, and possibly other places.
Proposed implementation:
LanguageTag
utility type
LanguageTagConverter
to convert to/from JSONASLink.HRefLang
to LanguageTag
Tests will be needed for type, converter, and the modified uses.
Related to #12.
The following behavior is incorrect, but currently displayed by the JSON implementation:
null
value are written to the output[]
)I think both can be fixed with some changes in ASTypeConverter.
Full JSON-LD is probably not practical to support, but these features are critical for ActivityPub support:
Additionally, these would be nice to have:
Implement a property in ASType
to capture overflow JSON. This will capture and contain any properties that exist within an incoming object but don't map to any known properties. Overflow data should persist all the way to re-serialization.
This should be a good starting point: How to handle overflow JSON or use JsonElement or JsonNode in System.Text.Json
Hello!
I recently became interested in the fediverse concept, and learned about the underlying protocols (ActivityPub/WebFinger) under which Mastodon relies. I was curious if the .net ecosystem had any implementation of ActivityPub. This is one of two repositories I've found, both without a license. This repository is more recent, and is more in line with an implementation that I was considering myself, so I was considering contributing instead of starting from scratch.
But without a license, I cannot safely do so. I see that adding an open source license is on your To-Do list already, but I figured adding an issue might encourage you to do it sooner rather than later. ๐
If you're your having difficulty deciding which license to use, I'm more than happy to discuss it with you.
Currently, IgnoreEmptyCollectionsModifier
is automatically attached to the JsonSerializerOptions
to strip out empty collections. This should be configurable through some kind of toggle.
The spec-compliant API is garbage - either OneOf
or AnyOf
can contain the list of options, and code is supposed to check which is populated in order to determine the question type. This is clunky and uncomfortable, so we will do this instead:
OneOf
and AnyOf
into Options
(or another name)AllowMultiple
(add [JsonIgnore]
)[CustomJsonDeserializer]
oneOf
to Options
and set AllowMultiple
to falseanyOf
to Options
and set AllowMultiple
to trueCustomJsonSerializer]
AllowMultiple
is false, then write Options
to oneOf
AllowMultiple
is true, then write Options
to anyOf
Note: the custom serializer API doesn't handle this case well - it may need an upgrade. We don't want to manually convert every property.
We currently support JSON-LD's @context
field in either string
or string[]
forms, but we don't support the object
form. This is needed, at the very least, for #15.
There exists a package to support Dependency Injection in XUnit tests. Integrate it so that tests don't have to manually create entire dependency graphs just to construct an object. Ensure that the implementation allows complete isolation (by replacing implementations with fakes) if desired.
We currently have JsonLDContext
to model the object-form, but the property itself is modeled as HashSet<JsonLDContext>
. This works, but makes prevents us from adding special methods and makes it clunky to pass the context around.
We should create a dedicated type to contain the HashSet. I recommend JsonLDContext
, and we rename the existing JsonLDContext
to JsonLDContextObject
. Thus, we will end up with this heirarchy:
JsonLDContext 1->x JsonLDContextObject
(IsExternal) 1->1 IRI (actually just a string)
(IsInternal) 1->x JsonLDContextTerm
JsonLDExpandedTerm
Not exactly... nice, but "nice" and "JSON-LD" don't exactly go together ๐คทโโ๏ธ
Place a readme.md
file at the root of each distinct package / C# project. Describe the goals, function, and status of each project. Link each package from the table in the root readme.
Currently, you have to type the case-sensitive .NET property name. We should ignore case and also allow the JSON property name.
There's an example of this in the documentation here: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-7-0#sample-factory-pattern-converter
This is to explore a possible implementation of JSON-LD based on interfaces and Roslyn Source Generators. If successful, this will resolve #2.
The first step is to model all AP / AS types as interfaces rather than classes. This accomplishes two key things:
And has two key flaws:
Both flaws are dealbreakers, which leads to the second step - implementing a source generator. The source generator will automatically produce all necessary implementation classes at compile time. Since this runs at application compile time, rather than library compile time, the implementation class is guaranteed to have all fields supported by the application. Application users would just need to create an empty partial class for each type needed.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.