Skip to content

Commit

Permalink
C# interop example (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
lontivero authored Jan 22, 2024
1 parent 77e0385 commit 50dc467
Show file tree
Hide file tree
Showing 16 changed files with 248 additions and 27 deletions.
10 changes: 10 additions & 0 deletions Nostra.CSharp/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
global using Microsoft.FSharp.Collections;
global using Microsoft.FSharp.Core;
global using static Microsoft.FSharp.Core.FSharpOption<Nostra.AuthorIdT>;
global using static Microsoft.FSharp.Core.FSharpOption<Nostra.Kind>;
global using static Nostra.Client.Request;
global using static Nostra.Client.Response;
global using RelayConnection = Nostra.Client.RelayConnection;
global using EventId = Nostra.EventIdModule;
global using Event = Nostra.EventModule;
global using SecretKey = Nostra.SecretKeyModule;
14 changes: 14 additions & 0 deletions Nostra.CSharp/Nostra.CSharp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Nostra\Nostra.fsproj" />
</ItemGroup>

</Project>
59 changes: 59 additions & 0 deletions Nostra.CSharp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace Nostra.CSharp;
using NostrListenerCallback = Action<FSharpResult<RelayMessage, string>>;

public static class Program
{
public static async Task Main(string[] args)
{
var relay = await Client.ConnectToRelayAsync(new Uri("wss://relay.damus.io"));
if (args.Length > 1)
{
var textToPublish = args[1];
PublishNote(relay, textToPublish);
}
await ListenEverything(relay);
}

private static void PublishNote(RelayConnection relay, string noteText)
{
// Creates a note with the given text, sign it using a new randomly-generated
// secret key and send it to the connected relay.
var unsignedEvent = Event.CreateNote(noteText);
var signedEvent = Event.Sign(SecretKey.CreateRandom(), unsignedEvent);
Client.Publish(signedEvent, relay);

// Encode the event as a shareable bech32 nevent event and prints it
// in the console.
Console.WriteLine(Shareable.ToNEvent(
signedEvent.Id,
ToFSharpList(["wss://relay.damus.io"]),
Some(signedEvent.PubKey),
Some(signedEvent.Kind)));

// Serialized the event as SJON and prints it in the console.
var signedEventAsJson = Event.Serialize(signedEvent);
Console.WriteLine(signedEventAsJson);
}

private static async Task ListenEverything(RelayConnection relay)
{
// Subscribes to all the new events.
var filter = Filter.since(DateTime.UtcNow, Filter.all);
var filters = ToFSharpList([filter]);
Client.Subscribe("all", filters, relay);

// Start listeniong for all the events.
await Client.StartListening(FuncConvert.ToFSharpFunc((NostrListenerCallback) (mresult =>
{
var relayMessage = Result.requiresOk(mresult);
if (relayMessage.IsRMEvent)
{
var (_, relayEvent) = GetEvent(relayMessage);
Console.WriteLine(Event.Serialize(relayEvent));
}
})), relay);
}

private static FSharpList<T> ToFSharpList<T>(this IEnumerable<T> seq) =>
seq.Aggregate(FSharpList<T>.Empty, (state, e) => new FSharpList<T>(e, state));
}
43 changes: 43 additions & 0 deletions Nostra.CSharp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# C# Interop demo

This is a simple working example of how to consume Nostra from C# projects.

### Create a note, sign it and publish it
Creates a note with a text, signs it using a new randomly-generated secret key and sends it to the connected relay.
```c#
// Connect to the relay
var relay = await Client.ConnectToRelayAsync(new Uri("wss://relay.damus.io"));

// Create a note
var unsignedEvent = Event.CreateNote("Hello everybody!");

// Sign the note
var signedEvent = Event.Sign(SecretKey.CreateRandom(), unsignedEvent);

// Publish the note in the relay
Client.Publish(signedEvent, relay);
```

