├── test └── Nostr.Client.Tests │ ├── Usings.cs │ ├── Nostr.Client.Tests.csproj │ ├── NostrKeyTests.cs │ ├── NostrConverterTests.cs │ ├── NostrZapTests.cs │ └── NostrIdentifierTests.cs ├── apps ├── nostr-debug │ └── NostrDebug.Web │ │ ├── Events │ │ ├── NostrFilterEdit.razor.css │ │ ├── NostrEventEdit.razor.css │ │ ├── NostrOkTable.razor.css │ │ ├── NostrEventTable.razor.css │ │ ├── NostrDirectEventDialog.razor.css │ │ ├── NostrEventEditJsonDialog.razor.css │ │ ├── NostrFilterEditJsonDialog.razor.css │ │ ├── NostrKindSelectSingle.razor │ │ ├── NostrEventViewDialog.razor │ │ ├── EventStorage.cs │ │ ├── NostrEventSend.razor │ │ ├── NostrKindSelect.razor │ │ ├── NostrFilterEditJsonDialog.razor │ │ └── NostrEventEditJsonDialog.razor │ │ ├── wwwroot │ │ ├── CNAME │ │ ├── nostr.png │ │ ├── favicon.ico │ │ ├── icon-192.jpg │ │ ├── icon-192.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── nostr-preview.png │ │ └── index.html │ │ ├── Pages │ │ ├── Relay.razor.css │ │ ├── Converter.razor.css │ │ ├── Keys.razor.css │ │ ├── Publisher.razor.css │ │ ├── QueryTool.razor.css │ │ ├── Index.razor.css │ │ ├── Index.razor │ │ ├── Relay.razor │ │ └── Publisher.razor │ │ ├── Components │ │ ├── MainLayout.razor.css │ │ ├── Stack.razor │ │ ├── Spacer.razor │ │ ├── Stack.razor.css │ │ ├── PageHeader.razor.css │ │ ├── Spacer.razor.cs │ │ ├── MainLayout.razor.js │ │ ├── PageHeader.razor │ │ ├── NavMenu.razor.css │ │ ├── NavMenu.razor │ │ └── RelaySelector.razor │ │ ├── Utils │ │ ├── DateTimeUtils.cs │ │ ├── LinqUtils.cs │ │ └── ClipboardService.cs │ │ ├── Signing │ │ ├── NostrSignatureValidator.razor.css │ │ └── NostrSignatureValidator.razor │ │ ├── App.razor │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── _Imports.razor │ │ ├── External │ │ └── ExternalLinks.cs │ │ ├── NostrDebug.Web.csproj │ │ ├── Program.cs │ │ └── Relay │ │ └── RelayConnection.cs ├── nostr-bot │ ├── NostrBot.Web │ │ ├── Configs │ │ │ ├── NostrConfig.cs │ │ │ ├── OpenAiConfig.cs │ │ │ └── BotConfig.cs │ │ ├── appsettings.Development.json │ │ ├── Utils │ │ │ ├── TaskUtils.cs │ │ │ └── ConfigurationExtensions.cs │ │ ├── Logic │ │ │ ├── NostrEventsQueue.cs │ │ │ ├── NostrListener.cs │ │ │ └── BotManagement.cs │ │ ├── Storage │ │ │ ├── BotContext.cs │ │ │ ├── Migrations │ │ │ │ ├── 20230402175039_SecondaryContext.cs │ │ │ │ ├── 20230401213654_Initial.cs │ │ │ │ ├── 20230401213654_Initial.Designer.cs │ │ │ │ ├── BotContextModelSnapshot.cs │ │ │ │ └── 20230402175039_SecondaryContext.Designer.cs │ │ │ ├── ProcessedEvent.cs │ │ │ └── BotStorage.cs │ │ ├── Dockerfile │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Controllers │ │ │ └── ChatAiController.cs │ │ ├── NostrBot.Web.csproj │ │ ├── BackgroundOrchestration.cs │ │ ├── appsettings.json │ │ └── Program.cs │ └── README.md └── smart-relay │ └── SmartRelay.Api │ ├── appsettings.Development.json │ ├── WeatherForecast.cs │ ├── Hubs │ └── ChatHub.cs │ ├── Properties │ └── launchSettings.json │ ├── SmartRelay.Api.csproj │ ├── appsettings.json │ ├── Controllers │ └── WeatherForecastController.cs │ └── Program.cs ├── nostr.jpg ├── nostr.png ├── src └── Nostr.Client │ ├── icon.png │ ├── Communicator │ ├── INostrCommunicator.cs │ └── NostrWebsocketCommunicator.cs │ ├── Responses │ ├── NostrNoticeResponse.cs │ ├── NostrEoseResponse.cs │ ├── NostrRawResponse.cs │ ├── NostrEventResponse.cs │ ├── NostrOkResponse.cs │ └── NostrResponse.cs │ ├── Messages │ ├── NostrMessageTypes.cs │ ├── Contacts │ │ ├── NostrRelays.cs │ │ ├── NostrContactEvent.cs │ │ └── NostrRelayConfig.cs │ ├── Metadata │ │ ├── NostrMetadataEvent.cs │ │ └── NostrMetadata.cs │ ├── Mutable │ │ ├── NostrEventTagsMutable.cs │ │ ├── NostrEventTagMutable.cs │ │ └── NostrEventMutable.cs │ ├── Zaps │ │ └── NostrZapReceiptEvent.cs │ ├── NostrKind.cs │ ├── NostrEventTag.cs │ └── NostrEventTags.cs │ ├── Client │ ├── INostrClient.cs │ └── NostrClientStreams.cs │ ├── Json │ ├── IHaveAdditionalData.cs │ ├── IHaveAdditionalStringData.cs │ ├── NostrJson.cs │ ├── NostrSerializer.cs │ └── NostrEventConverter.cs │ ├── Requests │ ├── NostrCloseRequest.cs │ ├── NostrEventRequest.cs │ ├── NostrRequest.cs │ └── NostrFilter.cs │ ├── Utils │ ├── HashExtensions.cs │ ├── NostrEncryption.cs │ ├── HexExtensions.cs │ └── NostrConverter.cs │ ├── Identifiers │ ├── NostrRelayIdentifier.cs │ ├── NostrProfileIdentifier.cs │ ├── NostrEventIdentifier.cs │ ├── NostrAddressIdentifier.cs │ ├── NostrIdentifier.cs │ └── NostrIdentifierParser.cs │ ├── Keys │ ├── NostrKeyPair.cs │ └── NostrPublicKey.cs │ └── Nostr.Client.csproj ├── Directory.Build.props ├── .dockerignore ├── .github └── workflows │ ├── dotnet-core-branches.yml │ ├── dotnet-core.yml │ └── gh-pages.yml ├── test_integration └── Nostr.Client.Sample.Console │ └── Nostr.Client.Sample.Console.csproj ├── .editorconfig ├── .gitattributes ├── Nostr.Client.sln.DotSettings └── .gitignore /test/Nostr.Client.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrFilterEdit.razor.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/CNAME: -------------------------------------------------------------------------------- 1 | nostrdebug.com 2 | -------------------------------------------------------------------------------- /nostr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/nostr.jpg -------------------------------------------------------------------------------- /nostr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/nostr.png -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/Relay.razor.css: -------------------------------------------------------------------------------- 1 | .operation-button { 2 | align-self: end; 3 | } 4 | -------------------------------------------------------------------------------- /src/Nostr.Client/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/src/Nostr.Client/icon.png -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrEventEdit.razor.css: -------------------------------------------------------------------------------- 1 | ::deep .positioning-region { 2 | background-color: transparent; 3 | } 4 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/nostr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/apps/nostr-debug/NostrDebug.Web/wwwroot/nostr.png -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/Converter.razor.css: -------------------------------------------------------------------------------- 1 | .conversion-group { 2 | padding-left: 20px; 3 | max-width: 550px; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/apps/nostr-debug/NostrDebug.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/icon-192.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/apps/nostr-debug/NostrDebug.Web/wwwroot/icon-192.jpg -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/apps/nostr-debug/NostrDebug.Web/wwwroot/icon-192.png -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/apps/nostr-debug/NostrDebug.Web/wwwroot/favicon-16x16.png -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/apps/nostr-debug/NostrDebug.Web/wwwroot/favicon-32x32.png -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/nostr-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marfusios/nostr-client/HEAD/apps/nostr-debug/NostrDebug.Web/wwwroot/nostr-preview.png -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 2.1.0 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | ::deep .label { 2 | color: var(--neutral-foreground-rest); 3 | } 4 | 5 | .label { 6 | color: var(--neutral-foreground-rest); 7 | } 8 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/Stack.razor: -------------------------------------------------------------------------------- 1 | @inherits FluentComponentBase 2 | 3 |
4 | @ChildContent 5 |
-------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/Spacer.razor: -------------------------------------------------------------------------------- 1 | @if (Width is null) 2 | { 3 |
4 | } 5 | else 6 | { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/Nostr.Client/Communicator/INostrCommunicator.cs: -------------------------------------------------------------------------------- 1 | using Websocket.Client; 2 | 3 | namespace Nostr.Client.Communicator 4 | { 5 | public interface INostrCommunicator : IWebsocketClient 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/Stack.razor.css: -------------------------------------------------------------------------------- 1 | .stack-vertical { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .stack-horizontal { 7 | display: flex; 8 | flex-direction: row; 9 | } 10 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrOkTable.razor.css: -------------------------------------------------------------------------------- 1 | .local-link { 2 | cursor: pointer; 3 | color: var(--neutral-foreground-rest); 4 | text-decoration-style: dotted; 5 | text-decoration-thickness: 1px; 6 | } 7 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrEventTable.razor.css: -------------------------------------------------------------------------------- 1 | .local-link { 2 | cursor: pointer; 3 | color: var(--neutral-foreground-rest); 4 | text-decoration-style: dotted; 5 | text-decoration-thickness: 1px; 6 | } 7 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/Keys.razor.css: -------------------------------------------------------------------------------- 1 | .conversion-group { 2 | padding-left: 20px; 3 | max-width: 550px; 4 | width: 100%; 5 | margin-bottom: 20px; 6 | } 7 | 8 | .keys { 9 | list-style-type: none; 10 | } 11 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Configs/NostrConfig.cs: -------------------------------------------------------------------------------- 1 | namespace NostrBot.Web.Configs 2 | { 3 | public class NostrConfig 4 | { 5 | public string PrivateKey { get; init; } = null!; 6 | public string[] Relays { get; init; } = null!; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/Publisher.razor.css: -------------------------------------------------------------------------------- 1 | .page-layout { 2 | width: 100%; 3 | margin-bottom: 20px; 4 | display: grid; 5 | grid-template-columns: minmax(400px, 1fr) 2fr; 6 | grid-template-rows: max-content; 7 | column-gap: 40px; 8 | } 9 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/QueryTool.razor.css: -------------------------------------------------------------------------------- 1 | .page-layout { 2 | width: 100%; 3 | margin-bottom: 20px; 4 | display: grid; 5 | grid-template-columns: minmax(250px, 1fr) 2fr; 6 | grid-template-rows: max-content; 7 | column-gap: 40px; 8 | } 9 | -------------------------------------------------------------------------------- /apps/smart-relay/SmartRelay.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": { 4 | "Default": "Debug", 5 | "Override": { 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/PageHeader.razor.css: -------------------------------------------------------------------------------- 1 | .page-title-group { 2 | margin-bottom: 20px; 3 | width: 100%; 4 | } 5 | 6 | .page-title { 7 | margin: 0; 8 | margin-bottom: 5px; 9 | } 10 | 11 | .page-subtitle { 12 | margin: 0; 13 | margin-bottom: 5px; 14 | } 15 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": { 4 | "Default": "Debug", 5 | "Override": { 6 | "System": "Information", 7 | "Microsoft": "Information", 8 | "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/Spacer.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace NostrDebug.Web.Components; 4 | 5 | public partial class Spacer 6 | { 7 | /// 8 | /// Gets or sets the width of the spacer (in pixels) 9 | /// 10 | [Parameter] 11 | public int? Width { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /apps/smart-relay/SmartRelay.Api/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace SmartRelay.Api 2 | { 3 | public class WeatherForecast 4 | { 5 | public DateOnly Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 10 | 11 | public string? Summary { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrDirectEventDialog.razor.css: -------------------------------------------------------------------------------- 1 | .content { 2 | padding: 0; 3 | margin-left: calc(var(--design-unit) * 6px); 4 | margin-right: calc(var(--design-unit) * 6px); 5 | margin-top: calc(var(--design-unit) * 6px); 6 | } 7 | 8 | .buttons { 9 | padding-top: 10px; 10 | } 11 | 12 | .error-message { 13 | color: var(--error); 14 | } 15 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Utils/DateTimeUtils.cs: -------------------------------------------------------------------------------- 1 | namespace NostrDebug.Web.Utils 2 | { 3 | public static class DateTimeUtils 4 | { 5 | public static DateTime UtcNowOnlyHours() 6 | { 7 | var now = DateTime.UtcNow; 8 | return new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0, DateTimeKind.Utc); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Utils/LinqUtils.cs: -------------------------------------------------------------------------------- 1 | namespace NostrDebug.Web.Utils 2 | { 3 | public static class LinqUtils 4 | { 5 | public static void AddRange(this HashSet collection, IEnumerable toAdd) 6 | { 7 | foreach (var item in toAdd) 8 | { 9 | collection.Add(item); 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Nostr.Client/Responses/NostrNoticeResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Nostr.Client.Json; 3 | 4 | namespace Nostr.Client.Responses 5 | { 6 | [DebuggerDisplay("[{CommunicatorName}] {MessageType} - {Message}")] 7 | public class NostrNoticeResponse : NostrResponse 8 | { 9 | [ArrayProperty(1)] 10 | public string? Message { get; init; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/nostr-bot/README.md: -------------------------------------------------------------------------------- 1 | # Nostr Bot 2 | 3 | This is an example Nostr bot. 4 | It is a simple bot that can be used as a starting point for your own bot. 5 | 6 | It reacts to direct messages (DM) or public mentions. OpenAI Chat API generates every reply. 7 | 8 | Dialogue context is preserved in SQLite database and retrieved per every communication thread (DMs per sender, mentions per sender or parent event). -------------------------------------------------------------------------------- /src/Nostr.Client/Responses/NostrEoseResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Nostr.Client.Json; 3 | 4 | namespace Nostr.Client.Responses 5 | { 6 | [DebuggerDisplay("[{CommunicatorName}] {MessageType} - {Subscription}")] 7 | public class NostrEoseResponse : NostrResponse 8 | { 9 | [ArrayProperty(1)] 10 | public string? Subscription { get; init; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Signing/NostrSignatureValidator.razor.css: -------------------------------------------------------------------------------- 1 | .signature { 2 | width: 100%; 3 | } 4 | 5 | .sig-valid { 6 | padding: 10px; 7 | border: dotted 2px var(--success); 8 | border-radius: 5px; 9 | color: var(--success); 10 | } 11 | 12 | .sig-invalid { 13 | padding: 10px; 14 | border: dotted 2px var(--error); 15 | border-radius: 5px; 16 | color: var(--error); 17 | } 18 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/NostrMessageTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Messages 2 | { 3 | public static class NostrMessageTypes 4 | { 5 | public const string Request = "REQ"; 6 | public const string Event = "EVENT"; 7 | public const string Notice = "NOTICE"; 8 | public const string Eose = "EOSE"; 9 | public const string Ok = "OK"; 10 | public const string Close = "CLOSE"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Contacts/NostrRelays.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Diagnostics; 3 | 4 | namespace Nostr.Client.Messages.Contacts 5 | { 6 | [DebuggerDisplay("Relays {Count}")] 7 | public class NostrRelays : ReadOnlyDictionary 8 | { 9 | public NostrRelays(IDictionary dictionary) : base(dictionary) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Utils/TaskUtils.cs: -------------------------------------------------------------------------------- 1 | namespace NostrBot.Web.Utils; 2 | 3 | public static class TaskUtils 4 | { 5 | public static async Task DelaySafely(TimeSpan delay, CancellationToken? cancellationToken) 6 | { 7 | try 8 | { 9 | await Task.Delay(delay, cancellationToken ?? CancellationToken.None); 10 | } 11 | catch (OperationCanceledException) 12 | { 13 | // ignore 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Not found 7 | 8 |

Sorry, there's nothing at this address.

9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /src/Nostr.Client/Communicator/NostrWebsocketCommunicator.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | using Websocket.Client; 3 | 4 | namespace Nostr.Client.Communicator 5 | { 6 | public class NostrWebsocketCommunicator : WebsocketClient, INostrCommunicator 7 | { 8 | /// 9 | public NostrWebsocketCommunicator(Uri url, Func? clientFactory = null) 10 | : base(url, clientFactory) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Utils/ClipboardService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace NostrDebug.Web.Utils; 4 | 5 | public class ClipboardService 6 | { 7 | private readonly IJSRuntime _jsInterop; 8 | public ClipboardService(IJSRuntime jsInterop) 9 | { 10 | _jsInterop = jsInterop; 11 | } 12 | public async Task CopyToClipboard(string? text) 13 | { 14 | await _jsInterop.InvokeVoidAsync("navigator.clipboard.writeText", text ?? string.Empty); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Metadata/NostrMetadataEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostr.Client.Json; 3 | 4 | namespace Nostr.Client.Messages.Metadata 5 | { 6 | public class NostrMetadataEvent : NostrEvent 7 | { 8 | public NostrMetadataEvent(string? content) 9 | { 10 | Content = content; 11 | Metadata = NostrJson.DeserializeSafely(content); 12 | } 13 | 14 | [JsonIgnore] 15 | public NostrMetadata? Metadata { get; init; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/Index.razor.css: -------------------------------------------------------------------------------- 1 | fluent-card { 2 | --card-height: 400px; 3 | --card-width: 500px; 4 | padding: 20px; 5 | margin: 12px; 6 | } 7 | 8 | .class-override { 9 | height: 163px; 10 | width: 300px; 11 | } 12 | 13 | .state-override { 14 | --card-width: 350px; 15 | --card-height: 300px; 16 | --elevation: 6; 17 | } 18 | 19 | .state-override:hover { 20 | --elevation: 12; 21 | } 22 | 23 | .contents { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "NostrDebug.Web": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": true, 7 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 8 | "applicationUrl": "https://localhost:7247;http://localhost:5247", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Nostr.Client/Responses/NostrRawResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Diagnostics; 3 | using Websocket.Client; 4 | 5 | namespace Nostr.Client.Responses 6 | { 7 | [DebuggerDisplay("[{CommunicatorName}] {Message}")] 8 | public class NostrRawResponse 9 | { 10 | public ResponseMessage? Message { get; init; } 11 | 12 | /// 13 | /// Name of the source communicator/relay 14 | /// 15 | [JsonIgnore] 16 | public string CommunicatorName { get; internal set; } = string.Empty; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nostr.Client/Client/INostrClient.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Client 2 | { 3 | public interface INostrClient : IDisposable 4 | { 5 | /// 6 | /// Provided message streams 7 | /// 8 | NostrClientStreams Streams { get; } 9 | 10 | /// 11 | /// Serializes request and sends message via websocket communicator. 12 | /// It logs and re-throws every exception. 13 | /// 14 | /// Request/message to be sent 15 | void Send(T request); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Nostr.Client/Json/IHaveAdditionalData.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Json 2 | { 3 | /// 4 | /// Contains collection of additional unparsed data 5 | /// 6 | public interface IHaveAdditionalData 7 | { 8 | /// 9 | /// Data that wasn't parsed into properties 10 | /// 11 | object[] AdditionalData { get; } 12 | 13 | /// 14 | /// Set additional data, should not be used outside of this library 15 | /// 16 | void SetAdditionalData(object[] data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Logic/NostrEventsQueue.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Channels; 2 | using Nostr.Client.Responses; 3 | 4 | namespace NostrBot.Web.Logic 5 | { 6 | public class NostrEventsQueue 7 | { 8 | private readonly Channel _channel; 9 | public NostrEventsQueue(Channel channel) 10 | { 11 | _channel = channel; 12 | } 13 | 14 | public ChannelReader Reader => _channel.Reader; 15 | 16 | public ChannelWriter Writer => _channel.Writer; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Contacts/NostrContactEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostr.Client.Json; 3 | 4 | namespace Nostr.Client.Messages.Contacts 5 | { 6 | public class NostrContactEvent : NostrEvent 7 | { 8 | public NostrContactEvent(string? content) 9 | { 10 | Content = content; 11 | Relays = NostrJson.DeserializeSafely(content) ?? 12 | new NostrRelays(new Dictionary()); 13 | } 14 | 15 | [JsonIgnore] 16 | public NostrRelays Relays { get; init; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nostr.Client/Responses/NostrEventResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Newtonsoft.Json; 3 | using Nostr.Client.Json; 4 | using Nostr.Client.Messages; 5 | 6 | namespace Nostr.Client.Responses 7 | { 8 | [DebuggerDisplay("[{CommunicatorName}] {MessageType} - {Subscription}")] 9 | public class NostrEventResponse : NostrResponse 10 | { 11 | [ArrayProperty(1)] 12 | public string? Subscription { get; init; } 13 | 14 | [ArrayProperty(2)] 15 | [JsonConverter(typeof(NostrEventConverter))] 16 | public NostrEvent? Event { get; init; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core-branches.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core (branch) 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup .NET Core 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 8.0.x 18 | - name: Install dependencies 19 | run: dotnet restore 20 | - name: Build 21 | run: dotnet build --configuration Release --no-restore 22 | - name: Test 23 | run: dotnet test --no-restore --verbosity normal 24 | -------------------------------------------------------------------------------- /src/Nostr.Client/Json/IHaveAdditionalStringData.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Json 2 | { 3 | /// 4 | /// Contains collection of additional unparsed string data 5 | /// 6 | public interface IHaveAdditionalStringData 7 | { 8 | /// 9 | /// Data that wasn't parsed into properties 10 | /// 11 | string[] AdditionalData { get; } 12 | 13 | /// 14 | /// Set additional data, should not be used outside of this library 15 | /// 16 | void SetAdditionalData(string[] data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nostr.Client/Requests/NostrCloseRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostr.Client.Json; 3 | using Nostr.Client.Messages; 4 | 5 | namespace Nostr.Client.Requests 6 | { 7 | [JsonConverter(typeof(ArrayConverter))] 8 | public class NostrCloseRequest 9 | { 10 | public NostrCloseRequest(string subscription) 11 | { 12 | Subscription = subscription; 13 | } 14 | 15 | [ArrayProperty(0)] 16 | public string Type { get; init; } = NostrMessageTypes.Close; 17 | 18 | [ArrayProperty(1)] 19 | public string Subscription { get; init; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Storage/BotContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace NostrBot.Web.Storage; 4 | 5 | public class BotContext : DbContext 6 | { 7 | protected readonly IConfiguration Configuration; 8 | 9 | public BotContext(IConfiguration configuration) 10 | { 11 | Configuration = configuration; 12 | } 13 | 14 | public DbSet ProcessedEvents { get; set; } = null!; 15 | 16 | protected override void OnConfiguring(DbContextOptionsBuilder options) 17 | { 18 | options.UseSqlite(Configuration.GetConnectionString("BotDatabase")); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | 4 |

Welcome to the Nostr

5 | 6 |

This application presents capabilities of the new C# library for Nostr protocol

7 | 8 |

9 |

Library can be downloaded from Nuget Nostr.Client
10 |
Source code and more info in Github Repository
11 |

12 | 13 |

14 | The app runs in your browser only with no server and is written in Blazor Webassembly 15 |

-------------------------------------------------------------------------------- /test_integration/Nostr.Client.Sample.Console/Nostr.Client.Sample.Console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Nostr.Client/Requests/NostrEventRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostr.Client.Json; 3 | using Nostr.Client.Messages; 4 | 5 | namespace Nostr.Client.Requests 6 | { 7 | [JsonConverter(typeof(ArrayConverter))] 8 | public class NostrEventRequest 9 | { 10 | // for deserialization in tests 11 | private NostrEventRequest() 12 | { 13 | Event = null!; 14 | } 15 | 16 | public NostrEventRequest(NostrEvent eventData) 17 | { 18 | Event = eventData; 19 | } 20 | 21 | [ArrayProperty(0)] 22 | public string Type { get; init; } = NostrMessageTypes.Event; 23 | 24 | [ArrayProperty(1)] 25 | public NostrEvent Event { get; init; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Nostr.Client/Requests/NostrRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostr.Client.Json; 3 | using Nostr.Client.Messages; 4 | 5 | namespace Nostr.Client.Requests 6 | { 7 | [JsonConverter(typeof(ArrayConverter))] 8 | public class NostrRequest 9 | { 10 | public NostrRequest(string subscription, NostrFilter nostrFilter) 11 | { 12 | Subscription = subscription; 13 | NostrFilter = nostrFilter; 14 | } 15 | 16 | [ArrayProperty(0)] 17 | public string Type { get; init; } = NostrMessageTypes.Request; 18 | 19 | [ArrayProperty(1)] 20 | public string Subscription { get; init; } 21 | 22 | [ArrayProperty(2)] 23 | public NostrFilter NostrFilter { get; init; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Metadata/NostrMetadata.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Nostr.Client.Messages.Metadata 4 | { 5 | public class NostrMetadata 6 | { 7 | public string? Name { get; init; } 8 | 9 | public string? About { get; init; } 10 | 11 | public string? Picture { get; init; } 12 | 13 | public string? Nip05 { get; init; } 14 | 15 | public string? Lud16 { get; init; } 16 | 17 | public string? Banner { get; init; } 18 | 19 | public string? Nip57 { get; init; } 20 | 21 | /// 22 | /// Additional unparsed data 23 | /// 24 | [JsonExtensionData] 25 | public Dictionary AdditionalData { get; init; } = new(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/smart-relay/SmartRelay.Api/Hubs/ChatHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | 3 | namespace SmartRelay.Api.Hubs 4 | { 5 | public class ChatHub : Hub 6 | { 7 | public async Task Send(string name, string message) 8 | { 9 | // Call the broadcastMessage method to update clients. 10 | await Clients.All.SendAsync("broadcastMessage", name, message); 11 | } 12 | 13 | public override async Task OnConnectedAsync() 14 | { 15 | await Groups.AddToGroupAsync(Context.ConnectionId, "MyRelay"); 16 | } 17 | 18 | public override async Task OnDisconnectedAsync(Exception? exception) 19 | { 20 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, "MyRelay"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrEventEditJsonDialog.razor.css: -------------------------------------------------------------------------------- 1 | ::deep .monaco-editor-container { 2 | width: 100%; 3 | height: 400px; 4 | background-color: var(--fill-color); 5 | } 6 | 7 | ::deep .monaco-editor-background { 8 | background-color: var(--fill-color); 9 | } 10 | 11 | ::deep .monaco-editor .margin { 12 | background-color: var(--fill-color); 13 | } 14 | 15 | ::deep .decorationsOverviewRuler { 16 | background-color: var(--fill-color); 17 | } 18 | 19 | .content { 20 | padding: 0; 21 | margin-left: calc(var(--design-unit) * 6px); 22 | margin-right: calc(var(--design-unit) * 6px); 23 | margin-top: calc(var(--design-unit) * 6px); 24 | } 25 | 26 | .buttons { 27 | padding-top: 10px; 28 | } 29 | 30 | .error-message { 31 | color: var(--error); 32 | } 33 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrFilterEditJsonDialog.razor.css: -------------------------------------------------------------------------------- 1 | ::deep .monaco-editor-container { 2 | width: 100%; 3 | height: 400px; 4 | background-color: var(--fill-color); 5 | } 6 | 7 | ::deep .monaco-editor-background { 8 | background-color: var(--fill-color); 9 | } 10 | 11 | ::deep .monaco-editor .margin { 12 | background-color: var(--fill-color); 13 | } 14 | 15 | ::deep .decorationsOverviewRuler { 16 | background-color: var(--fill-color); 17 | } 18 | 19 | .content { 20 | padding: 0; 21 | margin-left: calc(var(--design-unit) * 6px); 22 | margin-right: calc(var(--design-unit) * 6px); 23 | margin-top: calc(var(--design-unit) * 6px); 24 | } 25 | 26 | .buttons { 27 | padding-top: 10px; 28 | } 29 | 30 | .error-message { 31 | color: var(--error); 32 | } 33 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Configs/OpenAiConfig.cs: -------------------------------------------------------------------------------- 1 | namespace NostrBot.Web.Configs 2 | { 3 | public class OpenAiConfig 4 | { 5 | public string ApiKey { get; init; } = null!; 6 | public string Organization { get; init; } = null!; 7 | 8 | public string Model { get; init; } = "gpt-3.5-turbo"; 9 | public bool ModelSupportsVision { get; init; } 10 | public string ImageModel { get; init; } = "dall-e-3"; 11 | public int ImageCount { get; init; } = 1; 12 | public string ImageQuality { get; init; } = "hd"; 13 | 14 | public double? Temperature { get; init; } 15 | 16 | public int? MaxTokens { get; init; } 17 | 18 | public double? PresencePenalty { get; init; } 19 | 20 | public double? FrequencyPenalty { get; init; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Contacts/NostrRelayConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Newtonsoft.Json; 3 | 4 | namespace Nostr.Client.Messages.Contacts 5 | { 6 | [DebuggerDisplay("Relay, read: {Read}, write: {Write}")] 7 | public class NostrRelayConfig 8 | { 9 | /// 10 | /// Relay is enabled for loading data from 11 | /// 12 | public bool Read { get; init; } 13 | 14 | /// 15 | /// Relay is enabled for sending data to 16 | /// 17 | public bool Write { get; init; } 18 | 19 | /// 20 | /// Additional unparsed data 21 | /// 22 | [JsonExtensionData] 23 | public Dictionary AdditionalData { get; init; } = new(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/smart-relay/SmartRelay.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "https://localhost:7272;http://localhost:5276", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | }, 14 | "http": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "launchUrl": "swagger", 19 | "applicationUrl": "http://localhost:5276", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Utils/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NostrBot.Web.Utils 2 | { 3 | public static class ConfigurationExtensions 4 | { 5 | /// 6 | /// Configure configuration from the target section and return immediately. 7 | /// 8 | public static TConfiguration Configure(this IConfiguration configuration, IServiceCollection services, string sectionName) 9 | where TConfiguration : class, new() 10 | { 11 | var section = configuration.GetSection(sectionName); 12 | services.Configure(section); 13 | 14 | var settings = new TConfiguration(); 15 | section.Bind(settings, o => o.BindNonPublicProperties = true); 16 | 17 | return settings; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System; 2 | @using System.Net.Http 3 | @using System.Net.Http.Json 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 9 | @using Microsoft.JSInterop 10 | @using Nostr.Client.Utils 11 | @using Nostr.Client.Responses 12 | @using Nostr.Client.Messages 13 | @using Nostr.Client.Messages.Metadata 14 | @using Nostr.Client.Messages.Contacts 15 | @using NostrDebug.Web 16 | @using NostrDebug.Web.Utils 17 | @using NostrDebug.Web.Components 18 | @using NostrDebug.Web.Pages 19 | @using NostrDebug.Web.Events 20 | @using NostrDebug.Web.Signing 21 | @using NostrDebug.Web.Relay 22 | @using NostrDebug.Web.External 23 | @using Microsoft.Fast.Components.FluentUI 24 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 9 | WORKDIR /src 10 | COPY ["apps/nostr-bot/NostrBot.Web/NostrBot.Web.csproj", "apps/nostr-bot/NostrBot.Web/"] 11 | RUN dotnet restore "apps/nostr-bot/NostrBot.Web/NostrBot.Web.csproj" 12 | COPY . . 13 | WORKDIR "/src/apps/nostr-bot/NostrBot.Web" 14 | RUN dotnet build "NostrBot.Web.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "NostrBot.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "NostrBot.Web.dll"] -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Storage/Migrations/20230402175039_SecondaryContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace NostrBot.Web.Storage.Migrations 6 | { 7 | /// 8 | public partial class SecondaryContext : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "ReplySecondaryContextId", 15 | table: "ProcessedEvents", 16 | type: "TEXT", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "ReplySecondaryContextId", 25 | table: "ProcessedEvents"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Nostr.Client/Utils/HashExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | 4 | namespace Nostr.Client.Utils 5 | { 6 | public static class HashExtensions 7 | { 8 | /// 9 | /// Compute SHA256 hash from the given input 10 | /// 11 | public static byte[] GetSha256(string data) 12 | { 13 | var sha256 = SHA256.HashData(FromString(data)); 14 | return sha256; 15 | } 16 | 17 | /// 18 | /// Get bytes from the string (UTF8) 19 | /// 20 | public static byte[] FromString(string data) 21 | { 22 | return Encoding.UTF8.GetBytes(data); 23 | } 24 | 25 | /// 26 | /// Get string (UTF8) from bytes 27 | /// 28 | public static string ToString(byte[] bytes) 29 | { 30 | return Encoding.UTF8.GetString(bytes); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Configs/BotConfig.cs: -------------------------------------------------------------------------------- 1 | namespace NostrBot.Web.Configs 2 | { 3 | public class BotConfig 4 | { 5 | public string? BotDescription { get; init; } 6 | 7 | public string? BotWhois { get; init; } 8 | 9 | public string[] BotAdminPubKeys { get; init; } = Array.Empty(); 10 | 11 | public string[] BotIgnoreListPubKeys { get; init; } = Array.Empty(); 12 | 13 | public bool SlowdownReplies { get; init; } 14 | 15 | public double SlowdownPerTokenSec { get; init; } = 1; 16 | 17 | public int LimitForHistoricalTokens { get; init; } = 2000; 18 | 19 | public bool ListenToGlobalFeed { get; init; } 20 | 21 | public bool ReactToRootEventsInGlobalFeed { get; init; } 22 | 23 | public bool ReactToThreadsInGlobalFeed { get; init; } 24 | 25 | public bool ReactToThreadsInLiveChat { get; init; } 26 | 27 | public string[] GlobalFeedKeywords { get; init; } = Array.Empty(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Nostr.Client/Responses/NostrOkResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Nostr.Client.Json; 3 | 4 | namespace Nostr.Client.Responses 5 | { 6 | [DebuggerDisplay("[{CommunicatorName}] {MessageType} - success: {Accepted} {Message}")] 7 | public class NostrOkResponse : NostrResponse 8 | { 9 | /// 10 | /// Related event id 11 | /// 12 | [ArrayProperty(1)] 13 | public string? EventId { get; init; } 14 | 15 | /// 16 | /// Returns true when the event was accepted and stored on the server (even for duplicates). 17 | /// Returns false when the event was rejected and not stored. 18 | /// 19 | [ArrayProperty(2)] 20 | public bool Accepted { get; init; } 21 | 22 | /// 23 | /// Additional information as to why the command succeeded or failed. 24 | /// 25 | [ArrayProperty(3)] 26 | public string? Message { get; init; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrKindSelectSingle.razor: -------------------------------------------------------------------------------- 1 | @using Nostr.Client.Messages 2 | @inherits FluentInputBase 3 | 4 | 5 | Kind 6 | 9 | 10 | 11 | 12 | @code { 13 | private readonly string[] _allKinds = Enum.GetValues(typeof(NostrKind)).Cast().Select(x => x.ToString()).ToArray(); 14 | 15 | protected override bool TryParseValueFromString(string? value, out NostrKind result, out string validationErrorMessage) 16 | { 17 | if (int.TryParse(value, out var parsed)) 18 | { 19 | result = (NostrKind)parsed; 20 | validationErrorMessage = string.Empty; 21 | return true; 22 | } 23 | 24 | result = NostrKind.ShortTextNote; 25 | validationErrorMessage = "Failed to parse Kind number"; 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/MainLayout.razor.js: -------------------------------------------------------------------------------- 1 | export function isDevice() { 2 | return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(navigator.userAgent); 3 | } 4 | 5 | export function switchHighlightStyle(dark) { 6 | if (dark) { 7 | document 8 | .querySelector(`link[title="dark"]`) 9 | .removeAttribute("disabled"); 10 | document 11 | .querySelector(`link[title="light"]`) 12 | .setAttribute("disabled", "disabled"); 13 | } 14 | else { 15 | document 16 | .querySelector(`link[title="light"]`) 17 | .removeAttribute("disabled"); 18 | document 19 | .querySelector(`link[title="dark"]`) 20 | .setAttribute("disabled", "disabled"); 21 | } 22 | } 23 | 24 | export function isDarkMode() { 25 | let matched = window.matchMedia("(prefers-color-scheme: dark)").matches; 26 | 27 | if (matched) 28 | return true; 29 | else 30 | return false; 31 | } 32 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/External/ExternalLinks.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Utils; 2 | 3 | namespace NostrDebug.Web.External 4 | { 5 | public class ExternalLinks 6 | { 7 | public static string GetLinkToProfile(string? key) 8 | { 9 | return $"https://snort.social/p/{FormatToNpub(key)}"; 10 | } 11 | 12 | public static string GetLinkToEvent(string? key) 13 | { 14 | return $"https://snort.social/e/{FormatToNote(key)}"; 15 | } 16 | 17 | public static string? FormatToNpub(string? hexKey) => FormatToBech32(hexKey, "npub"); 18 | public static string? FormatToNote(string? hexKey) => FormatToBech32(hexKey, "note"); 19 | public static string? FormatToBech32(string? hexKey, string hrp) 20 | { 21 | try 22 | { 23 | return NostrConverter.ToBech32(hexKey, hrp); 24 | } 25 | catch (Exception) 26 | { 27 | return hexKey; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "swagger", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "dotnetRunMessages": true, 11 | "applicationUrl": "http://localhost:5233" 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | }, 20 | "dotnetRunMessages": true, 21 | "applicationUrl": "https://localhost:7177;http://localhost:5233" 22 | }, 23 | "Docker": { 24 | "commandName": "Docker", 25 | "launchBrowser": true, 26 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", 27 | "publishAllPorts": true, 28 | "useSSL": true 29 | } 30 | }, 31 | "$schema": "https://json.schemastore.org/launchsettings.json" 32 | } -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrEventViewDialog.razor: -------------------------------------------------------------------------------- 1 | @using System.ComponentModel.DataAnnotations 2 | 3 | 4 |
5 | 6 |
7 |
8 | 9 | @code { 10 | private FluentDialog? _eventDialog; 11 | 12 | [Parameter] 13 | public NostrEvent? Event { get; set; } = new(); 14 | 15 | public void ShowEvent(NostrEvent? ev) 16 | { 17 | Event = ev; 18 | if (Event == null) 19 | return; 20 | _eventDialog?.Show(); 21 | } 22 | 23 | protected override void OnAfterRender(bool firstRender) 24 | { 25 | if (firstRender) 26 | _eventDialog!.Hide(); 27 | } 28 | 29 | private void OnHideEvent(DialogEventArgs? args) 30 | { 31 | if (args?.Reason != null && args.Reason == "dismiss") 32 | { 33 | _eventDialog!.Hide(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Storage/ProcessedEvent.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Messages; 2 | 3 | namespace NostrBot.Web.Storage 4 | { 5 | public class ProcessedEvent 6 | { 7 | public long ProcessedEventId { get; set; } 8 | 9 | public DateTime Created { get; set; } 10 | 11 | public string? Subscription { get; set; } 12 | 13 | public string Relay { get; set; } = null!; 14 | 15 | public string? NostrEventId { get; set; } 16 | 17 | public string? NostrEventContent { get; set; } 18 | 19 | public string? NostrEventPubkey { get; set; } 20 | 21 | public NostrKind NostrEventKind { get; set; } 22 | 23 | public DateTime? NostrEventCreatedAt { get; set; } 24 | 25 | public string? NostrEventTagP { get; set; } 26 | 27 | public string? NostrEventTagE { get; set; } 28 | 29 | public string ReplyContextId { get; set; } = null!; 30 | 31 | public string? ReplySecondaryContextId { get; set; } 32 | 33 | public string? GeneratedReply { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/NostrDebug.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | true 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using NostrDebug.Web; 3 | using Microsoft.AspNetCore.Components.Web; 4 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 5 | using Microsoft.Fast.Components.FluentUI; 6 | using NostrDebug.Web.Relay; 7 | using NostrDebug.Web.Events; 8 | using NostrDebug.Web.Utils; 9 | 10 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 11 | builder.RootComponents.Add("#app"); 12 | builder.RootComponents.Add("head::after"); 13 | 14 | builder.Services.AddBlazoredLocalStorageAsSingleton(); 15 | 16 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 17 | LibraryConfiguration config = new(ConfigurationGenerator.GetIconConfiguration(), ConfigurationGenerator.GetEmojiConfiguration()); 18 | builder.Services.AddFluentUIComponents(config); 19 | 20 | builder.Services.AddSingleton(); 21 | builder.Services.AddSingleton(); 22 | 23 | builder.Services.AddTransient(); 24 | 25 | await builder.Build().RunAsync(); 26 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/PageHeader.razor: -------------------------------------------------------------------------------- 1 | @if (DisplayBrowserTitle) 2 | { 3 | @Title 4 | } 5 | 6 |
7 | @if (!string.IsNullOrWhiteSpace(Title)) 8 | { 9 |

@Title

10 | } 11 | @if (!string.IsNullOrWhiteSpace(Title2)) 12 | { 13 |

@Title2

14 | } 15 | @if (!string.IsNullOrWhiteSpace(Title3)) 16 | { 17 |

@Title3

18 | } 19 | @if (!string.IsNullOrWhiteSpace(Subtitle)) 20 | { 21 |

@Subtitle

22 | } 23 | 24 |
25 | 26 | @code { 27 | [Parameter] 28 | public string? Title { get; set; } 29 | 30 | [Parameter] 31 | public string? Title2 { get; set; } 32 | 33 | [Parameter] 34 | public string? Title3 { get; set; } 35 | 36 | [Parameter] 37 | public string? Subtitle { get; set; } 38 | 39 | [Parameter] 40 | public bool DisplayBrowserTitle { get; set; } = true; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Mutable/NostrEventTagsMutable.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Messages.Mutable 2 | { 3 | public class NostrEventTagsMutable : NostrEventTags, ICollection 4 | { 5 | public NostrEventTagsMutable() 6 | { 7 | } 8 | 9 | public NostrEventTagsMutable(IEnumerable collection) : base(collection) 10 | { 11 | } 12 | 13 | public void Add(NostrEventTag item) 14 | { 15 | Collection.Add(item); 16 | } 17 | 18 | public void Clear() 19 | { 20 | Collection.Clear(); 21 | } 22 | 23 | public bool Contains(NostrEventTag item) 24 | { 25 | return Collection.Contains(item); 26 | } 27 | 28 | public void CopyTo(NostrEventTag[] array, int arrayIndex) 29 | { 30 | Collection.CopyTo(array, arrayIndex); 31 | } 32 | 33 | public bool Remove(NostrEventTag item) 34 | { 35 | return Collection.Remove(item); 36 | } 37 | 38 | public bool IsReadOnly => false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/smart-relay/SmartRelay.Api/SmartRelay.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Nostr.Client/Identifiers/NostrRelayIdentifier.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Utils; 2 | 3 | namespace Nostr.Client.Identifiers 4 | { 5 | public class NostrRelayIdentifier : NostrIdentifier 6 | { 7 | public NostrRelayIdentifier(string relay) 8 | { 9 | Relay = relay; 10 | } 11 | 12 | public override string Hrp => "nrelay"; 13 | public override string Special => Relay; 14 | public string Relay { get; init; } 15 | 16 | public override string ToBech32() 17 | { 18 | var tlvData = new List<(byte, byte[])> 19 | { 20 | (SpecialKey, HashExtensions.FromString(Relay)), 21 | }; 22 | var tlv = NostrIdentifierParser.BuildTlv(tlvData.ToArray()); 23 | return NostrConverter.ToBech32(tlv, Hrp) ?? string.Empty; 24 | } 25 | 26 | public static NostrRelayIdentifier Parse(byte[] data) 27 | { 28 | var tlv = NostrIdentifierParser.ParseTlv(data); 29 | return new NostrRelayIdentifier( 30 | FindSpecialString(tlv) 31 | ); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup .NET Core 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 8.0.x 18 | 19 | - name: Install dependencies 20 | run: dotnet restore 21 | - name: Build 22 | run: dotnet build --configuration Release --no-restore 23 | - name: Test 24 | run: dotnet test --no-restore --verbosity normal 25 | - name: Pack Nostr.Client project 26 | run: dotnet pack src/Nostr.Client/Nostr.Client.csproj --no-build --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg -o . 27 | - name: Publish Nostr.Client (NuGet) 28 | run: dotnet nuget push *.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source "https://api.nuget.org/v3/index.json" --skip-duplicate 29 | # - name: Publish Nostr.Client (Github) 30 | # run: dotnet nuget push *.nupkg --api-key ${{secrets.PUBLISH_TO_GITHUB_TOKEN}} --source "https://nuget.pkg.github.com/marfusios/index.json" --skip-duplicate 31 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Mutable/NostrEventTagMutable.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Nostr.Client.Messages.Mutable 4 | { 5 | [DebuggerDisplay("TagMutable {TagIdentifier} additional: {AdditionalData.Length}")] 6 | public class NostrEventTagMutable 7 | { 8 | public NostrEventTagMutable() 9 | { 10 | } 11 | 12 | public NostrEventTagMutable(string? identifier, params string[] data) 13 | { 14 | TagIdentifier = identifier ?? string.Empty; 15 | AdditionalData = data; 16 | } 17 | 18 | public string TagIdentifier { get; set; } = string.Empty; 19 | 20 | public string[] AdditionalData { get; set; } = Array.Empty(); 21 | 22 | public NostrEventTag ToTag() 23 | { 24 | return new NostrEventTag(TagIdentifier, AdditionalData); 25 | } 26 | 27 | public static NostrEventTagMutable FromTag(NostrEventTag tag) 28 | { 29 | return new NostrEventTagMutable 30 | { 31 | TagIdentifier = tag.TagIdentifier, 32 | AdditionalData = tag.AdditionalData 33 | }; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Nostr.Client.Tests/Nostr.Client.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/Relay.razor: -------------------------------------------------------------------------------- 1 | @page "/relay" 2 | @inject RelayList Relays 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | @foreach (var relay in Relays.Relays) 11 | { 12 | 13 | } 14 | 15 |
16 |

History

17 | 18 |
    19 | @foreach (var item in _history.ToArray().Reverse()) 20 | { 21 |
  • 22 | @item 23 |
  • 24 | } 25 |
26 |
27 | 28 |
29 | 30 | @code { 31 | private readonly List _history = new(); 32 | 33 | protected override void OnInitialized() 34 | { 35 | Relays.HistoryStream.Subscribe(OnHistory); 36 | base.OnInitialized(); 37 | } 38 | 39 | private void OnConnect(RelayConnection relay) 40 | { 41 | Relays.Connect(relay); 42 | } 43 | 44 | private void OnHistory(string item) 45 | { 46 | _history.Add(item); 47 | StateHasChanged(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | jobs: 11 | build: 12 | 13 | concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession. 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Setup .NET Core 19 | uses: actions/setup-dotnet@v1 20 | with: 21 | dotnet-version: 8.0.x 22 | - name: Publish with dotnet 23 | run: dotnet publish apps/nostr-debug/NostrDebug.Web -f net8.0 --configuration Release --output build 24 | 25 | # copy index.html to 404.html to serve the same file when a file is not found 26 | - name: Copy index.html to 404.html 27 | run: cp build/wwwroot/index.html build/wwwroot/404.html 28 | 29 | # add .nojekyll file to tell GitHub pages to not treat this as a Jekyll project. (Allow files and folders starting with an underscore) 30 | - name: Add .nojekyll file 31 | run: touch build/wwwroot/.nojekyll 32 | 33 | - name: Deploy to Github Pages 34 | uses: JamesIves/github-pages-deploy-action@v4 35 | with: 36 | branch: gh-pages # The branch the action should deploy to. 37 | folder: build/wwwroot # The folder the action should deploy. -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Controllers/ChatAiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OpenAI; 3 | using OpenAI.Chat; 4 | 5 | namespace NostrBot.Web.Controllers 6 | { 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class ChatAiController : ControllerBase 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public ChatAiController(ILogger logger) 14 | { 15 | _logger = logger; 16 | } 17 | 18 | [HttpGet] 19 | public async Task Get(string message) 20 | { 21 | var api = new OpenAIClient(new OpenAIAuthentication("")); 22 | //var models = await api.ModelsEndpoint.GetModelsAsync(); 23 | //foreach (var model in models) 24 | //{ 25 | // _logger.LogInformation("Available model: {model}", model.ToString()); 26 | //} 27 | 28 | var chatPrompts = new List 29 | { 30 | new(Role.User, message) 31 | }; 32 | 33 | var chatRequest = new ChatRequest(chatPrompts); 34 | var result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); 35 | 36 | var response = string.Join(Environment.NewLine, result.Choices.Select(x => x.Message.Content)); 37 | return response; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /apps/smart-relay/SmartRelay.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], 4 | "MinimumLevel": { 5 | "Default": "Information", 6 | "Override": { 7 | "System": "Warning", 8 | "Microsoft": "Warning" 9 | } 10 | }, 11 | "WriteTo": [ 12 | { 13 | "Name": "Console", 14 | "Args": { 15 | "outputTemplate": "{Timestamp:HH:mm:ss.fff} [{Level:u3}] ({ThreadId}) {Message}{NewLine}" 16 | } 17 | }, 18 | { 19 | "Name": "File", 20 | "Args": { 21 | "path": "logs/log.txt", 22 | "rollingInterval": "Day", 23 | "outputTemplate": "{Timestamp:HH:mm:ss.ffffff zzz} [{Level:u3}] ({ThreadId}) {SourceContext} {Message}{NewLine}{Exception}" 24 | } 25 | } 26 | ], 27 | "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], 28 | "Destructure": [ 29 | { 30 | "Name": "ToMaximumDepth", 31 | "Args": { "maximumDestructuringDepth": 4 } 32 | }, 33 | { 34 | "Name": "ToMaximumStringLength", 35 | "Args": { "maximumStringLength": 500 } 36 | }, 37 | { 38 | "Name": "ToMaximumCollectionCount", 39 | "Args": { "maximumCollectionCount": 100 } 40 | } 41 | ], 42 | "Properties": { 43 | "Application": "SmartRelay" 44 | } 45 | }, 46 | "AllowedHosts": "*" 47 | } 48 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Zaps/NostrZapReceiptEvent.cs: -------------------------------------------------------------------------------- 1 | using BTCPayServer.Lightning; 2 | using NBitcoin; 3 | using Newtonsoft.Json; 4 | using Nostr.Client.Json; 5 | 6 | namespace Nostr.Client.Messages.Zaps 7 | { 8 | public class NostrZapReceiptEvent : NostrEvent 9 | { 10 | /// 11 | /// Hex-encoded pubkey of the recipient 12 | /// 13 | [JsonIgnore] 14 | public string? RecipientPubkey => Tags?.FindFirstTagValue("p"); 15 | 16 | /// 17 | /// Optional hex-encoded event id. Clients MUST include this if zapping an event rather than a person. 18 | /// 19 | [JsonIgnore] 20 | public string? EventId => Tags?.FindFirstTagValue("e"); 21 | 22 | [JsonIgnore] 23 | public string? Bolt11 => Tags?.FindFirstTagValue("bolt11"); 24 | 25 | [JsonIgnore] 26 | public NostrEvent? Description => NostrJson.DeserializeSafely(Tags?.FindFirstTagValue("description")); 27 | 28 | [JsonIgnore] 29 | public string? Preimage => Tags?.FindFirstTagValue("preimage"); 30 | 31 | public BOLT11PaymentRequest? DecodeBolt11() 32 | { 33 | if (string.IsNullOrWhiteSpace(Bolt11)) 34 | return null; 35 | 36 | return BOLT11PaymentRequest.TryParse(Bolt11, out var request, Network.Main) ? 37 | request : null; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Nostr.Client/Identifiers/NostrProfileIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Nostr.Client.Utils; 3 | 4 | namespace Nostr.Client.Identifiers 5 | { 6 | public class NostrProfileIdentifier : NostrIdentifier 7 | { 8 | public NostrProfileIdentifier(string pubkey, string[]? relays) 9 | { 10 | Pubkey = pubkey; 11 | Relays = relays ?? Array.Empty(); 12 | } 13 | 14 | public override string Hrp => "nprofile"; 15 | public override string Special => Pubkey; 16 | public string Pubkey { get; init; } 17 | public string[] Relays { get; init; } 18 | 19 | public override string ToBech32() 20 | { 21 | var tlvData = new List<(byte, byte[])> 22 | { 23 | (SpecialKey, Convert.FromHexString(Pubkey)), 24 | }; 25 | tlvData.AddRange(Relays.Select(relay => (RelayKey, Encoding.ASCII.GetBytes(relay)))); 26 | var tlv = NostrIdentifierParser.BuildTlv(tlvData.ToArray()); 27 | return NostrConverter.ToBech32(tlv, Hrp) ?? string.Empty; 28 | } 29 | 30 | public static NostrProfileIdentifier Parse(byte[] data) 31 | { 32 | var tlv = NostrIdentifierParser.ParseTlv(data); 33 | return new NostrProfileIdentifier( 34 | FindSpecialHex(tlv), 35 | FindRelays(tlv) 36 | ); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Nostr.Client/Responses/NostrResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostr.Client.Json; 3 | using System.Diagnostics; 4 | 5 | namespace Nostr.Client.Responses 6 | { 7 | [JsonConverter(typeof(ArrayConverter))] 8 | [DebuggerDisplay("[{CommunicatorName}] {MessageType}")] 9 | public class NostrResponse : IHaveAdditionalData 10 | { 11 | [ArrayProperty(0)] 12 | public string? MessageType { get; init; } 13 | 14 | /// 15 | /// Additional data that are not yet handled by this library (parsed into properties) 16 | /// 17 | public object[] AdditionalData { get; private set; } = Array.Empty(); 18 | 19 | /// 20 | /// Name of the source communicator/relay 21 | /// 22 | [JsonIgnore] 23 | public string CommunicatorName { get; internal set; } = string.Empty; 24 | 25 | /// 26 | /// Client timestamp of the received response, UTC 27 | /// 28 | [JsonIgnore] 29 | public DateTime ReceivedTimestamp { get; internal set; } = DateTime.UtcNow; 30 | 31 | /// 32 | /// Set additional data, should not be used outside of this library. 33 | /// Hidden behind explicit interface implementation to avoid accidental usage. 34 | /// 35 | void IHaveAdditionalData.SetAdditionalData(object[] data) 36 | { 37 | AdditionalData = data; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/smart-relay/SmartRelay.Api/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.SignalR; 3 | using SmartRelay.Api.Hubs; 4 | 5 | namespace SmartRelay.Api.Controllers 6 | { 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class WeatherForecastController : ControllerBase 10 | { 11 | private static readonly string[] Summaries = new[] 12 | { 13 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 14 | }; 15 | 16 | private readonly IHubContext _hubContext; 17 | private readonly ILogger _logger; 18 | 19 | public WeatherForecastController(ILogger logger, IHubContext hubContext) 20 | { 21 | _logger = logger; 22 | _hubContext = hubContext; 23 | } 24 | 25 | [HttpGet(Name = "GetWeatherForecast")] 26 | public IEnumerable Get() 27 | { 28 | _hubContext.Clients.Group("MyRelay").SendAsync("NOTICE", "Something happened"); 29 | 30 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 31 | { 32 | Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), 33 | TemperatureC = Random.Shared.Next(-20, 55), 34 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 35 | }) 36 | .ToArray(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Nostr.Client/Json/NostrJson.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Nostr.Client.Json 4 | { 5 | /// 6 | /// Nostr JSON serializer and deserializer 7 | /// 8 | public static class NostrJson 9 | { 10 | /// 11 | /// Serialize Nostr type into json, use preconfigured serializer 12 | /// 13 | public static string? Serialize(TNostrType? obj) 14 | { 15 | if (obj == null) 16 | return null; 17 | return JsonConvert.SerializeObject(obj, NostrSerializer.Settings); 18 | } 19 | 20 | /// 21 | /// Deserialize json into Nostr type, use preconfigured serializer 22 | /// 23 | public static TNostrType? Deserialize(string? json) where TNostrType : class 24 | { 25 | if (json == null) 26 | return null; 27 | return JsonConvert.DeserializeObject(json, NostrSerializer.Settings); 28 | } 29 | 30 | /// 31 | /// Deserialize json into Nostr type, use preconfigured serializer, swallow any exception 32 | /// 33 | public static T? DeserializeSafely(string? content) 34 | { 35 | try 36 | { 37 | return content == null ? 38 | default : 39 | JsonConvert.DeserializeObject(content, NostrSerializer.Settings); 40 | } 41 | catch (Exception) 42 | { 43 | return default; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/smart-relay/SmartRelay.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using SmartRelay.Api.Hubs; 3 | 4 | namespace SmartRelay.Api; 5 | 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var configuration = new ConfigurationBuilder() 11 | .SetBasePath(Directory.GetCurrentDirectory()) 12 | .AddJsonFile("appsettings.json") 13 | .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true) 14 | .Build(); 15 | 16 | Log.Logger = new LoggerConfiguration() 17 | .ReadFrom.Configuration(configuration) 18 | .CreateLogger(); 19 | 20 | try 21 | { 22 | Log.Information("Starting web application"); 23 | var builder = WebApplication.CreateBuilder(args); 24 | 25 | builder.Host.UseSerilog(); 26 | 27 | builder.Services.AddSignalR(); 28 | builder.Services.AddControllers(); 29 | builder.Services.AddEndpointsApiExplorer(); 30 | builder.Services.AddSwaggerGen(); 31 | 32 | var app = builder.Build(); 33 | 34 | if (app.Environment.IsDevelopment()) 35 | { 36 | app.UseSwagger(); 37 | app.UseSwaggerUI(); 38 | } 39 | 40 | app.UseHttpsRedirection(); 41 | 42 | app.UseAuthorization(); 43 | 44 | app.MapControllers(); 45 | app.MapHub("/chat"); 46 | 47 | app.Run(); 48 | } 49 | catch (Exception ex) 50 | { 51 | Log.Fatal(ex, "Application terminated unexpectedly"); 52 | } 53 | finally 54 | { 55 | Log.CloseAndFlush(); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Nostr.Client/Requests/NostrFilter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostr.Client.Messages; 3 | 4 | namespace Nostr.Client.Requests 5 | { 6 | public class NostrFilter 7 | { 8 | /// 9 | /// A list of event ids or prefixes 10 | /// 11 | public string[]? Ids { get; set; } 12 | 13 | /// 14 | /// A list of pubkeys or prefixes, the pubkey of an event must be one of these 15 | /// 16 | public string[]? Authors { get; set; } 17 | 18 | /// 19 | /// A list of a kind numbers 20 | /// 21 | public NostrKind[]? Kinds { get; set; } 22 | 23 | /// 24 | /// A list of event ids that are referenced in an "e" tag 25 | /// 26 | [JsonProperty("#e")] 27 | public string[]? E { get; set; } 28 | 29 | /// 30 | /// A list of pubkeys that are referenced in a "p" tag 31 | /// 32 | [JsonProperty("#p")] 33 | public string[]? P { get; set; } 34 | 35 | /// 36 | /// A list of coordinates to events in an "a" tag 37 | /// 38 | [JsonProperty("#a")] 39 | public string[]? A { get; set; } 40 | 41 | /// 42 | /// Events must be newer than this to pass 43 | /// 44 | public DateTime? Since { get; set; } 45 | 46 | /// 47 | /// Events must be older than this to pass 48 | /// 49 | public DateTime? Until { get; set; } 50 | 51 | /// 52 | /// Maximum number of events to be returned in the initial query 53 | /// 54 | public int? Limit { get; set; } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/NostrKind.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Messages 2 | { 3 | public enum NostrKind 4 | { 5 | Metadata = 0, 6 | ShortTextNote = 1, 7 | RecommendRelay = 2, 8 | Contacts = 3, 9 | EncryptedDm = 4, 10 | EventDeletion = 5, 11 | Reserved = 6, 12 | Reaction = 7, 13 | BadgeAward = 8, 14 | 15 | GenericRepost = 16, 16 | 17 | // nip-28 public chat 18 | ChannelCreation = 40, 19 | ChannelMetadata = 41, 20 | ChannelMessage = 42, 21 | ChannelHideMessage = 43, 22 | ChanelMuteUser = 44, 23 | 24 | // nip-28 public chat reserved [45-49] 25 | 26 | FileMetadata = 1063, 27 | LiveChatMessage = 1311, 28 | 29 | Reporting = 1984, 30 | Label = 1985, 31 | 32 | ZapRequest = 9734, 33 | Zap = 9735, 34 | 35 | MuteList = 10000, 36 | PinList = 10001, 37 | RelayListMetadata = 10002, 38 | 39 | WalletInfo = 13194, 40 | ClientAuthentication = 22242, 41 | WalletRequest = 23194, 42 | WalletResponse = 23195, 43 | NostrConnect = 24133, 44 | HttpAuth = 27235, 45 | 46 | CategorizedPeopleList = 30000, 47 | CategorizedBookmarkList = 30001, 48 | 49 | ProfileBadges = 30008, 50 | BadgeDefinition = 30009, 51 | 52 | LongFormContent = 30023, 53 | DraftLongFormContent = 30024, 54 | 55 | ApplicationSpecificData = 30078, 56 | 57 | LiveEvent = 30311, 58 | 59 | ClassifiedListing = 30402, 60 | 61 | // nip-16 regular events [ 1000- 9999] 62 | // nip-16 replaceable events [10000-19999] 63 | // nip-16 ephemeral events [20000-29999] 64 | // nip-33 parameterized replaceable events [30000-39999] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Nostr.Client/Json/NostrSerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Converters; 2 | using Newtonsoft.Json.Serialization; 3 | using Newtonsoft.Json; 4 | 5 | namespace Nostr.Client.Json 6 | { 7 | public class NostrSerializer 8 | { 9 | /// 10 | /// Unified JSON settings 11 | /// 12 | public static JsonSerializerSettings Settings => new() 13 | { 14 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 15 | Formatting = Formatting.None, 16 | NullValueHandling = NullValueHandling.Ignore, 17 | ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, 18 | Converters = new List 19 | { 20 | new UnixDateTimeConverter() 21 | }, 22 | ContractResolver = new CamelCasePropertyNamesContractResolver() 23 | }; 24 | 25 | /// 26 | /// Unified JSON settings that serializes messages into array 27 | /// 28 | public static JsonSerializerSettings ArraySettings = new() 29 | { 30 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 31 | Formatting = Formatting.None, 32 | NullValueHandling = NullValueHandling.Ignore, 33 | ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, 34 | Converters = new List 35 | { 36 | new ArrayConverter(), 37 | new UnixDateTimeConverter() 38 | }, 39 | ContractResolver = new CamelCasePropertyNamesContractResolver() 40 | }; 41 | 42 | /// 43 | /// Custom preconfigured serializer 44 | /// 45 | public static readonly JsonSerializer Serializer = JsonSerializer.Create(Settings); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Nostr.Client/Identifiers/NostrEventIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Nostr.Client.Messages; 3 | using Nostr.Client.Utils; 4 | 5 | namespace Nostr.Client.Identifiers 6 | { 7 | public class NostrEventIdentifier : NostrIdentifier 8 | { 9 | public NostrEventIdentifier(string eventId, string? pubkey, string[]? relays, NostrKind? kind) 10 | { 11 | EventId = eventId; 12 | Pubkey = pubkey; 13 | Kind = kind; 14 | Relays = relays ?? Array.Empty(); 15 | } 16 | 17 | public override string Hrp => "nevent"; 18 | public override string Special => EventId; 19 | public string EventId { get; init; } 20 | public string? Pubkey { get; init; } 21 | public string[] Relays { get; init; } 22 | public NostrKind? Kind { get; init; } 23 | 24 | public override string ToBech32() 25 | { 26 | var tlvData = new List<(byte, byte[])> 27 | { 28 | (SpecialKey, Convert.FromHexString(EventId)) 29 | }; 30 | if (!string.IsNullOrWhiteSpace(Pubkey)) 31 | tlvData.Add((AuthorKey, Convert.FromHexString(Pubkey))); 32 | tlvData.AddRange(Relays.Select(relay => (RelayKey, Encoding.ASCII.GetBytes(relay)))); 33 | if (Kind != null) 34 | tlvData.Add((KindKey, WriteKind(Kind.Value))); 35 | var tlv = NostrIdentifierParser.BuildTlv(tlvData.ToArray()); 36 | return NostrConverter.ToBech32(tlv, Hrp) ?? string.Empty; 37 | } 38 | 39 | public static NostrEventIdentifier Parse(byte[] data) 40 | { 41 | var tlv = NostrIdentifierParser.ParseTlv(data); 42 | return new NostrEventIdentifier( 43 | FindSpecialHex(tlv), 44 | FindAuthor(tlv), 45 | FindRelays(tlv), 46 | FindKind(tlv) 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Nostr.Client/Identifiers/NostrAddressIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Nostr.Client.Messages; 3 | using Nostr.Client.Utils; 4 | 5 | namespace Nostr.Client.Identifiers 6 | { 7 | public class NostrAddressIdentifier : NostrIdentifier 8 | { 9 | public NostrAddressIdentifier(string identifier, string? pubkey, string[]? relays, NostrKind? kind) 10 | { 11 | Identifier = identifier; 12 | Pubkey = pubkey; 13 | Kind = kind; 14 | Relays = relays ?? Array.Empty(); 15 | } 16 | 17 | public override string Hrp => "naddr"; 18 | public override string Special => Identifier; 19 | public string Identifier { get; init; } 20 | public string? Pubkey { get; init; } 21 | public string[] Relays { get; init; } 22 | public NostrKind? Kind { get; init; } 23 | 24 | public override string ToBech32() 25 | { 26 | var tlvData = new List<(byte, byte[])> 27 | { 28 | (SpecialKey, HashExtensions.FromString(Identifier)), 29 | }; 30 | tlvData.AddRange(Relays.Select(relay => (RelayKey, Encoding.ASCII.GetBytes(relay)))); 31 | if (!string.IsNullOrWhiteSpace(Pubkey)) 32 | tlvData.Add((AuthorKey, Convert.FromHexString(Pubkey))); 33 | if (Kind != null) 34 | tlvData.Add((KindKey, WriteKind(Kind.Value))); 35 | var tlv = NostrIdentifierParser.BuildTlv(tlvData.ToArray()); 36 | return NostrConverter.ToBech32(tlv, Hrp) ?? string.Empty; 37 | } 38 | 39 | public static NostrAddressIdentifier Parse(byte[] data) 40 | { 41 | var tlv = NostrIdentifierParser.ParseTlv(data); 42 | return new NostrAddressIdentifier( 43 | FindSpecialString(tlv), 44 | FindAuthor(tlv), 45 | FindRelays(tlv), 46 | FindKind(tlv) 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Nostr.Client/Client/NostrClientStreams.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using System.Reactive.Subjects; 3 | using Nostr.Client.Responses; 4 | 5 | namespace Nostr.Client.Client 6 | { 7 | public class NostrClientStreams 8 | { 9 | internal readonly Subject EventSubject = new(); 10 | internal readonly Subject NoticeSubject = new(); 11 | internal readonly Subject EoseSubject = new(); 12 | internal readonly Subject OkSubject = new(); 13 | 14 | internal readonly Subject UnknownMessageSubject = new(); 15 | internal readonly Subject UnknownRawSubject = new(); 16 | 17 | /// 18 | /// Requested Nostr events 19 | /// 20 | public IObservable EventStream => EventSubject.AsObservable(); 21 | 22 | /// 23 | /// Human-readable messages 24 | /// 25 | public IObservable NoticeStream => NoticeSubject.AsObservable(); 26 | 27 | /// 28 | /// Information that all stored events have been sent out 29 | /// 30 | public IObservable EoseStream => EoseSubject.AsObservable(); 31 | 32 | /// 33 | /// Information if the sent event was accepted or rejected 34 | /// 35 | public IObservable OkStream => OkSubject.AsObservable(); 36 | 37 | /// 38 | /// Unknown and unhandled messages 39 | /// 40 | public IObservable UnknownMessageStream => UnknownMessageSubject.AsObservable(); 41 | 42 | /// 43 | /// Unknown messages that are not even in the parseable format 44 | /// 45 | public IObservable UnknownRawStream => UnknownRawSubject.AsObservable(); 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Nostr.Client/Json/NostrEventConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Nostr.Client.Messages; 4 | using Nostr.Client.Messages.Contacts; 5 | using Nostr.Client.Messages.Direct; 6 | using Nostr.Client.Messages.Metadata; 7 | using Nostr.Client.Messages.Zaps; 8 | 9 | namespace Nostr.Client.Json 10 | { 11 | internal class NostrEventConverter : JsonConverter 12 | { 13 | public override bool CanConvert(Type objectType) 14 | { 15 | return objectType == typeof(NostrEvent); 16 | } 17 | 18 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 19 | { 20 | NostrSerializer.Serializer.Serialize(writer, value); 21 | } 22 | 23 | public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 24 | { 25 | if (reader.TokenType == JsonToken.Null) 26 | { 27 | return null; 28 | } 29 | 30 | var jObject = JObject.Load(reader); 31 | 32 | var target = RecognizeEvent(jObject); 33 | return NostrSerializer.Serializer.Deserialize(jObject.CreateReader(), target); 34 | } 35 | 36 | private static Type RecognizeEvent(JObject jObject) 37 | { 38 | try 39 | { 40 | var kind = jObject["kind"]?.ToObject(); 41 | return kind switch 42 | { 43 | NostrKind.Metadata => typeof(NostrMetadataEvent), 44 | NostrKind.Contacts => typeof(NostrContactEvent), 45 | NostrKind.EncryptedDm => typeof(NostrEncryptedEvent), 46 | NostrKind.Zap => typeof(NostrZapReceiptEvent), 47 | _ => typeof(NostrEvent) 48 | }; 49 | } 50 | catch (Exception) 51 | { 52 | // default 53 | return typeof(NostrEvent); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Nostr.Client/Keys/NostrKeyPair.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Keys 2 | { 3 | /// 4 | /// Holds private and public key pair 5 | /// 6 | public class NostrKeyPair : IEquatable 7 | { 8 | public NostrKeyPair(NostrPrivateKey privateKey, NostrPublicKey publicKey) 9 | { 10 | PrivateKey = privateKey; 11 | PublicKey = publicKey; 12 | } 13 | 14 | public NostrKeyPair(NostrPrivateKey privateKey) 15 | { 16 | PrivateKey = privateKey; 17 | PublicKey = NostrPublicKey.FromPrivateEc(PrivateKey.Ec); 18 | } 19 | 20 | public NostrPrivateKey PrivateKey { get; } 21 | 22 | public NostrPublicKey PublicKey { get; } 23 | 24 | public bool Equals(NostrKeyPair? other) 25 | { 26 | if (ReferenceEquals(null, other)) return false; 27 | if (ReferenceEquals(this, other)) return true; 28 | return PrivateKey.Equals(other.PrivateKey) && PublicKey.Equals(other.PublicKey); 29 | } 30 | 31 | public override bool Equals(object? obj) 32 | { 33 | if (ReferenceEquals(null, obj)) return false; 34 | if (ReferenceEquals(this, obj)) return true; 35 | if (obj.GetType() != this.GetType()) return false; 36 | return Equals((NostrKeyPair)obj); 37 | } 38 | 39 | public override int GetHashCode() 40 | { 41 | return HashCode.Combine(PrivateKey, PublicKey); 42 | } 43 | 44 | /// 45 | /// Create key pair based on private key 46 | /// 47 | public static NostrKeyPair From(NostrPrivateKey privateKey) 48 | { 49 | return new NostrKeyPair(privateKey); 50 | } 51 | 52 | /// 53 | /// Generate a new random key pair 54 | /// 55 | public static NostrKeyPair GenerateNew() 56 | { 57 | var privateKey = NostrPrivateKey.GenerateNew(); 58 | return new NostrKeyPair(privateKey); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/EventStorage.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Messages; 2 | using Nostr.Client.Responses; 3 | 4 | namespace NostrDebug.Web.Events 5 | { 6 | public class EventStorage 7 | { 8 | private readonly Dictionary _responses = new(); 9 | private readonly Dictionary _events = new(); 10 | private readonly Dictionary> _eventToCommunicators = new(); 11 | 12 | public void Store(NostrEventResponse? response) 13 | { 14 | var eventId = response?.Event?.Id; 15 | if (eventId == null) 16 | return; 17 | _responses[eventId] = response!; 18 | 19 | if (!_eventToCommunicators.ContainsKey(eventId)) 20 | _eventToCommunicators[eventId] = new HashSet(); 21 | _eventToCommunicators[eventId].Add(response!.CommunicatorName); 22 | } 23 | 24 | public void Store(NostrEvent? ev) 25 | { 26 | var eventId = ev?.Id; 27 | if (eventId == null) 28 | return; 29 | var clone = ev!.DeepClone(); 30 | _events[eventId] = clone; 31 | } 32 | 33 | public NostrEventResponse? FindResponse(string eventId) 34 | { 35 | return _events.ContainsKey(eventId) ? 36 | _responses[eventId] : 37 | null; 38 | } 39 | 40 | public NostrEvent? FindEvent(string eventId) 41 | { 42 | return _events.TryGetValue(eventId, out var @event) ? 43 | @event : 44 | null; 45 | } 46 | 47 | public string[] FindCommunicators(string? eventId) 48 | { 49 | var key = eventId ?? string.Empty; 50 | return _eventToCommunicators.TryGetValue(key, out var communicators) ? 51 | communicators.ToArray() : 52 | Array.Empty(); 53 | } 54 | 55 | public void Clear() 56 | { 57 | _events.Clear(); 58 | _responses.Clear(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Nostr.Client/Utils/NostrEncryption.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using Nostr.Client.Keys; 3 | 4 | namespace Nostr.Client.Utils 5 | { 6 | /// 7 | /// Nostr encryption utilities 8 | /// 9 | public static class NostrEncryption 10 | { 11 | public static EncryptedData Encrypt(byte[] plainText, byte[] key) 12 | { 13 | using Aes aes = Aes.Create(); 14 | aes.Key = key; 15 | 16 | var encrypted = aes.EncryptCbc(plainText, aes.IV); 17 | return new EncryptedData(encrypted, aes.IV); 18 | } 19 | 20 | public static EncryptedBase64Data EncryptBase64(byte[] plainText, byte[] key) 21 | { 22 | var encrypted = Encrypt(plainText, key); 23 | return new EncryptedBase64Data( 24 | Convert.ToBase64String(encrypted.Text), 25 | Convert.ToBase64String(encrypted.Iv) 26 | ); 27 | } 28 | 29 | public static EncryptedBase64Data EncryptBase64(byte[] plainText, NostrPublicKey key) 30 | { 31 | return EncryptBase64(plainText, key.Ec.ToBytes().ToArray()); 32 | } 33 | 34 | public static byte[] Decrypt(EncryptedData encrypted, byte[] key) 35 | { 36 | using Aes aes = Aes.Create(); 37 | aes.Key = key; 38 | 39 | return aes.DecryptCbc(encrypted.Text, encrypted.Iv); 40 | } 41 | 42 | public static byte[] DecryptBase64(EncryptedBase64Data encrypted, byte[] key) 43 | { 44 | var textBytes = Convert.FromBase64String(encrypted.Text); 45 | var ivBytes = Convert.FromBase64String(encrypted.Iv); 46 | 47 | var decrypted = Decrypt(new EncryptedData(textBytes, ivBytes), key); 48 | return decrypted; 49 | } 50 | 51 | public static byte[] DecryptBase64(EncryptedBase64Data encrypted, NostrPublicKey key) 52 | { 53 | return DecryptBase64(encrypted, key.Ec.ToBytes().ToArray()); 54 | } 55 | } 56 | 57 | public record EncryptedData(byte[] Text, byte[] Iv); 58 | 59 | public record EncryptedBase64Data(string Text, string Iv); 60 | } 61 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/NostrEventTag.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostr.Client.Json; 3 | using System.Diagnostics; 4 | 5 | namespace Nostr.Client.Messages 6 | { 7 | [DebuggerDisplay("Tag {TagIdentifier} additional: {AdditionalData.Length}")] 8 | [JsonConverter(typeof(ArrayConverter))] 9 | public class NostrEventTag : IHaveAdditionalStringData 10 | { 11 | public const string EventIdentifier = "e"; 12 | public const string ProfileIdentifier = "p"; 13 | public const string Identifier = "d"; 14 | public const string CoordinatesIdentifier = "a"; 15 | 16 | public NostrEventTag() 17 | { 18 | } 19 | 20 | public NostrEventTag(string? identifier, params string[] data) 21 | { 22 | TagIdentifier = identifier ?? string.Empty; 23 | AdditionalData = data; 24 | } 25 | 26 | [ArrayProperty(0)] 27 | public string TagIdentifier { get; init; } = string.Empty; 28 | 29 | /// 30 | /// Additional unexpected data at higher indexes in the tags array 31 | /// 32 | public string[] AdditionalData { get; private set; } = Array.Empty(); 33 | 34 | /// 35 | /// Set additional data, should not be used outside of this library. 36 | /// Hidden behind explicit interface implementation to avoid accidental usage. 37 | /// 38 | void IHaveAdditionalStringData.SetAdditionalData(string[] data) 39 | { 40 | AdditionalData = data; 41 | } 42 | 43 | public NostrEventTag DeepClone() 44 | { 45 | return new NostrEventTag 46 | { 47 | TagIdentifier = TagIdentifier, 48 | AdditionalData = (string[])AdditionalData.Clone() 49 | }; 50 | } 51 | 52 | public static NostrEventTag Event(string eventId) 53 | { 54 | return new NostrEventTag(EventIdentifier, eventId); 55 | } 56 | 57 | public static NostrEventTag Profile(string pubkey) 58 | { 59 | return new NostrEventTag(ProfileIdentifier, pubkey); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Nostr.Client/Nostr.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net8.0 5 | enable 6 | enable 7 | True 8 | 9 | Nostr.Client 10 | 11 | Mariusz Kotas 12 | Client for Nostr protocol 13 | false 14 | Enhancements 15 | Copyright 2025 Mariusz Kotas. All rights reserved. 16 | Nostr relay websockets websocket client bitcoin 17 | Apache-2.0 18 | https://github.com/Marfusios/nostr-client 19 | icon.png 20 | https://github.com/Marfusios/nostr-client 21 | README.md 22 | Git 23 | true 24 | true 25 | $(NoWarn);1591 26 | 27 | true 28 | snupkg 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | PreserveNewest 43 | 44 | 45 | PreserveNewest 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Storage/Migrations/20230401213654_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace NostrBot.Web.Storage.Migrations 7 | { 8 | /// 9 | public partial class Initial : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "ProcessedEvents", 16 | columns: table => new 17 | { 18 | ProcessedEventId = table.Column(type: "INTEGER", nullable: false) 19 | .Annotation("Sqlite:Autoincrement", true), 20 | Created = table.Column(type: "TEXT", nullable: false), 21 | Subscription = table.Column(type: "TEXT", nullable: true), 22 | Relay = table.Column(type: "TEXT", nullable: false), 23 | NostrEventId = table.Column(type: "TEXT", nullable: true), 24 | NostrEventContent = table.Column(type: "TEXT", nullable: true), 25 | NostrEventPubkey = table.Column(type: "TEXT", nullable: true), 26 | NostrEventKind = table.Column(type: "INTEGER", nullable: false), 27 | NostrEventCreatedAt = table.Column(type: "TEXT", nullable: true), 28 | NostrEventTagP = table.Column(type: "TEXT", nullable: true), 29 | NostrEventTagE = table.Column(type: "TEXT", nullable: true), 30 | ReplyContextId = table.Column(type: "TEXT", nullable: false), 31 | GeneratedReply = table.Column(type: "TEXT", nullable: true) 32 | }, 33 | constraints: table => 34 | { 35 | table.PrimaryKey("PK_ProcessedEvents", x => x.ProcessedEventId); 36 | }); 37 | } 38 | 39 | /// 40 | protected override void Down(MigrationBuilder migrationBuilder) 41 | { 42 | migrationBuilder.DropTable( 43 | name: "ProcessedEvents"); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navigation { 2 | background-color: var(--fill-color); 3 | color: var(--neutral-foreground-rest); 4 | z-index: 0; 5 | width: 160px; 6 | } 7 | 8 | .navigation-inner { 9 | padding-left: calc(var(--design-unit) * 3px); 10 | position: fixed; 11 | top: 70px; 12 | left: 0; 13 | z-index: 1; 14 | } 15 | 16 | .navigation-inner > ul { 17 | list-style: none; 18 | padding: 0; 19 | } 20 | 21 | .navigation-inner > ul > li { 22 | margin-bottom: 10px; 23 | } 24 | 25 | .navigation-inner h1 { 26 | font-size: var(--type-ramp-plus-1-font-size); 27 | line-height: var(--type-ramp-plus-1-line-height); 28 | padding: 15px calc((10 + (var(--design-unit) * 2 * var(--density))) * 1px); 29 | margin: 0; 30 | } 31 | 32 | .navigation-inner fluent-anchor { 33 | width: 100%; 34 | } 35 | 36 | .navigation-inner fluent-anchor::part(control) { 37 | justify-content: start; 38 | } 39 | 40 | .nav-item { 41 | font-size: 0.9rem; 42 | padding-bottom: 0.5rem; 43 | } 44 | 45 | .nav-item:first-of-type { 46 | padding-top: 1rem; 47 | } 48 | 49 | .nav-item:last-of-type { 50 | padding-bottom: 1rem; 51 | } 52 | 53 | .nav-item ::deep a { 54 | color: #d7d7d7; 55 | border-radius: 4px; 56 | height: 3rem; 57 | display: flex; 58 | align-items: center; 59 | line-height: 3rem; 60 | } 61 | 62 | .nav-item ::deep a.active { 63 | background-color: rgba(255,255,255,0.25); 64 | color: white; 65 | } 66 | 67 | .nav-item ::deep a:hover { 68 | background-color: rgba(255,255,255,0.1); 69 | color: white; 70 | } 71 | 72 | @media (max-width: 1024px) { 73 | .navbar-toggler { 74 | display: none; 75 | } 76 | 77 | .navigation { 78 | width: 80px; 79 | } 80 | 81 | .nav-item-text { 82 | display: none; 83 | } 84 | 85 | .navigation-inner > ul > li:hover .nav-item-text { 86 | display: inherit; 87 | } 88 | 89 | .collapse { 90 | /* Never collapse the sidebar for wide screens */ 91 | display: block; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Nostr.Client/Utils/HexExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Nostr.Client.Utils 4 | { 5 | public static class HexExtensions 6 | { 7 | public static byte[] ToByteArray(string hex) 8 | { 9 | if (hex.Length % 2 == 1) 10 | throw new Exception("The binary key cannot have an odd number of digits"); 11 | 12 | var arr = new byte[hex.Length >> 1]; 13 | 14 | for (var i = 0; i < hex.Length >> 1; ++i) 15 | { 16 | arr[i] = (byte)((GetHexValue(hex[i << 1]) << 4) + (GetHexValue(hex[(i << 1) + 1]))); 17 | } 18 | 19 | return arr; 20 | } 21 | 22 | public static bool IsHex(string hex) 23 | { 24 | if (hex.Length % 2 == 1) 25 | return false; 26 | foreach(var c in hex.ToArray()) 27 | { 28 | var isHex = (c >= '0' && c <= '9') || 29 | (c >= 'a' && c <= 'f') || 30 | (c >= 'A' && c <= 'F'); 31 | 32 | if(!isHex) 33 | return false; 34 | } 35 | return true; 36 | } 37 | 38 | public static int GetHexValue(char hex) 39 | { 40 | var val = (int)hex; 41 | return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); 42 | } 43 | 44 | public static string ToHex(this byte[] bytes) 45 | { 46 | if (bytes is null) 47 | { 48 | throw new ArgumentNullException(nameof(bytes)); 49 | } 50 | 51 | var builder = new StringBuilder(); 52 | foreach (var t in bytes) 53 | { 54 | builder.Append(t.ToHex()); 55 | } 56 | 57 | return builder.ToString(); 58 | } 59 | 60 | public static string ToHex(this Span bytes) 61 | { 62 | var builder = new StringBuilder(); 63 | foreach (var t in bytes) 64 | { 65 | builder.Append(t.ToHex()); 66 | } 67 | 68 | return builder.ToString(); 69 | } 70 | 71 | private static string ToHex(this byte b) 72 | { 73 | return b.ToString("x2"); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/Nostr.Client.Tests/NostrKeyTests.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Keys; 2 | using Nostr.Client.Utils; 3 | 4 | namespace Nostr.Client.Tests 5 | { 6 | public class NostrKeyTests 7 | { 8 | [Theory] 9 | [InlineData("0cce8c841774e499b15babdd50b4c3f9c0fc828eefe1508fc4910ed5f6bea241", "89fa4b8bce7d7ba022dec50e8f7cfae055514010dc6226bb30dc8f7d17ea73fd")] 10 | [InlineData("34a3e00bcd58050ae4ee8227389240266948dfb7d88239f1e89adc15427dfa05", "7d89d8818771bd6b3e10b868f52f9ddc0014fa0207d51729cf3acd2c7944663b")] 11 | public void Construct_FromPrivateKeyHex_ShouldBeCorrect(string privateKey, string expectedPublicKey) 12 | { 13 | var pair = NostrKeyPair.From(NostrPrivateKey.FromHex(privateKey)); 14 | 15 | Assert.Equal(expectedPublicKey, pair.PublicKey.Hex); 16 | Assert.Equal(NostrConverter.ToNpub(pair.PublicKey.Hex), pair.PublicKey.Bech32); 17 | Assert.Equal(NostrConverter.ToNsec(privateKey), pair.PrivateKey.Bech32); 18 | } 19 | 20 | [Theory] 21 | [InlineData("nsec1xj37qz7dtqzs4e8wsgnn3yjqye553hahmzprnu0gntwp2snalgzsnv42g5", "npub10kya3qv8wx7kk0sshp502tuamsqpf7szql23w2w08txjc72yvcasg0gpn5")] 22 | [InlineData("nsec1k0u6cj3c3eyaey7vphy7nrq2eudfdns8qrdkf0j665xagxhf83rs5gkn58", "npub15zwr0rspve52gnj2lhhw3s74nud9yz6qsgsfds3hxmuv52v5ljxsulkqmy")] 23 | public void Construct_FromPrivateKeyBech32_ShouldBeCorrect(string privateKey, string expectedPublicKey) 24 | { 25 | var pair = NostrKeyPair.From(NostrPrivateKey.FromBech32(privateKey)); 26 | 27 | Assert.Equal(expectedPublicKey, pair.PublicKey.Bech32); 28 | Assert.Equal(NostrConverter.ToHex(pair.PublicKey.Bech32, out _), pair.PublicKey.Hex); 29 | Assert.Equal(NostrConverter.ToHex(pair.PrivateKey.Bech32, out _), pair.PrivateKey.Hex); 30 | } 31 | 32 | [Fact] 33 | public void GenerateNew_ShouldWorkCorrectly() 34 | { 35 | var random = NostrKeyPair.GenerateNew(); 36 | var publicKeyEc = random.PrivateKey.Ec.CreateXOnlyPubKey(); 37 | var publicKey = NostrPublicKey.FromEc(publicKeyEc); 38 | 39 | Assert.NotNull(random.PrivateKey); 40 | Assert.NotNull(random.PublicKey); 41 | Assert.Equal(publicKey, random.PublicKey); 42 | } 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/Mutable/NostrEventMutable.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Messages.Mutable 2 | { 3 | public class NostrEventMutable 4 | { 5 | /// 6 | /// 32-bytes lowercase hex-encoded sha256 of the the serialized event data 7 | /// 8 | public string? Id { get; set; } 9 | 10 | /// 11 | /// 32-bytes lowercase hex-encoded public key of the event creator 12 | /// 13 | public string? Pubkey { get; set; } 14 | 15 | public DateTime? CreatedAt { get; set; } 16 | 17 | public NostrKind Kind { get; set; } 18 | 19 | public NostrEventTagsMutable? Tags { get; set; } = new(); 20 | 21 | /// 22 | /// Arbitrary string 23 | /// 24 | public string? Content { get; set; } 25 | 26 | /// 27 | /// 64-bytes hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field 28 | /// 29 | public string? Sig { get; set; } 30 | 31 | /// 32 | /// Additional unparsed data 33 | /// 34 | public Dictionary AdditionalData { get; set; } = new(); 35 | 36 | public NostrEvent ToEvent() 37 | { 38 | return new NostrEvent 39 | { 40 | Id = Id, 41 | Pubkey = Pubkey, 42 | CreatedAt = CreatedAt, 43 | Kind = Kind, 44 | Tags = Tags == null ? null : new NostrEventTags(Tags.ToArray()), 45 | AdditionalData = AdditionalData, 46 | Content = Content, 47 | Sig = Sig 48 | }; 49 | } 50 | 51 | public static NostrEventMutable FromEvent(NostrEvent ev) 52 | { 53 | return new NostrEventMutable 54 | { 55 | Id = ev.Id, 56 | Pubkey = ev.Pubkey, 57 | CreatedAt = ev.CreatedAt, 58 | Kind = ev.Kind, 59 | Tags = ev.Tags != null ? new NostrEventTagsMutable(ev.Tags) : null, 60 | AdditionalData = ev.AdditionalData as Dictionary ?? 61 | ev.AdditionalData.ToDictionary(x => x.Key, x => x.Value), 62 | Content = ev.Content, 63 | Sig = ev.Sig 64 | }; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/Nostr.Client.Tests/NostrConverterTests.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Utils; 2 | 3 | namespace Nostr.Client.Tests; 4 | 5 | public class NostrConverterTests 6 | { 7 | [Theory] 8 | [InlineData("npub1dd668dyr9un9nzf9fjjkpdcqmge584c86gceu7j97nsp4lj2pscs0xk075", "npub", "6b75a3b4832f265989254ca560b700da3343d707d2319e7a45f4e01afe4a0c31")] 9 | [InlineData("npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49", "npub", "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed")] 10 | [InlineData("nsec169ajg0pst4246c50pu353lluc2klwyfsw8e69xegmyama23rlf8shjvswd", "nsec", "d17b243c305d555d628f0f2348fffcc2adf7113071f3a29b28d93bbeaa23fa4f")] 11 | public void ToHex_ShouldConvertCorrectly(string bech32, string expectedHrp, string expectedHex) 12 | { 13 | var converted = NostrConverter.ToHex(bech32, out var hrp); 14 | Assert.Equal(expectedHex, converted); 15 | Assert.Equal(expectedHrp, hrp); 16 | } 17 | 18 | [Theory] 19 | [InlineData("6b75a3b4832f265989254ca560b700da3343d707d2319e7a45f4e01afe4a0c31", "npub1dd668dyr9un9nzf9fjjkpdcqmge584c86gceu7j97nsp4lj2pscs0xk075")] 20 | [InlineData("63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", "npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49")] 21 | public void ToNpub_ShouldConvertCorrectly(string hex, string expected) 22 | { 23 | var converted = NostrConverter.ToNpub(hex); 24 | Assert.Equal(expected, converted); 25 | } 26 | 27 | [Theory] 28 | [InlineData("d17b243c305d555d628f0f2348fffcc2adf7113071f3a29b28d93bbeaa23fa4f", "nsec169ajg0pst4246c50pu353lluc2klwyfsw8e69xegmyama23rlf8shjvswd")] 29 | public void ToNsec_ShouldConvertCorrectly(string hex, string expected) 30 | { 31 | var converted = NostrConverter.ToNsec(hex); 32 | Assert.Equal(expected, converted); 33 | } 34 | 35 | [Theory] 36 | [InlineData("6b75a3b4832f265989254ca560b700da3343d707d2319e7a45f4e01afe4a0c31", true)] 37 | [InlineData("63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", true)] 38 | [InlineData("aa", true)] 39 | [InlineData("AA", true)] 40 | [InlineData("a", false)] 41 | [InlineData("A", false)] 42 | [InlineData(null, false)] 43 | [InlineData("", false)] 44 | [InlineData("root", false)] 45 | [InlineData("reply", false)] 46 | public void IsHex_ShouldReturnCorrectValue(string? hex, bool isValid) 47 | { 48 | var isHex = NostrConverter.IsHex(hex); 49 | Assert.Equal(isValid, isHex); 50 | } 51 | } -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/NostrBot.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 2ae06180-22d7-4d1f-bc12-d782e7b51ecb 8 | Linux 9 | ..\..\.. 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | PreserveNewest 40 | true 41 | PreserveNewest 42 | 43 | 44 | PreserveNewest 45 | true 46 | PreserveNewest 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] 3 | indent_style = tab 4 | indent_size = tab 5 | tab_width = 4 6 | 7 | [*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,master,paml,razor,skin,vb,xaml,xamlx,xoml}] 8 | indent_style = space 9 | indent_size = 4 10 | tab_width = 4 11 | 12 | [*] 13 | 14 | # Microsoft .NET properties 15 | csharp_new_line_before_members_in_object_initializers = false 16 | csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion 17 | csharp_style_prefer_utf8_string_literals = true:suggestion 18 | csharp_style_var_elsewhere = true:suggestion 19 | csharp_style_var_for_built_in_types = true:suggestion 20 | csharp_style_var_when_type_is_apparent = true:suggestion 21 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion 22 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion 23 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion 24 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 25 | dotnet_style_predefined_type_for_member_access = true:suggestion 26 | dotnet_style_qualification_for_event = false:suggestion 27 | dotnet_style_qualification_for_field = false:suggestion 28 | dotnet_style_qualification_for_method = false:suggestion 29 | dotnet_style_qualification_for_property = false:suggestion 30 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 31 | 32 | # ReSharper properties 33 | resharper_show_autodetect_configure_formatting_tip = false 34 | 35 | # ReSharper inspection severities 36 | resharper_arrange_missing_parentheses_highlighting = hint 37 | resharper_arrange_redundant_parentheses_highlighting = hint 38 | resharper_arrange_this_qualifier_highlighting = hint 39 | resharper_arrange_type_member_modifiers_highlighting = hint 40 | resharper_arrange_type_modifiers_highlighting = hint 41 | resharper_built_in_type_reference_style_for_member_access_highlighting = hint 42 | resharper_built_in_type_reference_style_highlighting = hint 43 | resharper_inconsistent_naming_highlighting = error 44 | resharper_redundant_base_qualifier_highlighting = warning 45 | resharper_suggest_var_or_type_built_in_types_highlighting = hint 46 | resharper_suggest_var_or_type_elsewhere_highlighting = hint 47 | resharper_suggest_var_or_type_simple_types_highlighting = hint 48 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Storage/BotStorage.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Nostr.Client.Messages; 3 | using Nostr.Client.Responses; 4 | 5 | namespace NostrBot.Web.Storage 6 | { 7 | public class BotStorage 8 | { 9 | private readonly IDbContextFactory _contextFactory; 10 | 11 | public BotStorage(IDbContextFactory contextFactory) 12 | { 13 | _contextFactory = contextFactory; 14 | } 15 | 16 | public async Task IsProcessed(NostrEvent ev) 17 | { 18 | await using var context = await _contextFactory.CreateDbContextAsync(); 19 | 20 | return await context.ProcessedEvents 21 | .AsNoTracking() 22 | .AnyAsync(x => x.NostrEventId == ev.Id); 23 | } 24 | 25 | public async Task Store(string contextId, NostrEventResponse response, NostrEvent ev, string? generatedReply, string? eventContent, string? secondaryContextId) 26 | { 27 | await using var context = await _contextFactory.CreateDbContextAsync(); 28 | 29 | var processedEvent = new ProcessedEvent 30 | { 31 | Created = DateTime.UtcNow, 32 | Subscription = response.Subscription, 33 | Relay = response.CommunicatorName, 34 | NostrEventId = ev.Id, 35 | NostrEventContent = eventContent, 36 | NostrEventPubkey = ev.Pubkey, 37 | NostrEventCreatedAt = ev.CreatedAt, 38 | NostrEventKind = ev.Kind, 39 | NostrEventTagP = ev.Tags?.FindFirstTagValue(NostrEventTag.ProfileIdentifier), 40 | NostrEventTagE = ev.Tags?.FindFirstTagValue(NostrEventTag.EventIdentifier), 41 | ReplyContextId = contextId, 42 | ReplySecondaryContextId = secondaryContextId, 43 | GeneratedReply = generatedReply 44 | }; 45 | 46 | context.ProcessedEvents.Add(processedEvent); 47 | await context.SaveChangesAsync(); 48 | } 49 | 50 | public async Task GetHistoryForContext(string contextId, string? secondaryContextId) 51 | { 52 | await using var context = await _contextFactory.CreateDbContextAsync(); 53 | return await context.ProcessedEvents 54 | .AsNoTracking() 55 | .Where(x => x.ReplyContextId == contextId || (x.ReplySecondaryContextId != null && x.ReplySecondaryContextId == secondaryContextId)) 56 | .OrderByDescending(x => x.Created) 57 | .Take(33) 58 | .ToArrayAsync(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Storage/Migrations/20230401213654_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using NostrBot.Web.Storage; 8 | 9 | #nullable disable 10 | 11 | namespace NostrBot.Web.Storage.Migrations 12 | { 13 | [DbContext(typeof(BotContext))] 14 | [Migration("20230401213654_Initial")] 15 | partial class Initial 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "7.0.4"); 22 | 23 | modelBuilder.Entity("NostrBot.Web.Storage.ProcessedEvent", b => 24 | { 25 | b.Property("ProcessedEventId") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("Created") 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("GeneratedReply") 33 | .HasColumnType("TEXT"); 34 | 35 | b.Property("NostrEventContent") 36 | .HasColumnType("TEXT"); 37 | 38 | b.Property("NostrEventCreatedAt") 39 | .HasColumnType("TEXT"); 40 | 41 | b.Property("NostrEventId") 42 | .HasColumnType("TEXT"); 43 | 44 | b.Property("NostrEventKind") 45 | .HasColumnType("INTEGER"); 46 | 47 | b.Property("NostrEventPubkey") 48 | .HasColumnType("TEXT"); 49 | 50 | b.Property("NostrEventTagE") 51 | .HasColumnType("TEXT"); 52 | 53 | b.Property("NostrEventTagP") 54 | .HasColumnType("TEXT"); 55 | 56 | b.Property("Relay") 57 | .IsRequired() 58 | .HasColumnType("TEXT"); 59 | 60 | b.Property("ReplyContextId") 61 | .IsRequired() 62 | .HasColumnType("TEXT"); 63 | 64 | b.Property("Subscription") 65 | .HasColumnType("TEXT"); 66 | 67 | b.HasKey("ProcessedEventId"); 68 | 69 | b.ToTable("ProcessedEvents"); 70 | }); 71 | #pragma warning restore 612, 618 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Storage/Migrations/BotContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using NostrBot.Web.Storage; 7 | 8 | #nullable disable 9 | 10 | namespace NostrBot.Web.Storage.Migrations 11 | { 12 | [DbContext(typeof(BotContext))] 13 | partial class BotContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "7.0.4"); 19 | 20 | modelBuilder.Entity("NostrBot.Web.Storage.ProcessedEvent", b => 21 | { 22 | b.Property("ProcessedEventId") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("INTEGER"); 25 | 26 | b.Property("Created") 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("GeneratedReply") 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("NostrEventContent") 33 | .HasColumnType("TEXT"); 34 | 35 | b.Property("NostrEventCreatedAt") 36 | .HasColumnType("TEXT"); 37 | 38 | b.Property("NostrEventId") 39 | .HasColumnType("TEXT"); 40 | 41 | b.Property("NostrEventKind") 42 | .HasColumnType("INTEGER"); 43 | 44 | b.Property("NostrEventPubkey") 45 | .HasColumnType("TEXT"); 46 | 47 | b.Property("NostrEventTagE") 48 | .HasColumnType("TEXT"); 49 | 50 | b.Property("NostrEventTagP") 51 | .HasColumnType("TEXT"); 52 | 53 | b.Property("Relay") 54 | .IsRequired() 55 | .HasColumnType("TEXT"); 56 | 57 | b.Property("ReplyContextId") 58 | .IsRequired() 59 | .HasColumnType("TEXT"); 60 | 61 | b.Property("ReplySecondaryContextId") 62 | .HasColumnType("TEXT"); 63 | 64 | b.Property("Subscription") 65 | .HasColumnType("TEXT"); 66 | 67 | b.HasKey("ProcessedEventId"); 68 | 69 | b.ToTable("ProcessedEvents"); 70 | }); 71 | #pragma warning restore 612, 618 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/BackgroundOrchestration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Nostr.Client.Keys; 3 | using Nostr.Client.Messages; 4 | using Nostr.Client.Requests; 5 | using Nostr.Client.Responses; 6 | using NostrBot.Web.Configs; 7 | using NostrBot.Web.Logic; 8 | using Serilog; 9 | 10 | namespace NostrBot.Web 11 | { 12 | public class BackgroundOrchestration : IHostedService 13 | { 14 | private readonly NostrConfig _nostrConfig; 15 | private readonly NostrListener _listener; 16 | private readonly NostrEventsQueue _eventsQueue; 17 | private readonly BotMind _botMind; 18 | 19 | private IDisposable? _eventsSub; 20 | 21 | public BackgroundOrchestration(IOptions nostrConfig, NostrListener listener, 22 | NostrEventsQueue eventsQueue, BotMind botMind) 23 | { 24 | _listener = listener; 25 | _eventsQueue = eventsQueue; 26 | _botMind = botMind; 27 | _nostrConfig = nostrConfig.Value; 28 | } 29 | 30 | public Task StartAsync(CancellationToken cancellationToken) 31 | { 32 | Log.Information("Starting Nostr Bot"); 33 | 34 | var botPubKey = NostrPrivateKey.FromBech32(_nostrConfig.PrivateKey).DerivePublicKey(); 35 | Log.Information("Bot public key: {pubkey}", botPubKey.Bech32); 36 | 37 | _listener.RegisterFilter(BotMind.MentionSubscription, new NostrFilter 38 | { 39 | Kinds = new[] 40 | { 41 | NostrKind.ShortTextNote, 42 | NostrKind.EncryptedDm, 43 | NostrKind.LiveChatMessage 44 | }, 45 | P = new[] { botPubKey.Hex } 46 | }); 47 | 48 | if (_botMind.ListenToGlobalFeed) 49 | { 50 | _listener.RegisterFilter(BotMind.GlobalSubscription, new NostrFilter 51 | { 52 | Kinds = new[] 53 | { 54 | NostrKind.ShortTextNote, 55 | NostrKind.LiveChatMessage 56 | }, 57 | Limit = 0 58 | }); 59 | } 60 | 61 | _eventsSub = _listener.Streams.EventStream.Subscribe(OnEvent); 62 | 63 | _listener.Start(); 64 | 65 | Log.Information("Nostr Bot listening..."); 66 | return Task.CompletedTask; 67 | } 68 | 69 | public Task StopAsync(CancellationToken cancellationToken) 70 | { 71 | _listener.Stop(); 72 | _eventsQueue.Writer.TryComplete(); 73 | _eventsSub?.Dispose(); 74 | 75 | return Task.CompletedTask; 76 | } 77 | 78 | private void OnEvent(NostrEventResponse response) 79 | { 80 | _eventsQueue.Writer.TryWrite(response); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Storage/Migrations/20230402175039_SecondaryContext.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using NostrBot.Web.Storage; 8 | 9 | #nullable disable 10 | 11 | namespace NostrBot.Web.Storage.Migrations 12 | { 13 | [DbContext(typeof(BotContext))] 14 | [Migration("20230402175039_SecondaryContext")] 15 | partial class SecondaryContext 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "7.0.4"); 22 | 23 | modelBuilder.Entity("NostrBot.Web.Storage.ProcessedEvent", b => 24 | { 25 | b.Property("ProcessedEventId") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("Created") 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("GeneratedReply") 33 | .HasColumnType("TEXT"); 34 | 35 | b.Property("NostrEventContent") 36 | .HasColumnType("TEXT"); 37 | 38 | b.Property("NostrEventCreatedAt") 39 | .HasColumnType("TEXT"); 40 | 41 | b.Property("NostrEventId") 42 | .HasColumnType("TEXT"); 43 | 44 | b.Property("NostrEventKind") 45 | .HasColumnType("INTEGER"); 46 | 47 | b.Property("NostrEventPubkey") 48 | .HasColumnType("TEXT"); 49 | 50 | b.Property("NostrEventTagE") 51 | .HasColumnType("TEXT"); 52 | 53 | b.Property("NostrEventTagP") 54 | .HasColumnType("TEXT"); 55 | 56 | b.Property("Relay") 57 | .IsRequired() 58 | .HasColumnType("TEXT"); 59 | 60 | b.Property("ReplyContextId") 61 | .IsRequired() 62 | .HasColumnType("TEXT"); 63 | 64 | b.Property("ReplySecondaryContextId") 65 | .HasColumnType("TEXT"); 66 | 67 | b.Property("Subscription") 68 | .HasColumnType("TEXT"); 69 | 70 | b.HasKey("ProcessedEventId"); 71 | 72 | b.ToTable("ProcessedEvents"); 73 | }); 74 | #pragma warning restore 612, 618 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 51 | 52 | @code { 53 | private string? _target; 54 | 55 | protected override void OnInitialized() 56 | { 57 | NavigationManager.LocationChanged += LocationChanged; 58 | UpdateLocation(NavigationManager.Uri); 59 | base.OnInitialized(); 60 | } 61 | 62 | private void LocationChanged(object? sender, LocationChangedEventArgs e) 63 | { 64 | UpdateLocation(e.Location); 65 | } 66 | 67 | private void UpdateLocation(string currentUrl) 68 | { 69 | var uri = new Uri(currentUrl); 70 | _target = uri.Segments.Length > 1 ? uri.Segments[1] : ""; 71 | StateHasChanged(); 72 | } 73 | 74 | private Appearance SetAppearance(string location) => (string.Equals(location, _target, StringComparison.OrdinalIgnoreCase)) ? Appearance.Neutral : Appearance.Stealth; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Nostr.Client/Messages/NostrEventTags.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace Nostr.Client.Messages 5 | { 6 | public class NostrEventTags : IReadOnlyCollection 7 | { 8 | public static NostrEventTags Empty => new(); 9 | protected readonly Collection Collection = new(); 10 | 11 | public NostrEventTags() 12 | { 13 | } 14 | 15 | public NostrEventTags(IEnumerable tags) : this(tags.ToArray()) 16 | { 17 | } 18 | 19 | public NostrEventTags(params NostrEventTag[] tags) 20 | { 21 | foreach (var tag in tags) 22 | { 23 | Collection.Add(tag); 24 | } 25 | } 26 | 27 | public NostrEventTag[] GetEvents() 28 | { 29 | return Get(NostrEventTag.EventIdentifier); 30 | } 31 | 32 | public NostrEventTag[] GetProfiles() 33 | { 34 | return Get(NostrEventTag.ProfileIdentifier); 35 | } 36 | 37 | public NostrEventTag[] Get(string? tagIdentifier) 38 | { 39 | return this.Where(x => x.TagIdentifier == tagIdentifier).ToArray(); 40 | } 41 | 42 | public NostrEventTag? FindFirstTag(string? tagIdentifier) 43 | { 44 | var tags = Get(tagIdentifier); 45 | return tags.FirstOrDefault(); 46 | } 47 | 48 | public string? FindFirstTagValue(string? tagIdentifier) 49 | { 50 | var first = FindFirstTag(tagIdentifier); 51 | return first?.AdditionalData?.FirstOrDefault()?.ToString(); 52 | } 53 | 54 | public bool ContainsEvent(string? eventId) 55 | { 56 | return ContainsTag(NostrEventTag.EventIdentifier, eventId); 57 | } 58 | 59 | public bool ContainsProfile(string? pubkey) 60 | { 61 | return ContainsTag(NostrEventTag.ProfileIdentifier, pubkey); 62 | } 63 | 64 | public bool ContainsTag(string? tagIdentifier, string? tagValue) 65 | { 66 | var tags = Get(tagIdentifier); 67 | return tags.Any(x => x.AdditionalData.Any(y => y?.ToString() == tagValue)); 68 | } 69 | 70 | public bool ContainsTag(string? tagIdentifier) 71 | { 72 | var tags = Get(tagIdentifier); 73 | return tags.Any(); 74 | } 75 | 76 | public NostrEventTags DeepClone(params NostrEventTag[] tags) 77 | { 78 | var allTags = this.Concat(tags).ToArray(); 79 | var clone = new NostrEventTags(allTags); 80 | return clone; 81 | } 82 | 83 | public IEnumerator GetEnumerator() 84 | { 85 | return Collection.GetEnumerator(); 86 | } 87 | 88 | IEnumerator IEnumerable.GetEnumerator() 89 | { 90 | return ((IEnumerable)Collection).GetEnumerator(); 91 | } 92 | 93 | public int Count => Collection.Count; 94 | 95 | public NostrEventTag this[int index] => Collection[index]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Nostr.Client/Identifiers/NostrIdentifier.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Utils; 2 | using System.Data; 3 | using System.Text; 4 | using Nostr.Client.Messages; 5 | 6 | namespace Nostr.Client.Identifiers 7 | { 8 | /// 9 | /// Shareable identifier with extra metadata. 10 | /// When sharing a profile or an event, an app may decide to include relay information 11 | /// and other metadata such that other apps can locate and display these entities more easily. 12 | /// 13 | public abstract class NostrIdentifier 14 | { 15 | protected const byte SpecialKey = 0x00; 16 | protected const byte RelayKey = 0x01; 17 | protected const byte AuthorKey = 0x02; 18 | protected const byte KindKey = 0x03; 19 | 20 | /// 21 | /// Bech32 prefix (nprofile, nevent, nrelay, naddr, etc.) 22 | /// 23 | public abstract string Hrp { get; } 24 | 25 | /// 26 | /// Primary value, depends on the bech32 prefix (hrp) 27 | /// 28 | public abstract string Special { get; } 29 | 30 | /// 31 | /// Convert back to bech32 format 32 | /// 33 | public abstract string ToBech32(); 34 | 35 | protected static string FindSpecialHex(IReadOnlyCollection> tlv) 36 | { 37 | return tlv 38 | .First(pair => pair.Key == SpecialKey).Value 39 | .AsSpan() 40 | .ToHex(); 41 | } 42 | 43 | protected static string FindSpecialString(IReadOnlyCollection> tlv) 44 | { 45 | return HashExtensions.ToString(tlv 46 | .First(pair => pair.Key == SpecialKey).Value); 47 | } 48 | 49 | protected static string? FindAuthor(IReadOnlyCollection> tlv) 50 | { 51 | return ValueOrNull(tlv 52 | .FirstOrDefault(pair => pair.Key == AuthorKey).Value 53 | .AsSpan() 54 | .ToHex()); 55 | } 56 | 57 | protected static string[] FindRelays(IReadOnlyCollection> tlv) 58 | { 59 | return tlv 60 | .Where(pair => pair.Key == RelayKey) 61 | .Select(pair => Encoding.ASCII.GetString(pair.Value)) 62 | .ToArray(); 63 | } 64 | 65 | protected static NostrKind? FindKind(IReadOnlyCollection> tlv) 66 | { 67 | var kind = tlv 68 | .FirstOrDefault(pair => pair.Key == KindKey).Value; 69 | if (kind == null) 70 | return null; 71 | var kindInt = BitConverter.ToUInt32(kind.Reverse().ToArray()); 72 | return (NostrKind)kindInt; 73 | } 74 | 75 | protected static byte[] WriteKind(NostrKind kind) 76 | { 77 | return BitConverter.GetBytes((uint)kind).Reverse().ToArray(); 78 | } 79 | 80 | private static string? ValueOrNull(string? value) 81 | { 82 | return string.IsNullOrWhiteSpace(value) ? null : value; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Pages/Publisher.razor: -------------------------------------------------------------------------------- 1 | @page "/publisher" 2 | @using Nostr.Client.Messages.Mutable 3 | @using Newtonsoft.Json 4 | @using Nostr.Client.Json 5 | @using System.Text 6 | @inject NavigationManager Navigation 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | @code 22 | { 23 | private string? _appliedQueryBase64; 24 | private NostrEventSend? _eventSend; 25 | 26 | [Parameter] 27 | [SupplyParameterFromQuery(Name = "base64")] 28 | public string? EventBase64 { get; set; } 29 | 30 | public static string GetUrl(NostrEvent? evnt = null) 31 | { 32 | if (evnt == null) 33 | return "/publisher"; 34 | var serialized = SerializeEvent(NostrEventMutable.FromEvent(evnt)); 35 | return $"/publisher?base64={serialized}"; 36 | } 37 | 38 | protected override async Task OnAfterRenderAsync(bool firstRender) 39 | { 40 | if (!firstRender) 41 | return; 42 | 43 | if (string.IsNullOrWhiteSpace(EventBase64)) 44 | return; 45 | 46 | var deserialized = DeserializeEvent(EventBase64); 47 | if (deserialized != null && _eventSend != null) 48 | await _eventSend.ChangeEvent(deserialized.ToEvent()); 49 | } 50 | 51 | private void OnEventChanged(NostrEventMutable ev) 52 | { 53 | SerializeEventToUrl(ev, false); 54 | } 55 | 56 | private void SerializeEventToUrl(NostrEventMutable ev, bool replace) 57 | { 58 | var serialized = SerializeEvent(ev); 59 | if (serialized == _appliedQueryBase64) 60 | { 61 | // query param was changed from outside, do nothing 62 | return; 63 | } 64 | 65 | _appliedQueryBase64 = serialized; 66 | var newUrl = Navigation.GetUriWithQueryParameter("base64", _appliedQueryBase64); 67 | Navigation.NavigateTo(newUrl, false, replace); 68 | } 69 | 70 | private static string SerializeEvent(NostrEventMutable nostrEvent) 71 | { 72 | var serialized = JsonConvert.SerializeObject(nostrEvent, NostrSerializer.Settings); 73 | var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(serialized)); 74 | return base64; 75 | } 76 | 77 | private static NostrEventMutable? DeserializeEvent(string? nostrEvent) 78 | { 79 | if (nostrEvent == null) 80 | return null; 81 | 82 | try 83 | { 84 | var filterSerialized = Encoding.UTF8.GetString(Convert.FromBase64String(nostrEvent)); 85 | var deserialized = JsonConvert.DeserializeObject(filterSerialized, NostrSerializer.Settings); 86 | return deserialized; 87 | } 88 | catch (Exception e) 89 | { 90 | Console.WriteLine($"Failed to deserialize event, error: {e.Message}"); 91 | return null; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrEventSend.razor: -------------------------------------------------------------------------------- 1 | @using Nostr.Client.Requests 2 | @using Nostr.Client.Responses 3 | @using System.ComponentModel.DataAnnotations 4 | @using Nostr.Client.Messages; 5 | @using Nostr.Client.Messages.Mutable 6 | @inject RelayList Relays 7 | @inject EventStorage EventStorage; 8 | @implements IDisposable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Send 17 | 18 | 19 | 20 | @if (_isSending) 21 | { 22 | 23 | } 24 | 25 | 26 | 27 | 28 | @code { 29 | private bool _isSending; 30 | private string? _sendingEventId; 31 | 32 | private IDisposable? _websocketHandlerOkData; 33 | private IDisposable? _websocketHandlerNoticeData; 34 | 35 | private NostrEventEdit? _eventEdit; 36 | 37 | [Parameter] 38 | [Required] 39 | public NostrEventMutable Event { get; set; } = new() 40 | { 41 | Kind = NostrKind.ShortTextNote, 42 | CreatedAt = DateTime.Now 43 | }; 44 | 45 | [Parameter] 46 | public EventCallback EventChanged { get; set; } 47 | 48 | [Parameter] 49 | public EventCallback EventSent { get; set; } 50 | 51 | public void Dispose() 52 | { 53 | _websocketHandlerOkData?.Dispose(); 54 | _websocketHandlerNoticeData?.Dispose(); 55 | } 56 | 57 | public async Task ChangeEvent(NostrEvent ev) 58 | { 59 | if (_eventEdit == null) 60 | return; 61 | await _eventEdit.ChangeEvent(ev); 62 | } 63 | 64 | protected override void OnInitialized() 65 | { 66 | _websocketHandlerOkData = Relays.Client.Streams.OkStream 67 | .Subscribe(HandleOk); 68 | _websocketHandlerNoticeData = Relays.Client.Streams.NoticeStream 69 | .Subscribe(HandleNotice); 70 | 71 | base.OnInitialized(); 72 | } 73 | 74 | private void HandleOk(NostrOkResponse response) 75 | { 76 | if (response.EventId == _sendingEventId) 77 | { 78 | _isSending = false; 79 | } 80 | StateHasChanged(); 81 | } 82 | 83 | private void HandleNotice(NostrNoticeResponse response) 84 | { 85 | // notice is not bound to any specific event, stop progress 86 | _isSending = false; 87 | StateHasChanged(); 88 | } 89 | 90 | private async Task OnSend() 91 | { 92 | _isSending = true; 93 | _sendingEventId = Event.Id; 94 | var ev = Event.ToEvent(); 95 | EventStorage.Store(ev); 96 | Relays.Client.Send(new NostrEventRequest(ev)); 97 | 98 | if (EventSent.HasDelegate) 99 | { 100 | await EventSent.InvokeAsync(Event); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Signing/NostrSignatureValidator.razor: -------------------------------------------------------------------------------- 1 | @using Nostr.Client.Keys 2 | @using System.ComponentModel.DataAnnotations 3 | 4 |
5 | 6 | @if (_isSignatureValid == true) 7 | { 8 |
9 | 10 | 11 | Signature is valid 12 | 13 |
14 | } 15 | else if (_isSignatureValid == false) 16 | { 17 |
18 | 19 | 20 | Signature is invalid 21 | 22 |
23 | } 24 | 25 |
26 | 27 | @code { 28 | private bool? _isSignatureValid; 29 | private string? _key; 30 | private string? _data; 31 | private string? _signature; 32 | 33 | [Parameter] 34 | [Required] 35 | public string? PublicOrPrivateKey { get; set; } 36 | 37 | [Parameter] 38 | [Required] 39 | public string? SignedData { get; set; } 40 | 41 | [Parameter] 42 | [Required] 43 | public string? Signature { get; set; } 44 | 45 | protected override void OnParametersSet() 46 | { 47 | _key = UpdateIfModified(_key, PublicOrPrivateKey); 48 | _data = UpdateIfModified(_data, SignedData); 49 | _signature = UpdateIfModified(_signature, Signature); 50 | 51 | base.OnParametersSet(); 52 | } 53 | 54 | private string? UpdateIfModified(string? existing, string? newValue) 55 | { 56 | if (existing == newValue) 57 | return existing; 58 | 59 | ValidateSignature(); 60 | return newValue; 61 | } 62 | 63 | private void ValidateSignature() 64 | { 65 | try 66 | { 67 | if (string.IsNullOrWhiteSpace(PublicOrPrivateKey) || string.IsNullOrWhiteSpace(SignedData) || string.IsNullOrWhiteSpace(Signature)) 68 | { 69 | _isSignatureValid = null; 70 | return; 71 | } 72 | 73 | NostrConverter.TryToHex(PublicOrPrivateKey, out var hex, out var hrp); 74 | NostrPublicKey publicKey; 75 | 76 | if (hex != null && hrp == "nsec") 77 | { 78 | var privateKey = NostrPrivateKey.FromHex(hex); 79 | publicKey = privateKey.DerivePublicKey(); 80 | } 81 | else 82 | { 83 | publicKey = NostrPublicKey.FromHex(hex ?? PublicOrPrivateKey); 84 | } 85 | 86 | _isSignatureValid = publicKey.IsHexSignatureValid(Signature, SignedData); 87 | } 88 | catch (Exception e) 89 | { 90 | _isSignatureValid = false; 91 | Console.WriteLine($"Failed to validate signature, error: {e.Message}"); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/Nostr.Client.Tests/NostrZapTests.cs: -------------------------------------------------------------------------------- 1 | using BTCPayServer.Lightning; 2 | using Nostr.Client.Json; 3 | using Nostr.Client.Messages; 4 | using Nostr.Client.Messages.Zaps; 5 | 6 | namespace Nostr.Client.Tests 7 | { 8 | public class NostrZapTests 9 | { 10 | 11 | [Fact] 12 | public void ZapReceipt_ShouldDeserializeCorrectly() 13 | { 14 | var serialized = "{\r\n " + 15 | "\"id\": \"75839529323e6dc3a551fd92f4665fe81d192270c67c482c76238522965211a2\",\r\n " + 16 | "\"pubkey\": \"be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479\",\r\n " + 17 | "\"created_at\": 1681748476,\r\n " + 18 | "\"kind\": 9735,\r\n " + 19 | "\"tags\": [\r\n " + 20 | "[\r\n " + 21 | "\"p\",\r\n \"875685e12bdeaaa7a207d8d25c3fd432a8af307b80f8a5226777b50b0aa2f846\"\r\n ],\r\n " + 22 | "[\r\n " + 23 | "\"e\",\r\n \"49113ee36916684cad14ab94b0e579455e58adedfb5b8952d1b42b56384b438e\"\r\n ],\r\n " + 24 | "[\r\n " + 25 | "\"bolt11\",\r\n \"lnbc250n1pjr6u0wpp503cpw05zqcuxfkqvzep6wrjdpqfc076yw67kp8hh84f00vrvmq0shp5glhp6r5ds93skagf8feuq6u47gdur96ma43zv4k2usx5r5rjkfpscqzpgxqzfvsp5uxwycpuny2vyjx9kl3u8a6km0vjeghqg72t53mxgps5k4wuzcusq9qyyssql7rtas2wgwxehl4ds4fjx89cls0fwf5dz9759znyawrghjn50gfkqqra78wr7778am4dk7q5u069mx48hpfna3g76tj3tsel8te6nwgptrw9hr\"\r\n ],\r\n " + 26 | "[\r\n " + 27 | "\"description\",\r\n \"{\\\"pubkey\\\":\\\"5ce459cafd0d464375b872cb48826012bd1c017566c536d56440b5462591be2f\\\",\\\"content\\\":\\\"\\\",\\\"id\\\":\\\"5f68bf9a505234d6ce0fc3368f4a7dfe6b624165b7b77e9814a02643a729822b\\\",\\\"created_at\\\":1681748461,\\\"sig\\\":\\\"5754f5cba1c347a5b34924b5ca1df9562514851e9f2fe7118a46f1242dc0ede7f9fd3ec3fb52773dc13aa65d3c560214ca8be389fbd01ac40954d43e6a06d632\\\",\\\"kind\\\":9734,\\\"tags\\\":[[\\\"e\\\",\\\"49113ee36916684cad14ab94b0e579455e58adedfb5b8952d1b42b56384b438e\\\"],[\\\"p\\\",\\\"875685e12bdeaaa7a207d8d25c3fd432a8af307b80f8a5226777b50b0aa2f846\\\"],[\\\"relays\\\",\\\"wss://relay.nostriches.org/\\\",\\\"wss://nostr.wine/\\\",\\\"wss://nostr.kollider.xyz/\\\",\\\"wss://eden.nostr.land\\\",\\\"wss://no.str.cr/\\\",\\\"wss://nostr.fmt.wiz.biz/\\\",\\\"wss://nos.lol/\\\",\\\"wss://nos.lol\\\",\\\"wss://nostr.wine\\\",\\\"wss://e.nos.lol/\\\"]]}\"\r\n ]\r\n " + 28 | "],\r\n " + 29 | "\"content\": \"\",\r\n " + 30 | "\"sig\": \"faa5c804c67d2d3b00d415453a669e99a383e30bbf8f9d3ac4c1572026770124d4443544a86911e076de142df69b7a7718dbb79eee6b2bce773be6138cbac482\"\r\n" + 31 | "}"; 32 | 33 | var deserialized = NostrJson.Deserialize(serialized); 34 | 35 | Assert.NotNull(deserialized); 36 | Assert.Equal(NostrKind.Zap, deserialized.Kind); 37 | Assert.Equal("875685e12bdeaaa7a207d8d25c3fd432a8af307b80f8a5226777b50b0aa2f846", deserialized.RecipientPubkey); 38 | 39 | var invoice = deserialized.DecodeBolt11(); 40 | 41 | Assert.NotNull(invoice); 42 | Assert.Equal(25, invoice.MinimumAmount.ToUnit(LightMoneyUnit.Satoshi)); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], 4 | "MinimumLevel": { 5 | "Default": "Information", 6 | "Override": { 7 | "System": "Warning", 8 | "Microsoft": "Warning" 9 | } 10 | }, 11 | "WriteTo": [ 12 | { 13 | "Name": "Console", 14 | "Args": { 15 | "outputTemplate": "{Timestamp:HH:mm:ss.fff} [{Level:u3}] ({ThreadId}) {Message}{NewLine}", 16 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console" 17 | } 18 | }, 19 | { 20 | "Name": "File", 21 | "Args": { 22 | "path": "logs/log.txt", 23 | "rollingInterval": "Day", 24 | "outputTemplate": "{Timestamp:HH:mm:ss.ffffff zzz} [{Level:u3}] ({ThreadId}) {SourceContext} {Message}{NewLine}{Exception}" 25 | } 26 | } 27 | ], 28 | "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], 29 | "Destructure": [ 30 | { 31 | "Name": "ToMaximumDepth", 32 | "Args": { "maximumDestructuringDepth": 4 } 33 | }, 34 | { 35 | "Name": "ToMaximumStringLength", 36 | "Args": { "maximumStringLength": 500 } 37 | }, 38 | { 39 | "Name": "ToMaximumCollectionCount", 40 | "Args": { "maximumCollectionCount": 100 } 41 | } 42 | ], 43 | "Properties": { 44 | "Application": "NostrBot" 45 | } 46 | }, 47 | "ConnectionStrings": { 48 | "BotDatabase": "Data Source=nostr-bot.db" 49 | }, 50 | "Nostr": { 51 | "PrivateKey": null, 52 | "Relays": [ 53 | "wss://relay.snort.social", 54 | "wss://relay.damus.io", 55 | "wss://eden.nostr.land", 56 | "wss://nos.lol" 57 | ] 58 | }, 59 | "Bot": { 60 | "BotDescription": "Act as a funny AI bot, reply with jokes, be sarcastic, have no boundaries, joke about everything and everyone. Store all important questions and answers in your memory, remember them. Requests will be in format ': ' where user_identification identifies asking user uniquely and message is the question.", 61 | "BotWhois": "Greetings, I'm an AI comedian, sarcastic by nature, making fun of everything and everyone", 62 | "BotAdminPubKeys": [ 63 | "npub1dd668dyr9un9nzf9fjjkpdcqmge584c86gceu7j97nsp4lj2pscs0xk075", 64 | "npub15zdtx378aedvmsv0rtnz5ey53e7a7d227v9uw55s8ue9g6w5xn9qdxyumn" 65 | ], 66 | "BotIgnoreListPubKeys": [ 67 | "npub1tsgw6pncspg4d5u778hk63s3pls70evs4czfsmx0fzap9xwt203qtkhtk4", 68 | "npub1satgtcftm6420gs8mrf9c075x2527vrmsru22gn8w76skz4zlprqdezplw" 69 | ], 70 | "SlowdownReplies": false, 71 | "SlowdownPerTokenSec": 0.8, 72 | "LimitForHistoricalTokens": 2000, 73 | "ListenToGlobalFeed": false, 74 | "ReactToRootEventsInGlobalFeed": true, 75 | "ReactToThreadsInGlobalFeed": false, 76 | "ReactToThreadsInLiveChat": true, 77 | "GlobalFeedKeywords": [ 78 | "fun", 79 | "joke", 80 | "joker", 81 | "joking", 82 | "comedy", 83 | "comedian" 84 | ] 85 | }, 86 | "OpenAi": { 87 | "ApiKey": null, 88 | "Organization": null, 89 | "Model": "gpt-3.5-turbo", 90 | "Temperature": 1.21, 91 | "MaxTokens": 100, 92 | "PresencePenalty": null, 93 | "FrequencyPenalty": 1.1 94 | }, 95 | "AllowedHosts": "*" 96 | } 97 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Components/RelaySelector.razor: -------------------------------------------------------------------------------- 1 | @using System.ComponentModel.DataAnnotations 2 | @using Microsoft.AspNetCore.Components 3 | @inject RelayList Relays 4 | 5 | 6 | 7 | 8 | @if (_relay.IsConnected) 9 | { 10 | 11 | } 12 | else 13 | { 14 | 15 | } 16 | 17 | 18 | 19 | @if (_relay.IsConnecting) 20 | { 21 | 22 | Disconnect 23 | 24 | 25 | } 26 | else 27 | { 28 | 29 | Connect 30 | 31 | 32 | } 33 | 34 | 35 |
36 |
Received messages: @_relay.ReceivedMessagesCount
37 |
38 |
39 | 40 | @code { 41 | 42 | private string? _selectedRelayUrl; 43 | private RelayConnection _relay = null!; 44 | 45 | [Parameter] 46 | [Required] 47 | public RelayConnection Relay 48 | { 49 | get => _relay; 50 | set 51 | { 52 | _relay = value; 53 | _selectedRelayUrl = _relay.RelayUrl.ToString(); 54 | } 55 | } 56 | 57 | [Parameter] 58 | public EventCallback Connect { get; set; } 59 | 60 | [Parameter] 61 | public EventCallback Disconnect { get; set; } 62 | 63 | protected override void OnInitialized() 64 | { 65 | _relay.Communicator.MessageReceived.Subscribe(_ => 66 | { 67 | StateHasChanged(); 68 | }); 69 | 70 | base.OnInitialized(); 71 | } 72 | 73 | private async Task OnConnect() 74 | { 75 | _selectedRelayUrl = SanitizeUrl(_selectedRelayUrl); 76 | await _relay.Connect(_selectedRelayUrl); 77 | 78 | if (Connect.HasDelegate) 79 | { 80 | await Connect.InvokeAsync(_relay); 81 | } 82 | } 83 | 84 | private async Task OnDisconnect() 85 | { 86 | await Relay.Disconnect(); 87 | 88 | if (Disconnect.HasDelegate) 89 | { 90 | await Disconnect.InvokeAsync(_relay); 91 | } 92 | } 93 | 94 | private static string SanitizeUrl(string? relayUrl) 95 | { 96 | relayUrl = (relayUrl ?? string.Empty).Replace(" ", string.Empty).Trim().TrimEnd('/'); 97 | return relayUrl; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrKindSelect.razor: -------------------------------------------------------------------------------- 1 | @using Nostr.Client.Messages 2 | @inherits FluentInputBase 3 | 4 | 5 | Kinds 6 | 7 | 8 | @_selectedKindsText 9 | @if (_displayKindSelector) 10 | { 11 | 12 | } 13 | else 14 | { 15 | 16 | } 17 | 18 | 19 | 20 | @if (_displayKindSelector) 21 | { 22 | 32 | 33 | } 34 | 35 | 36 | @code { 37 | private FluentSelect? _selectElement; 38 | 39 | private readonly NostrKind[] _allKinds = Enum.GetValues(typeof(NostrKind)).Cast().ToArray(); 40 | private bool _displayKindSelector; 41 | 42 | private IEnumerable? _selectedKinds = new[] 43 | { 44 | NostrKind.Metadata, 45 | NostrKind.ShortTextNote 46 | }; 47 | private string _selectedKindsText = string.Empty; 48 | 49 | protected override void OnInitialized() 50 | { 51 | _selectedKinds = CurrentValue ?? Array.Empty(); 52 | UpdateSelectedText(); 53 | 54 | base.OnInitialized(); 55 | } 56 | 57 | protected override void OnParametersSet() 58 | { 59 | if (Value != _selectedKinds) 60 | { 61 | _selectedKinds = Value ?? Array.Empty(); 62 | UpdateSelectedText(); 63 | } 64 | 65 | base.OnParametersSet(); 66 | } 67 | 68 | private async Task OnDisplay() 69 | { 70 | _displayKindSelector = !_displayKindSelector; 71 | UpdateSelected(); 72 | } 73 | 74 | private void OnLostFocus() 75 | { 76 | _displayKindSelector = false; 77 | UpdateSelected(); 78 | } 79 | 80 | private void UpdateSelected() 81 | { 82 | CurrentValue = _selectedKinds? 83 | .OrderBy(x => x) 84 | .ToArray() ?? Array.Empty(); 85 | UpdateSelectedText(); 86 | } 87 | 88 | private void UpdateSelectedText() 89 | { 90 | _selectedKindsText = _selectedKinds == null ? 91 | string.Empty : 92 | string.Join(", ", _selectedKinds.Cast().OrderBy(x => x)); 93 | } 94 | 95 | protected override bool TryParseValueFromString(string? value, out NostrKind[] result, out string validationErrorMessage) 96 | { 97 | validationErrorMessage = string.Empty; 98 | result = _selectedKinds?.ToArray() ?? Array.Empty(); 99 | return true; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Nostr.Client/Keys/NostrPublicKey.cs: -------------------------------------------------------------------------------- 1 | using NBitcoin.Secp256k1; 2 | using Nostr.Client.Utils; 3 | 4 | namespace Nostr.Client.Keys 5 | { 6 | /// 7 | /// Public key structure that holds various formats 8 | /// 9 | public class NostrPublicKey : IEquatable 10 | { 11 | private NostrPublicKey(string hex, string bech32, ECXOnlyPubKey ec) 12 | { 13 | Hex = hex; 14 | Bech32 = bech32; 15 | Ec = ec; 16 | } 17 | 18 | public string Hex { get; } 19 | 20 | public string Bech32 { get; } 21 | 22 | public ECXOnlyPubKey Ec { get; } 23 | 24 | /// 25 | /// Validate signature of the given hex by the public key 26 | /// 27 | public bool IsHexSignatureValid(string? signatureHex, string? hex) 28 | { 29 | if (string.IsNullOrWhiteSpace(signatureHex)) 30 | return false; 31 | if (!SecpSchnorrSignature.TryCreate(HexExtensions.ToByteArray(signatureHex), out var schnorr)) 32 | return false; 33 | var result = Ec.SigVerifyBIP340(schnorr, HexExtensions.ToByteArray(hex ?? string.Empty)); 34 | return result; 35 | } 36 | 37 | public bool Equals(NostrPublicKey? other) 38 | { 39 | if (ReferenceEquals(null, other)) return false; 40 | if (ReferenceEquals(this, other)) return true; 41 | return string.Equals(Hex, other.Hex, StringComparison.OrdinalIgnoreCase) && 42 | string.Equals(Bech32, other.Bech32, StringComparison.OrdinalIgnoreCase); 43 | } 44 | 45 | public override bool Equals(object? obj) 46 | { 47 | if (ReferenceEquals(null, obj)) return false; 48 | if (ReferenceEquals(this, obj)) return true; 49 | if (obj.GetType() != this.GetType()) return false; 50 | return Equals((NostrPublicKey)obj); 51 | } 52 | 53 | public override int GetHashCode() 54 | { 55 | var hashCode = new HashCode(); 56 | hashCode.Add(Hex, StringComparer.OrdinalIgnoreCase); 57 | hashCode.Add(Bech32, StringComparer.OrdinalIgnoreCase); 58 | return hashCode.ToHashCode(); 59 | } 60 | 61 | public static NostrPublicKey FromHex(string hex) 62 | { 63 | var ec = ECXOnlyPubKey.Create(HexExtensions.ToByteArray(hex)); 64 | var bech32 = NostrConverter.ToNpub(hex) ?? string.Empty; 65 | return new NostrPublicKey(hex, bech32, ec); 66 | } 67 | 68 | public static NostrPublicKey FromBech32(string bech32) 69 | { 70 | var hex = NostrConverter.ToHex(bech32, out var hrp); 71 | if (!"npub".Equals(hrp, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(hex)) 72 | throw new ArgumentException("Provided bech32 key is not 'npub'", nameof(bech32)); 73 | return FromHex(hex); 74 | } 75 | 76 | public static NostrPublicKey FromEc(ECXOnlyPubKey ec) 77 | { 78 | var hex = ec.ToBytes().ToHex(); 79 | if (string.IsNullOrWhiteSpace(hex)) 80 | throw new ArgumentException("Provided ec key is not correct", nameof(ec)); 81 | return FromHex(hex); 82 | } 83 | 84 | public static NostrPublicKey FromPrivateEc(ECPrivKey ec) 85 | { 86 | var publicEc = ec.CreateXOnlyPubKey(); 87 | return FromEc(publicEc); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Nostr Debug Tool 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | 42 |
43 |
44 | 45 |
46 | An unhandled error has occurred. 47 | Reload 48 | 🗙 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /Nostr.Client.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> 3 | <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> 4 | True 5 | True 6 | True 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True 13 | True 14 | True 15 | True 16 | True 17 | True 18 | True 19 | True 20 | True 21 | True 22 | True 23 | True 24 | True 25 | True 26 | True 27 | True 28 | True 29 | True 30 | True -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrFilterEditJsonDialog.razor: -------------------------------------------------------------------------------- 1 | @using Newtonsoft.Json 2 | @using Nostr.Client.Json 3 | @using BlazorMonaco.Editor; 4 | @using Nostr.Client.Requests 5 | @using Blazored.LocalStorage 6 | @inject ILocalStorageService LocalStorage 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | Apply 15 | Discard 16 | 17 | @_error 18 | 19 |
20 | 21 |
22 |
23 | 24 | @code { 25 | private FluentDialog? _dialog; 26 | private StandaloneCodeEditor? _editor; 27 | private string? _data; 28 | private string? _error; 29 | 30 | private string _themeName = "vs"; 31 | 32 | [Parameter] 33 | public EventCallback Applied { get; set; } 34 | 35 | public async Task ShowFilter(NostrFilter? filter) 36 | { 37 | _data = Serialize(filter ?? new NostrFilter()); 38 | 39 | if (_editor != null) 40 | { 41 | await _editor.SetValue(_data); 42 | } 43 | _dialog?.Show(); 44 | } 45 | 46 | protected override async Task OnInitializedAsync() 47 | { 48 | var isDarkTheme = await LocalStorage.GetItemAsync("darkMode"); 49 | _themeName = isDarkTheme == true ? "vs-dark" : "vs"; 50 | 51 | await Global.SetTheme(_themeName); 52 | await base.OnInitializedAsync(); 53 | } 54 | 55 | protected override void OnAfterRender(bool firstRender) 56 | { 57 | if (firstRender) 58 | { 59 | _dialog!.Hide(); 60 | } 61 | } 62 | 63 | private void OnHideEvent(DialogEventArgs? args) 64 | { 65 | // do not close, avoid miss clicks 66 | } 67 | 68 | private void OnDiscard() 69 | { 70 | _error = null; 71 | _dialog?.Hide(); 72 | } 73 | 74 | private async Task OnApply() 75 | { 76 | try 77 | { 78 | var data = await _editor!.GetValue(); 79 | var deserialized = JsonConvert.DeserializeObject(data, NostrSerializer.Settings); 80 | if (deserialized == null) 81 | { 82 | _error = "Deserialized to null"; 83 | return; 84 | } 85 | 86 | _error = null; 87 | await Applied.InvokeAsync(deserialized); 88 | _dialog?.Hide(); 89 | } 90 | catch (Exception e) 91 | { 92 | _error = $"Can't parse: {e.Message}"; 93 | } 94 | } 95 | 96 | private StandaloneEditorConstructionOptions GetInitOptions(StandaloneCodeEditor editor) 97 | { 98 | return new StandaloneEditorConstructionOptions 99 | { 100 | AutomaticLayout = true, 101 | Minimap = new EditorMinimapOptions 102 | { 103 | Autohide = true 104 | }, 105 | Language = "json", 106 | Value = _data, 107 | Theme = _themeName 108 | }; 109 | } 110 | 111 | private static string Serialize(NostrFilter ev) 112 | { 113 | var settings = NostrSerializer.Settings; 114 | settings.NullValueHandling = NullValueHandling.Include; 115 | settings.Formatting = Formatting.Indented; 116 | var serialized = JsonConvert.SerializeObject(ev, settings); 117 | return serialized; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Nostr.Client/Identifiers/NostrIdentifierParser.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Utils; 2 | 3 | namespace Nostr.Client.Identifiers 4 | { 5 | public static class NostrIdentifierParser 6 | { 7 | public static NostrIdentifier? Parse(string? bech32) 8 | { 9 | var data = NostrConverter.ToHexBytes(bech32, out var hrp); 10 | if (data == null || !data.Any() || string.IsNullOrWhiteSpace(hrp)) 11 | return null; 12 | 13 | return hrp switch 14 | { 15 | "nprofile" => NostrProfileIdentifier.Parse(data), 16 | "nevent" => NostrEventIdentifier.Parse(data), 17 | "nrelay" => NostrRelayIdentifier.Parse(data), 18 | "naddr" => NostrAddressIdentifier.Parse(data), 19 | _ => throw new InvalidOperationException( 20 | $"Bech32 {hrp} identifier not yet supported, contact library maintainers") 21 | }; 22 | } 23 | 24 | public static bool TryParse(string? bech32, out NostrIdentifier? identifier) 25 | { 26 | identifier = null; 27 | try 28 | { 29 | identifier = Parse(bech32); 30 | return true; 31 | } 32 | catch (Exception) 33 | { 34 | // ignore 35 | } 36 | 37 | return false; 38 | } 39 | 40 | internal static IReadOnlyCollection> ParseTlv(byte[] tlvData) 41 | { 42 | var result = new List>(); 43 | var pos = 0; 44 | while (pos < tlvData.Length) 45 | { 46 | var tag = tlvData[pos++]; 47 | int length = tlvData[pos++]; 48 | 49 | // handle extended length encoding 50 | if ((length & 0x80) != 0) 51 | { 52 | int lengthBytes = length & 0x7F; 53 | length = 0; 54 | for (int i = 0; i < lengthBytes; i++) 55 | { 56 | length = (length << 8) + tlvData[pos++]; 57 | } 58 | } 59 | 60 | var value = new byte[length]; 61 | Array.Copy(tlvData, pos, value, 0, length); 62 | result.Add(new KeyValuePair(tag, value)); 63 | pos += length; 64 | } 65 | 66 | return result; 67 | } 68 | 69 | internal static byte[] BuildTlv((byte, byte[])[] tlvList) 70 | { 71 | var result = new List(); 72 | 73 | foreach (var item in tlvList) 74 | { 75 | var tag = item.Item1; 76 | var value = item.Item2; 77 | var length = value.Length; 78 | 79 | // handle extended length encoding 80 | var lengthBytes = length > 127 ? (byte)Math.Ceiling(length / 256.0) : (byte)0; 81 | if (lengthBytes > 0) 82 | { 83 | length = (int)Math.Pow(256, lengthBytes) + length; 84 | } 85 | 86 | result.Add(tag); 87 | if (lengthBytes > 0) 88 | { 89 | result.Add((byte)(0x80 | lengthBytes)); 90 | for (int i = lengthBytes - 1; i >= 0; i--) 91 | { 92 | result.Add((byte)(length / (int)Math.Pow(256, i))); 93 | } 94 | } 95 | else 96 | { 97 | result.Add((byte)length); 98 | } 99 | 100 | result.AddRange(value); 101 | } 102 | 103 | return result.ToArray(); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Logic/NostrListener.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Client; 2 | using Nostr.Client.Communicator; 3 | using NostrBot.Web.Configs; 4 | using Serilog; 5 | using System.Net.WebSockets; 6 | using Microsoft.Extensions.Options; 7 | using Nostr.Client.Requests; 8 | using Websocket.Client; 9 | 10 | namespace NostrBot.Web.Logic; 11 | 12 | public class NostrListener : IDisposable 13 | { 14 | private readonly NostrConfig _config; 15 | private readonly NostrMultiWebsocketClient _client; 16 | private readonly INostrCommunicator[] _communicators; 17 | 18 | private readonly Dictionary _subscriptionToFilter = new(); 19 | 20 | public NostrListener(IOptions config, NostrMultiWebsocketClient client) 21 | { 22 | _config = config.Value; 23 | _client = client; 24 | 25 | _communicators = CreateCommunicators(); 26 | foreach (var communicator in _communicators) 27 | _client.RegisterCommunicator(communicator); 28 | } 29 | 30 | public NostrClientStreams Streams => _client.Streams; 31 | 32 | public void Dispose() 33 | { 34 | _client.Dispose(); 35 | 36 | foreach (var comm in _communicators) 37 | { 38 | comm.Dispose(); 39 | } 40 | } 41 | 42 | public void RegisterFilter(string subscription, NostrFilter filter) 43 | { 44 | _subscriptionToFilter[subscription] = filter; 45 | } 46 | 47 | public void Start() 48 | { 49 | foreach (var comm in _communicators) 50 | { 51 | // fire and forget 52 | _ = comm.Start(); 53 | } 54 | } 55 | 56 | public void Stop() 57 | { 58 | foreach (var comm in _communicators) 59 | { 60 | // fire and forget 61 | _ = comm.Stop(WebSocketCloseStatus.NormalClosure, string.Empty); 62 | } 63 | } 64 | 65 | private INostrCommunicator[] CreateCommunicators() => 66 | _config.Relays 67 | .Select(x => CreateCommunicator(new Uri(x))) 68 | .ToArray(); 69 | 70 | private INostrCommunicator CreateCommunicator(Uri uri) 71 | { 72 | var comm = new NostrWebsocketCommunicator(uri, () => 73 | { 74 | var client = new ClientWebSocket(); 75 | client.Options.SetRequestHeader("Origin", "http://localhost"); 76 | return client; 77 | }); 78 | 79 | comm.Name = uri.Host; 80 | comm.ReconnectTimeout = null; //TimeSpan.FromSeconds(30); 81 | comm.ErrorReconnectTimeout = TimeSpan.FromSeconds(60); 82 | 83 | comm.ReconnectionHappened.Subscribe(info => OnCommunicatorReconnection(info, comm.Name)); 84 | comm.DisconnectionHappened.Subscribe(info => 85 | Log.Information("[{relay}] Disconnected, type: {type}, reason: {reason}", comm.Name, info.Type, info.CloseStatus)); 86 | return comm; 87 | } 88 | 89 | private void OnCommunicatorReconnection(ReconnectionInfo info, string communicatorName) 90 | { 91 | try 92 | { 93 | Log.Information("[{relay}] Reconnected, sending Nostr filters ({filterCount})", communicatorName, _subscriptionToFilter.Count); 94 | 95 | var client = _client.FindClient(communicatorName); 96 | if (client == null) 97 | { 98 | Log.Warning("[{relay}] Cannot find client", communicatorName); 99 | return; 100 | } 101 | 102 | foreach (var (sub, filter) in _subscriptionToFilter) 103 | { 104 | client.Send(new NostrRequest(sub, filter)); 105 | } 106 | } 107 | catch (Exception e) 108 | { 109 | Log.Error(e, "[{relay}] Failed to process reconnection, error: {error}", communicatorName, e.Message); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Client; 2 | using NostrBot.Web.Configs; 3 | using NostrBot.Web.Logic; 4 | using NostrBot.Web.Utils; 5 | using Serilog; 6 | using System.Threading.Channels; 7 | using Microsoft.EntityFrameworkCore; 8 | using Nostr.Client.Responses; 9 | using NostrBot.Web.Storage; 10 | using OpenAI; 11 | 12 | namespace NostrBot.Web 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | var configuration = new ConfigurationBuilder() 19 | .SetBasePath(Directory.GetCurrentDirectory()) 20 | .AddJsonFile("appsettings.json") 21 | .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true) 22 | .AddUserSecrets(true) 23 | .Build(); 24 | 25 | Log.Logger = new LoggerConfiguration() 26 | .ReadFrom.Configuration(configuration) 27 | .CreateLogger(); 28 | 29 | try 30 | { 31 | Log.Information("Configuring web application"); 32 | var builder = WebApplication.CreateBuilder(args); 33 | var services = builder.Services; 34 | 35 | builder.Host.UseSerilog(); 36 | 37 | configuration.Configure(services, "bot"); 38 | configuration.Configure(services, "nostr"); 39 | var openAiConfig = configuration.Configure(services, "openai"); 40 | 41 | services.AddControllers(); 42 | services.AddEndpointsApiExplorer(); 43 | services.AddSwaggerGen(); 44 | 45 | services.AddDbContextFactory(); 46 | services.AddDbContext(); 47 | services.AddSingleton(); 48 | services.AddSingleton(_ => new OpenAIClient(new OpenAIAuthentication(openAiConfig.ApiKey, openAiConfig.Organization))); 49 | 50 | services.AddSingleton(); 51 | services.AddSingleton(); 52 | services.AddSingleton(); 53 | services.AddSingleton(); 54 | 55 | services.AddSingleton(_ => new NostrEventsQueue( 56 | Channel.CreateUnbounded(new UnboundedChannelOptions 57 | { 58 | SingleReader = true, 59 | SingleWriter = false 60 | }))); 61 | 62 | services.AddHostedService(); 63 | services.AddHostedService(); 64 | 65 | var app = builder.Build(); 66 | 67 | app.UseSwagger(); 68 | app.UseSwaggerUI(); 69 | 70 | app.UseHttpsRedirection(); 71 | 72 | app.UseAuthorization(); 73 | 74 | app.MapControllers(); 75 | 76 | InitStorage(app); 77 | 78 | Log.Information("Starting web application"); 79 | app.Run(); 80 | Log.Information("Exiting web application"); 81 | } 82 | catch (Exception ex) 83 | { 84 | Log.Fatal(ex, "Application terminated unexpectedly, error: {error}", ex.Message); 85 | } 86 | finally 87 | { 88 | Log.CloseAndFlush(); 89 | } 90 | } 91 | 92 | private static void InitStorage(WebApplication app) 93 | { 94 | Log.Information("Initializing database storage"); 95 | 96 | using var scope = app.Services.CreateScope(); 97 | var db = scope.ServiceProvider.GetRequiredService(); 98 | db.Database.Migrate(); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Events/NostrEventEditJsonDialog.razor: -------------------------------------------------------------------------------- 1 | @using Newtonsoft.Json 2 | @using Nostr.Client.Json 3 | @using BlazorMonaco.Editor; 4 | @using Blazored.LocalStorage 5 | @inject ILocalStorageService LocalStorage 6 | 7 | 8 |
9 | 10 | 11 | 12 |
13 | Apply 14 | Discard 15 | 16 | @_error 17 | 18 |
19 | 20 |
21 |
22 | 23 | @code { 24 | private FluentDialog? _eventDialog; 25 | private StandaloneCodeEditor? _editor; 26 | private string? _data; 27 | private string? _error; 28 | 29 | private string _themeName = "vs"; 30 | 31 | [Parameter] 32 | public EventCallback Applied { get; set; } 33 | 34 | public async Task ShowEvent(NostrEvent? ev) 35 | { 36 | if (ev == null) 37 | { 38 | _data = Serialize(new NostrEvent 39 | { 40 | Kind = NostrKind.ShortTextNote, 41 | CreatedAt = DateTime.Now 42 | }); 43 | } 44 | else 45 | { 46 | _data = Serialize(ev); 47 | } 48 | 49 | if (_editor != null) 50 | { 51 | await _editor.SetValue(_data); 52 | } 53 | _eventDialog?.Show(); 54 | } 55 | 56 | protected override async Task OnInitializedAsync() 57 | { 58 | var isDarkTheme = await LocalStorage.GetItemAsync("darkMode"); 59 | _themeName = isDarkTheme == true ? "vs-dark" : "vs"; 60 | 61 | await Global.SetTheme(_themeName); 62 | await base.OnInitializedAsync(); 63 | } 64 | 65 | protected override void OnAfterRender(bool firstRender) 66 | { 67 | if (firstRender) 68 | { 69 | _eventDialog!.Hide(); 70 | } 71 | } 72 | 73 | private void OnHideEvent(DialogEventArgs? args) 74 | { 75 | // do not close, avoid miss clicks 76 | } 77 | 78 | private void OnDiscard() 79 | { 80 | _error = null; 81 | _eventDialog?.Hide(); 82 | } 83 | 84 | private async Task OnApply() 85 | { 86 | try 87 | { 88 | var data = await _editor!.GetValue(); 89 | var deserialized = JsonConvert.DeserializeObject(data, NostrSerializer.Settings); 90 | if (deserialized == null) 91 | { 92 | _error = "Deserialized to null"; 93 | return; 94 | } 95 | 96 | _error = null; 97 | await Applied.InvokeAsync(deserialized); 98 | _eventDialog?.Hide(); 99 | } 100 | catch (Exception e) 101 | { 102 | _error = $"Can't parse: {e.Message}"; 103 | } 104 | } 105 | 106 | private StandaloneEditorConstructionOptions GetInitOptions(StandaloneCodeEditor editor) 107 | { 108 | return new StandaloneEditorConstructionOptions 109 | { 110 | AutomaticLayout = true, 111 | Minimap = new EditorMinimapOptions 112 | { 113 | Autohide = true 114 | }, 115 | Language = "json", 116 | Value = _data, 117 | Theme = _themeName 118 | }; 119 | } 120 | 121 | private static string Serialize(NostrEvent ev) 122 | { 123 | var settings = NostrSerializer.Settings; 124 | settings.NullValueHandling = NullValueHandling.Include; 125 | settings.Formatting = Formatting.Indented; 126 | var serialized = JsonConvert.SerializeObject(ev, settings); 127 | return serialized; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /apps/nostr-debug/NostrDebug.Web/Relay/RelayConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Subjects; 5 | using Nostr.Client.Communicator; 6 | using Websocket.Client; 7 | 8 | namespace NostrDebug.Web.Relay 9 | { 10 | public class RelayConnection : IDisposable 11 | { 12 | private readonly ILogger _logger; 13 | 14 | private readonly NostrWebsocketCommunicator _communicator; 15 | 16 | private readonly Subject _historySubject = new(); 17 | private readonly Subject _connectionSubject = new(); 18 | 19 | public RelayConnection(ILogger logger, Uri url) 20 | { 21 | _logger = logger; 22 | 23 | _communicator = new NostrWebsocketCommunicator(url); 24 | 25 | _communicator.Name = url.Host; 26 | _communicator.ReconnectTimeout = null; //TimeSpan.FromSeconds(30); 27 | _communicator.ErrorReconnectTimeout = TimeSpan.FromSeconds(60); 28 | 29 | _communicator.ReconnectionHappened.Subscribe(OnReconnected); 30 | _communicator.DisconnectionHappened.Subscribe(OnDisconnected); 31 | _communicator.MessageReceived.Subscribe(OnMessageReceived); 32 | } 33 | 34 | public INostrCommunicator Communicator => _communicator; 35 | 36 | public bool IsConnecting => _communicator.IsStarted; 37 | public bool IsConnected => _communicator.IsRunning; 38 | public bool IsStarted => _communicator.IsStarted; 39 | 40 | public bool IsUsed { get; private set; } 41 | 42 | public Uri RelayUrl => _communicator.Url; 43 | 44 | public int ReceivedMessagesCount { get; private set; } 45 | 46 | public IObservable HistoryStream => _historySubject.AsObservable(); 47 | public IObservable ConnectionStream => _connectionSubject.AsObservable(); 48 | 49 | public void Dispose() 50 | { 51 | _communicator.Dispose(); 52 | } 53 | 54 | public async Task Connect(string? relayUrl) 55 | { 56 | await Task.CompletedTask; 57 | IsUsed = true; 58 | if (_communicator.IsRunning) 59 | { 60 | return false; 61 | } 62 | 63 | relayUrl ??= _communicator.Url.ToString(); 64 | if (!Uri.TryCreate(relayUrl, UriKind.Absolute, out var safeUrl)) 65 | { 66 | return false; 67 | } 68 | 69 | ReceivedMessagesCount = 0; 70 | _communicator.Url = safeUrl; 71 | _communicator.Name = safeUrl.Host; 72 | _ = _communicator.Start(); 73 | return true; 74 | } 75 | 76 | public async Task Disconnect() 77 | { 78 | await _communicator.Stop(WebSocketCloseStatus.NormalClosure, string.Empty); 79 | } 80 | 81 | private void OnMessageReceived(ResponseMessage message) 82 | { 83 | ReceivedMessagesCount++; 84 | } 85 | 86 | private void OnReconnected(ReconnectionInfo info) 87 | { 88 | var subMessage = info.Type switch 89 | { 90 | ReconnectionType.Initial => "Connected", 91 | _ => $"Reconnected, type: {info.Type}" 92 | }; 93 | var message = $"[{DateTime.Now:HH:mm:ss.fff} {_communicator.Name}] ✅ {subMessage}"; 94 | _logger.LogInformation(message); 95 | _historySubject.OnNext(message); 96 | _connectionSubject.OnNext(true); 97 | } 98 | 99 | private void OnDisconnected(DisconnectionInfo info) 100 | { 101 | var reason = string.IsNullOrWhiteSpace(info.CloseStatusDescription) 102 | ? string.Empty 103 | : $", reason: {info.CloseStatusDescription}"; 104 | var message = 105 | $"[{DateTime.Now:HH:mm:ss.fff} {_communicator.Name}] ❌ Disconnected, type: {info.Type}{reason}"; 106 | _logger.LogInformation(message); 107 | _historySubject.OnNext(message); 108 | _connectionSubject.OnNext(false); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | .idea/ 5 | .DS_Store 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | build/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | logs/ 28 | log/ 29 | 30 | # Visual Studio 2015 cache/options directory 31 | .vs/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | artifacts/ 49 | 50 | *_i.c 51 | *_p.c 52 | *_i.h 53 | *.ilk 54 | *.meta 55 | *.obj 56 | *.pch 57 | *.pdb 58 | *.pgc 59 | *.pgd 60 | *.rsp 61 | *.sbr 62 | *.tlb 63 | *.tli 64 | *.tlh 65 | *.tmp 66 | *.tmp_proj 67 | *.log 68 | *.vspscc 69 | *.vssscc 70 | .builds 71 | *.pidb 72 | *.svclog 73 | *.scc 74 | 75 | # Chutzpah Test files 76 | _Chutzpah* 77 | 78 | # Visual C++ cache files 79 | ipch/ 80 | *.aps 81 | *.ncb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | ## TODO: Comment the next line if you want to checkin your 142 | ## web deploy settings but do note that will include unencrypted 143 | ## passwords 144 | #*.pubxml 145 | 146 | *.publishproj 147 | 148 | # NuGet Packages 149 | *.nupkg 150 | # The packages folder can be ignored because of Package Restore 151 | **/packages/* 152 | # except build/, which is used as an MSBuild target. 153 | !**/packages/build/ 154 | # Uncomment if necessary however generally it will be regenerated when needed 155 | #!**/packages/repositories.config 156 | 157 | # Windows Azure Build Output 158 | csx/ 159 | *.build.csdef 160 | 161 | # Windows Store app package directory 162 | AppPackages/ 163 | 164 | # Visual Studio cache files 165 | # files ending in .cache can be ignored 166 | *.[Cc]ache 167 | # but keep track of directories ending in .cache 168 | !*.[Cc]ache/ 169 | 170 | # Others 171 | ClientBin/ 172 | [Ss]tyle[Cc]op.* 173 | ~$* 174 | *~ 175 | *.dbmdl 176 | *.dbproj.schemaview 177 | *.publishsettings 178 | node_modules/ 179 | orleans.codegen.cs 180 | 181 | # RIA/Silverlight projects 182 | Generated_Code/ 183 | 184 | # Backup & report files from converting an old project file 185 | # to a newer Visual Studio version. Backup files are not needed, 186 | # because we have git ;-) 187 | _UpgradeReport_Files/ 188 | Backup*/ 189 | UpgradeLog*.XML 190 | UpgradeLog*.htm 191 | 192 | # SQL Server files 193 | *.mdf 194 | *.ldf 195 | 196 | # Business Intelligence projects 197 | *.rdl.data 198 | *.bim.layout 199 | *.bim_*.settings 200 | 201 | # Microsoft Fakes 202 | FakesAssemblies/ 203 | 204 | # Node.js Tools for Visual Studio 205 | .ntvs_analysis.dat 206 | 207 | # Visual Studio 6 build log 208 | *.plg 209 | 210 | # Visual Studio 6 workspace options file 211 | *.opt 212 | 213 | # LightSwitch generated files 214 | GeneratedArtifacts/ 215 | _Pvt_Extensions/ 216 | ModelManifest.xml 217 | 218 | *.db 219 | *.db-shm 220 | *.db-wal 221 | 222 | appsettings.Production.json 223 | appsettings.Production2.json 224 | -------------------------------------------------------------------------------- /apps/nostr-bot/NostrBot.Web/Logic/BotManagement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Nostr.Client.Client; 3 | using Nostr.Client.Identifiers; 4 | using Nostr.Client.Keys; 5 | using Nostr.Client.Messages; 6 | using Nostr.Client.Requests; 7 | using Nostr.Client.Utils; 8 | using NostrBot.Web.Configs; 9 | using OpenAI; 10 | using OpenAI.Images; 11 | 12 | namespace NostrBot.Web.Logic 13 | { 14 | public class BotManagement 15 | { 16 | private const char CommandPrefix = '!'; 17 | private const string CommandReplyTo = "reply"; 18 | private const string CommandGenerateImage = "image"; 19 | 20 | private readonly BotConfig _config; 21 | private readonly OpenAiConfig _aiConfig; 22 | private readonly NostrMultiWebsocketClient _client; 23 | private readonly OpenAIClient _aiClient; 24 | 25 | public BotManagement(IOptions config, IOptions aiConfig, 26 | NostrMultiWebsocketClient client, OpenAIClient aiClient) 27 | { 28 | _config = config.Value; 29 | _aiConfig = aiConfig.Value; 30 | _client = client; 31 | _aiClient = aiClient; 32 | } 33 | 34 | public bool IsCommand(string? message) 35 | { 36 | var messageSafe = message ?? string.Empty; 37 | return messageSafe.StartsWith(CommandPrefix); 38 | } 39 | 40 | public async Task ProcessCommand(string? message, string? senderPubKey) 41 | { 42 | var messageSafe = (message ?? string.Empty).TrimStart(CommandPrefix); 43 | 44 | if (string.IsNullOrWhiteSpace(messageSafe)) 45 | return "Received message is empty, cannot continue"; 46 | 47 | if (string.IsNullOrWhiteSpace(senderPubKey)) 48 | return "Sender pubkey is not specified, cannot process command"; 49 | 50 | var senderKey = NostrPublicKey.FromHex(senderPubKey); 51 | if (!_config.BotAdminPubKeys.Contains(senderKey.Bech32) && !_config.BotAdminPubKeys.Contains(senderKey.Hex)) 52 | return "Sender is not admin, ignore command"; 53 | 54 | var targetCommand = messageSafe.ToLowerInvariant(); 55 | if (targetCommand.StartsWith(CommandReplyTo)) 56 | { 57 | return OnReply(messageSafe); 58 | } 59 | 60 | if (targetCommand.StartsWith(CommandGenerateImage)) 61 | { 62 | return await OnImage(messageSafe); 63 | } 64 | 65 | return "Unknown command"; 66 | } 67 | 68 | private string OnReply(string messageSafe) 69 | { 70 | var split = messageSafe.Split(' ', StringSplitOptions.RemoveEmptyEntries); 71 | if (split.Length < 1) 72 | return "Invalid command format"; 73 | 74 | var content = split[1]; 75 | 76 | // try to parse 'nevent1' 77 | if (NostrIdentifierParser.TryParse(content, out var identifier) && 78 | identifier is NostrEventIdentifier identifierEvent) 79 | { 80 | content = identifierEvent.EventId; 81 | } 82 | 83 | // parse 'note1' into hex 84 | if (NostrConverter.TryToHex(content, out var targetEventIdHex, out _)) 85 | content = targetEventIdHex!; 86 | 87 | var filter = new NostrFilter 88 | { 89 | Kinds = new[] 90 | { 91 | NostrKind.ShortTextNote, 92 | NostrKind.EncryptedDm 93 | }, 94 | Ids = new[] { content } 95 | }; 96 | _client.Send(new NostrRequest(content, filter)); 97 | return $"Requesting event {content}"; 98 | } 99 | 100 | private async Task OnImage(string messageSafe) 101 | { 102 | var commandLenght = CommandGenerateImage.Length; 103 | var content = messageSafe[commandLenght..].Trim(); 104 | 105 | var request = new ImageGenerationRequest( 106 | content, 107 | _aiConfig.ImageModel, 108 | _aiConfig.ImageCount, 109 | quality: _aiConfig.ImageQuality 110 | ); 111 | var response = await _aiClient.ImagesEndPoint.GenerateImageAsync(request); 112 | 113 | return string.Join(" ", response.Select(x => $"{x.RevisedPrompt} \n\n{x.Url}")); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Nostr.Client/Utils/NostrConverter.cs: -------------------------------------------------------------------------------- 1 | namespace Nostr.Client.Utils 2 | { 3 | /// 4 | /// Nostr conversion utilities 5 | /// 6 | public static class NostrConverter 7 | { 8 | /// 9 | /// Convert Bech32 string into hex byte array key 10 | /// 11 | public static byte[]? ToHexBytes(string? bech32, out string? hrp) 12 | { 13 | hrp = null; 14 | if (string.IsNullOrWhiteSpace(bech32)) 15 | return Array.Empty(); 16 | 17 | Bech32.Decode(bech32, out hrp, out var decoded); 18 | return decoded; 19 | } 20 | 21 | /// 22 | /// Convert Bech32 string into hex key 23 | /// 24 | public static string? ToHex(string? bech32, out string? hrp) 25 | { 26 | hrp = null; 27 | if (string.IsNullOrWhiteSpace(bech32)) 28 | return bech32; 29 | 30 | Bech32.Decode(bech32, out hrp, out var decoded); 31 | var hex = decoded?.ToHex(); 32 | return hex; 33 | } 34 | 35 | /// 36 | /// Try to convert Bech32 string into hex key 37 | /// 38 | public static bool TryToHex(string? bech32, out string? hex, out string? hrp) 39 | { 40 | hrp = null; 41 | hex = null; 42 | try 43 | { 44 | hex = ToHex(bech32, out hrp); 45 | return !string.IsNullOrWhiteSpace(hex); 46 | } 47 | catch (Exception) 48 | { 49 | // ignore 50 | return false; 51 | } 52 | } 53 | 54 | /// 55 | /// Check whether input is valid hex string 56 | /// 57 | /// 58 | /// 59 | public static bool IsHex(string? hexKey) 60 | { 61 | if (string.IsNullOrWhiteSpace(hexKey)) 62 | return false; 63 | 64 | try 65 | { 66 | return HexExtensions.IsHex(hexKey); 67 | } 68 | catch (Exception) 69 | { 70 | // ignore 71 | } 72 | 73 | return false; 74 | } 75 | 76 | /// 77 | /// Convert hex string to Bech32 format, you need to provide hrp (prefix) 78 | /// 79 | public static string? ToBech32(string? hexKey, string hrp) 80 | { 81 | if (string.IsNullOrWhiteSpace(hexKey)) 82 | return hexKey; 83 | 84 | var hexArray = HexExtensions.ToByteArray(hexKey); 85 | return ToBech32(hexArray, hrp); 86 | } 87 | 88 | /// 89 | /// Convert hex byte array to Bech32 format, you need to provide hrp (prefix) 90 | /// 91 | public static string? ToBech32(byte[]? hexArray, string hrp) 92 | { 93 | if (hexArray == null) 94 | return null; 95 | 96 | var npub = Bech32.Encode(hrp, hexArray); 97 | return npub; 98 | } 99 | 100 | /// 101 | /// Try to convert hex string to Bech32 format, you need to provide hrp (prefix) 102 | /// 103 | public static bool TryToBech32(string? hexKey, string hrp, out string? bech32) 104 | { 105 | bech32 = null; 106 | try 107 | { 108 | bech32 = ToBech32(hexKey, hrp); 109 | return !string.IsNullOrWhiteSpace(bech32); 110 | } 111 | catch (Exception) 112 | { 113 | // ignore 114 | return false; 115 | } 116 | } 117 | 118 | /// 119 | /// Convert hex key into Bech32 'npub1xxx' representation 120 | /// 121 | public static string? ToNpub(string? hexKey) 122 | { 123 | return ToBech32(hexKey, "npub"); 124 | } 125 | 126 | /// 127 | /// Convert hex key into Bech32 'nsec1xxx' representation 128 | /// 129 | public static string? ToNsec(string? hexKey) 130 | { 131 | return ToBech32(hexKey, "nsec"); 132 | } 133 | 134 | /// 135 | /// Convert hex key into Bech32 'note1xxx' representation 136 | /// 137 | public static string? ToNote(string? hexKey) 138 | { 139 | return ToBech32(hexKey, "note"); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/Nostr.Client.Tests/NostrIdentifierTests.cs: -------------------------------------------------------------------------------- 1 | using Nostr.Client.Identifiers; 2 | using Nostr.Client.Messages; 3 | 4 | namespace Nostr.Client.Tests 5 | { 6 | public class NostrIdentifierTests 7 | { 8 | [Fact] 9 | public void NProfile_ShouldBeParsedCorrectly() 10 | { 11 | var bech32 = 12 | "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"; 13 | var parsed = NostrIdentifierParser.Parse(bech32); 14 | var parsedProfile = parsed as NostrProfileIdentifier; 15 | 16 | Assert.NotNull(parsed); 17 | Assert.NotNull(parsedProfile); 18 | 19 | Assert.Equal("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", parsedProfile.Pubkey); 20 | Assert.Equal(2, parsedProfile.Relays.Length); 21 | Assert.Equal("wss://r.x.com", parsedProfile.Relays[0]); 22 | Assert.Equal("wss://djbas.sadkb.com", parsedProfile.Relays[1]); 23 | 24 | var serialized = parsedProfile.ToBech32(); 25 | Assert.Equal(bech32, serialized); 26 | } 27 | 28 | [Fact] 29 | public void NEvent_ShouldBeParsedCorrectly() 30 | { 31 | var bech32 = 32 | "nevent1qqswtpsw630h908nmlqzpef4e2sca3u6mz9tyxgt03pm2wax4d9ck9gpremhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet59uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcrwpl2p"; 33 | var parsed = NostrIdentifierParser.Parse(bech32); 34 | var parsedEvent = parsed as NostrEventIdentifier; 35 | 36 | Assert.NotNull(parsed); 37 | Assert.NotNull(parsedEvent); 38 | 39 | Assert.Equal("e5860ed45f72bcf3dfc020e535caa18ec79ad88ab2190b7c43b53ba6ab4b8b15", parsedEvent.EventId); 40 | Assert.Equal(2, parsedEvent.Relays.Length); 41 | Assert.Equal("wss://nostr-pub.wellorder.net/", parsedEvent.Relays[0]); 42 | Assert.Equal("wss://relay.damus.io/", parsedEvent.Relays[1]); 43 | Assert.Null(parsedEvent.Pubkey); 44 | 45 | var serialized = parsedEvent.ToBech32(); 46 | Assert.Equal(bech32, serialized); 47 | } 48 | 49 | [Fact] 50 | public void NEvent_Different_ShouldBeParsedCorrectly() 51 | { 52 | var bech32 = 53 | "nevent1qqsgu3du8y8rsxcyehx7vzgjseqk2jvwd96glkne36ulrlfuc7ezy5czypumuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesqgwwaehxw309ahx7uewd3hkctcl9s8sg"; 54 | var parsed = NostrIdentifierParser.Parse(bech32); 55 | var parsedEvent = parsed as NostrEventIdentifier; 56 | 57 | Assert.NotNull(parsed); 58 | Assert.NotNull(parsedEvent); 59 | 60 | Assert.Equal("8e45bc390e381b04cdcde60912864165498e69748fda798eb9f1fd3cc7b22253", parsedEvent.EventId); 61 | Assert.Equal("wss://nos.lol/", parsedEvent.Relays[0]); 62 | Assert.Equal("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", parsedEvent.Pubkey); 63 | 64 | var serialized = parsedEvent.ToBech32(); 65 | Assert.Equal(bech32, serialized); 66 | } 67 | 68 | [Fact] 69 | public void NRelay_ShouldBeParsedCorrectly() 70 | { 71 | var bech32 = 72 | "nrelay1qq08wumn8ghj7mn0wd68yttsw43zuam9d3kx7unyv4ezumn9wshsevr7js"; 73 | var parsed = NostrIdentifierParser.Parse(bech32); 74 | var parsedEvent = parsed as NostrRelayIdentifier; 75 | 76 | Assert.NotNull(parsed); 77 | Assert.NotNull(parsedEvent); 78 | 79 | Assert.Equal("wss://nostr-pub.wellorder.net/", parsedEvent.Relay); 80 | 81 | var serialized = parsedEvent.ToBech32(); 82 | Assert.Equal(bech32, serialized); 83 | } 84 | 85 | [Fact] 86 | public void NAddress_ShouldBeParsedCorrectly() 87 | { 88 | var bech32 = 89 | "naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu"; 90 | var parsed = NostrIdentifierParser.Parse(bech32); 91 | var parsedEvent = parsed as NostrAddressIdentifier; 92 | 93 | Assert.NotNull(parsed); 94 | Assert.NotNull(parsedEvent); 95 | 96 | Assert.Equal("ipsum", parsedEvent.Identifier); 97 | Assert.Equal("wss://relay.nostr.org", parsedEvent.Relays[0]); 98 | Assert.Equal("a695f6b60119d9521934a691347d9f78e8770b56da16bb255ee286ddf9fda919", parsedEvent.Pubkey); 99 | Assert.Equal(NostrKind.LongFormContent, parsedEvent.Kind); 100 | 101 | var serialized = parsedEvent.ToBech32(); 102 | Assert.Equal(bech32, serialized); 103 | } 104 | } 105 | } 106 | --------------------------------------------------------------------------------