#### Subscribe to events
Creates a filter to match all events created from now and start listening for them.
Display the raw json for those messages received from the relay that are events.
```c#
// Connect to the relay
var relay = await Client.ConnectToRelayAsync(new Uri("wss://relay.damus.io"));

// Subscribes to all the new events.
var filter = Filter.since(DateTime.UtcNow, Filter.all);
var filters = ToFSharpList([filter]);
Client.Subscribe("all", filters, relay);

// Start listeniong for all the events.
await Client.StartListening(FuncConvert.ToFSharpFunc((NostrListenerCallback) (mresult =>
{
var relayMessage = Result.requiresOk(mresult);
if (relayMessage.IsRMEvent)
{
var (_, relayEvent) = GetEvent(relayMessage);
Console.WriteLine(Event.Serialize(relayEvent));
}
})), relay);
```
8 changes: 4 additions & 4 deletions Nostra.Client/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ let displayResponse (contacts : Map<byte[], Contact>) (addContact: ContactKey ->

EventId.toBytes eventId
else
Author.toBytes event.PubKey
AuthorId.toBytes event.PubKey
let maybeContact = contacts |> Map.tryFind contactKey
let author = maybeContact
|> Option.map (fun c -> c.metadata.displayName |> Option.defaultValue c.metadata.name)
Expand Down Expand Up @@ -154,7 +154,7 @@ let Main args =
| Some [c] -> c, StdIn.read "Message"
| Some (c::msgs) -> c, msgs |> List.head

let channel = Shareable.decodeNpub channel' |> Option.map (fun pubkey -> EventId (Author.toBytes pubkey) ) |> Option.get
let channel = Shareable.decodeNpub channel' |> Option.map (fun pubkey -> EventId (AuthorId.toBytes pubkey) ) |> Option.get
let user = User.load userFilePath
let event = Event.createChannelMessage channel message |> Event.sign user.secret
publish event user.relays
Expand Down Expand Up @@ -207,7 +207,7 @@ let Main args =

let unknownAuthors =
user.subscribedAuthors
|> List.notInBy Author.equals knownAuthors
|> List.notInBy AuthorId.equals knownAuthors

let filterMetadata =
match unknownAuthors with
Expand All @@ -223,7 +223,7 @@ let Main args =
|> List.map (fun c ->
(match c.key with
| Channel e -> EventId.toBytes e
| Author p -> Author.toBytes p) , c )
| Author p -> AuthorId.toBytes p) , c )
|> Map.ofList

let addContact contactKey metadata =
Expand Down
4 changes: 2 additions & 2 deletions Nostra.Client/User.fs
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,12 @@ module User =
{ user with contacts = List.distinctBy (fun c -> c.key) contacts }

let subscribeAuthors (authors : AuthorId list) user =
let authors' = List.distinctBy Author.toBytes (user.subscribedAuthors @ authors)
let authors' = List.distinctBy AuthorId.toBytes (user.subscribedAuthors @ authors)
{ user with subscribedAuthors = authors' }

let unsubscribeAuthors (authors : AuthorId list) user =
let authors' = user.subscribedAuthors
|> List.notInBy (fun a1 a2 -> Author.toBytes a1 = Author.toBytes a2) authors
|> List.notInBy (fun a1 a2 -> AuthorId.toBytes a1 = AuthorId.toBytes a2) authors
{ user with subscribedAuthors = authors' }

let subscribeChannels (channels : Channel list) user =
Expand Down
6 changes: 3 additions & 3 deletions Nostra.Tests/Bech32.fs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type ``Nip19 Bech32-Shareable entities``(output:ITestOutputHelper) =
|> encodeDecode
|> function
| NPub decodedPubKey ->
should equal (Author.toBytes decodedPubKey) (Author.toBytes author)
should equal (AuthorId.toBytes decodedPubKey) (AuthorId.toBytes author)
| _ -> failwith "The entity is not a npub"

[<Fact>]
Expand All @@ -52,7 +52,7 @@ type ``Nip19 Bech32-Shareable entities``(output:ITestOutputHelper) =
[<Fact>]
let ``Encode/Decode nprofile`` () =
let nprofile =
let author = Author.parse "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" |> Result.requiresOk
let author = AuthorId.parse "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" |> Result.requiresOk
NProfile(author, [
"wss://r.x.com"
"wss://djbas.sadkb.com"
Expand All @@ -73,7 +73,7 @@ type ``Nip19 Bech32-Shareable entities``(output:ITestOutputHelper) =
NEvent(
EventId (Utils.fromHex "08a193492c7fb27ab1d95f258461e4a0dfc7f52bccd5e022746cb28418ef4905"),
["wss://nostr.mom"],
Some (Author.parse "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" |> Result.requiresOk),
Some (AuthorId.parse "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" |> Result.requiresOk),
Some Kind.Text
)
|> encodeDecode
Expand Down
2 changes: 1 addition & 1 deletion Nostra.Tests/TestingFramework.fs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ let latest n : FilterFactory =
let eventsFrom who : FilterFactory =
fun ctx ->
let user = ctx.Users[who]
let author = user.Secret |> SecretKey.getPubKey |> fun x -> Author.toBytes x |> Utils.toHex
let author = user.Secret |> SecretKey.getPubKey |> fun x -> AuthorId.toBytes x |> Utils.toHex
$"""{{"authors": ["{author}"]}}"""

let ``send event`` eventFactory : TestStep =
Expand Down
6 changes: 6 additions & 0 deletions Nostra.sln
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Nostra.Relay", "Nostra.Rela
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Nostra.Tests", "Nostra.Tests\Nostra.Tests.fsproj", "{36AE25AA-6314-4D66-A758-4442FE616BEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nostra.CSharp", "Nostra.CSharp\Nostra.CSharp.csproj", "{24396D29-6F1E-46A0-83DA-A3299C5F3470}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -30,5 +32,9 @@ Global
{36AE25AA-6314-4D66-A758-4442FE616BEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36AE25AA-6314-4D66-A758-4442FE616BEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36AE25AA-6314-4D66-A758-4442FE616BEE}.Release|Any CPU.Build.0 = Release|Any CPU
{24396D29-6F1E-46A0-83DA-A3299C5F3470}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24396D29-6F1E-46A0-83DA-A3299C5F3470}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24396D29-6F1E-46A0-83DA-A3299C5F3470}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24396D29-6F1E-46A0-83DA-A3299C5F3470}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
30 changes: 27 additions & 3 deletions Nostra/Bech32.fs
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,11 @@ module Shareable =
ecPrivKey.WriteToSpan bytes
bytes |> _encode "nsec"
| NPub author ->
Author.toBytes author |> _encode "npub"
AuthorId.toBytes author |> _encode "npub"
| Note(EventId eventId) ->
eventId |> _encode "note"
| NProfile(author, relays) ->
let author = Author.toBytes author |> Array.toList
let author = AuthorId.toBytes author |> Array.toList
let encodedPubKey = 0uy :: 32uy :: author
let encodedRelays =
relays
Expand All @@ -158,7 +158,7 @@ module Shareable =
|> List.concat
let encodedAuthor =
author
|> Option.map (fun author -> 2uy :: 32uy :: List.ofArray (Author.toBytes author))
|> Option.map (fun author -> 2uy :: 32uy :: List.ofArray (AuthorId.toBytes author))
|> Option.defaultValue []
let encodedKind =
kind
Expand Down Expand Up @@ -238,6 +238,7 @@ module Shareable =
| _ -> None
)

[<CompiledName("ToNPub")>]
let encodeNpub author =
encode (NPub author)

Expand All @@ -247,6 +248,10 @@ module Shareable =
| NPub pk -> Some pk
| _ -> None)

[<CompiledName("FromNPub")>]
let _decodeNpub = decodeNpub >> Option.get

[<CompiledName("ToNSec")>]
let encodeNsec secret =
encode (NSec secret)

Expand All @@ -256,6 +261,10 @@ module Shareable =
| NSec sk -> Some sk
| _ -> None)

[<CompiledName("FromNSec")>]
let _decodeNsec = decodeNsec >> Option.get

[<CompiledName("ToNote")>]
let encodeNote note =
encode (Note note)

Expand All @@ -265,6 +274,10 @@ module Shareable =
| Note eventId -> Some eventId
| _ -> None)

[<CompiledName("FromNote")>]
let _decodeNote = decodeNote >> Option.get

[<CompiledName("ToNProfile")>]
let encodeNprofile profile =
encode (NProfile profile)

Expand All @@ -274,6 +287,10 @@ module Shareable =
| NProfile (pk, relays) -> Some (pk, relays)
| _ -> None)

[<CompiledName("FromNProfile")>]
let _decodeNprofile = decodeNprofile >> Option.get

[<CompiledName("ToNEvent")>]
let encodeNevent event =
encode (NEvent event)

Expand All @@ -283,6 +300,10 @@ module Shareable =
| NEvent (eventId, relays, author, kind) -> Some (eventId, relays, author, kind)
| _ -> None)

[<CompiledName("FromNEvent")>]
let _decodeNevent = decodeNevent >> Option.get

[<CompiledName("ToNRelay")>]
let encodeNrelay relay =
encode (NRelay relay)

Expand All @@ -291,3 +312,6 @@ module Shareable =
|> Option.bind (function
| NRelay relays -> Some relays
| _ -> None)

[<CompiledName("FromNRelay")>]
let _decodeNrelay = decodeNrelay >> Option.get
Loading

0 comments on commit 50dc467

Please sign in to comment.