├── .gitmodules ├── Nostrid.Core.Test ├── Usings.cs ├── Utils │ ├── TestUtils.cs │ └── TempDb.cs └── Nostrid.Core.Test.csproj ├── Nostrid.Photino ├── favicon.ico ├── wwwroot │ ├── favicon.ico │ ├── css │ │ └── bootstrap-icons-1.10.3 │ │ │ └── fonts │ │ │ ├── bootstrap-icons.woff │ │ │ └── bootstrap-icons.woff2 │ └── index.html ├── Misc │ ├── NotificationCounter.cs │ ├── ClipboardService.cs │ └── DbConstants.cs ├── libman.json ├── _Imports.razor ├── Nostrid.Photino.csproj └── Program.cs ├── Nostrid ├── wwwroot │ ├── favicon.ico │ ├── css │ │ └── bootstrap-icons-1.10.3 │ │ │ └── fonts │ │ │ ├── bootstrap-icons.woff │ │ │ └── bootstrap-icons.woff2 │ └── index.html ├── Resources │ ├── Fonts │ │ └── OpenSans-Regular.ttf │ ├── Raw │ │ └── AboutAssets.txt │ └── AppIcon │ │ └── appiconfg.svg ├── Platforms │ ├── iOS │ │ ├── NotificationCounter.cs │ │ ├── AppDelegate.cs │ │ ├── Program.cs │ │ └── Info.plist │ ├── Android │ │ ├── NotificationCounter.cs │ │ ├── Resources │ │ │ └── values │ │ │ │ └── colors.xml │ │ ├── MainApplication.cs │ │ ├── MainActivity.cs │ │ └── AndroidManifest.xml │ ├── MacCatalyst │ │ ├── NotificationCounter.cs │ │ ├── AppDelegate.cs │ │ ├── Entitlements.plist │ │ ├── Program.cs │ │ └── Info.plist │ ├── Windows │ │ ├── App.xaml │ │ ├── app.manifest │ │ ├── App.xaml.cs │ │ ├── NotificationCounter.cs │ │ └── Package.appxmanifest │ └── Tizen │ │ ├── Main.cs │ │ └── tizen-manifest.xml ├── MainPage.xaml.cs ├── Properties │ └── launchSettings.json ├── libman.json ├── Misc │ ├── DbConstants.cs │ ├── ClipboardService.cs │ └── NotificationCounterForMobile.cs ├── _Imports.razor ├── MainPage.xaml ├── App.xaml.cs ├── App.xaml └── MauiProgram.cs ├── Nostrid.Web ├── wwwroot │ ├── favicon.ico │ ├── icon-192.png │ ├── icon-512.png │ ├── css │ │ └── bootstrap-icons-1.10.3 │ │ │ └── fonts │ │ │ ├── bootstrap-icons.woff │ │ │ └── bootstrap-icons.woff2 │ ├── service-worker.js │ ├── manifest.json │ ├── file.js │ ├── signer.js │ ├── index.html │ └── service-worker.published.js ├── NotificationCounter.cs ├── _Imports.razor ├── libman.json ├── Services │ └── ClipboardService.cs ├── Nostrid.Web.csproj ├── AesSubtleCrypto.cs ├── Properties │ └── launchSettings.json ├── DatabaseService.cs ├── SerialQueue.cs └── Program.cs ├── Nostrid.Core ├── Interfaces │ ├── IClipboardService.cs │ └── ITreeItem.cs ├── Pages │ ├── Home.razor │ ├── Compose.razor │ ├── Long.razor │ ├── ChannelViewer.razor │ ├── HashtagFeed.razor │ ├── SavedFeed.razor │ ├── Explore.razor │ └── Accounts.razor ├── Model │ ├── NostrNip.cs │ ├── Reaction.cs │ ├── TextInput.cs │ ├── NostrKindClass.cs │ ├── IdType.cs │ ├── Mention.cs │ ├── Database │ │ ├── EventSeen.cs │ │ ├── Follow.cs │ │ ├── Channel.cs │ │ ├── Mute.cs │ │ ├── FeedSource.cs │ │ ├── DmPair.cs │ │ ├── Account.cs │ │ ├── TagData.cs │ │ ├── Config.cs │ │ ├── Relay.cs │ │ ├── ChannelDetails.cs │ │ └── AccountDetails.cs │ ├── Lud06Data.cs │ ├── ChannelWithInfo.cs │ ├── NoteTreeShowMore.cs │ ├── EventDetailsCount.cs │ ├── Nip11Response.cs │ ├── NostrKind.cs │ ├── NostrTvl.cs │ └── NoteTree.cs ├── Data │ ├── Relays │ │ ├── IDbFilter.cs │ │ ├── LimitFilterData.cs │ │ ├── LongContentSubscriptionFilter.cs │ │ ├── PastFollowsSubscriptionFilter.cs │ │ ├── FollowersSubscriptionFilter.cs │ │ ├── ChannelListSubscriptionFilter.cs │ │ ├── FollowsAndDetailsSubscriptionFilter.cs │ │ ├── MentionSubscriptionFilter.cs │ │ ├── MainAccountSubscriptionFilter.cs │ │ ├── AuthorSubscriptionFilter.cs │ │ ├── EventSubscriptionFilter.cs │ │ ├── TagSubscriptionFilter.cs │ │ ├── ReplaceableEventSubscriptionFilter.cs │ │ ├── SubscriptionFilter.cs │ │ ├── AccountDetailsSubscriptionFilter.cs │ │ ├── Subscription.cs │ │ ├── ChannelSubscriptionFilter.cs │ │ └── DmSubscriptionFilter.cs │ ├── RelayFilters │ │ ├── IRelayFilter.cs │ │ ├── AllSubscriptionFilterFactory.cs │ │ └── AllSubscriptionFilter.cs │ ├── ISigner.cs │ └── ConfigService.cs ├── _Imports.razor ├── Externals │ ├── IMediaService.cs │ ├── MediaServiceProvider.cs │ ├── NostrBuildMediaService.cs │ ├── VoidCatMediaService.cs │ ├── NostrcheckMediaService.cs │ └── NostrImgMediaService.cs ├── Shared │ ├── CustomErrorBoundary.razor │ ├── EventViewer.razor.css │ ├── GlobalFeed.razor │ ├── MessageWrapperViewer.razor.css │ ├── UpdatabableElement.razor │ ├── AccountFeed.razor │ ├── NavMenu.razor.css │ ├── ModalContainer.razor │ ├── TimeStamp.razor │ ├── ChannelName.razor │ ├── ChannelMessageCount.razor │ ├── AccountAbout.razor │ ├── UnreadDmCounter.razor │ ├── ElementObserver.razor │ ├── Alert.razor │ ├── MentionsFeed.razor │ ├── PayButton.razor │ ├── ChannelPicture.razor │ ├── MainLayout.razor │ ├── ConfigSection.razor │ ├── FollowsFeed.razor │ ├── NoteViewerSeeMore.razor │ ├── AccountName.razor │ ├── FollowAccount.razor │ ├── Scripts.razor │ └── FollowsCounter.razor ├── Misc │ ├── Extensions.cs │ ├── IdGenerator.cs │ └── ProgressStream.cs ├── Main.razor ├── Migrations │ ├── 20230209163414_RelayFilters.cs │ ├── 20230126090204_AddAccountLastDetailsReceived.cs │ ├── 20230209075431_ProxyConfig.cs │ ├── 20230128154625_ManualRelayManagement.cs │ ├── 20230305143822_ReplaceableEvents.cs │ ├── 20230310163955_Muting.cs │ ├── 20230215113014_DirectMessages.cs │ └── 20230129160606_FollowsTable.cs └── Nostrid.Core.csproj ├── Doc ├── HowToSetupProxy.md └── HowToUseFollowsEditor.md ├── NNostr.Client ├── WebSocketExtensions.cs ├── NostrEventTag.cs ├── NNostr.Client.csproj ├── StringExtensions.cs ├── JsonConverters │ ├── UnixTimestampSecondsJsonConverter.cs │ └── NostrEventTagJsonConverter.cs ├── NostrEvent.cs ├── NostrSubscriptionFilter.cs └── AesEncryptor.cs ├── LICENSE.md └── .gitattributes /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Nostrid.Core.Test/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /Nostrid.Photino/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Photino/favicon.ico -------------------------------------------------------------------------------- /Nostrid/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Web/wwwroot/icon-192.png -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Web/wwwroot/icon-512.png -------------------------------------------------------------------------------- /Nostrid.Photino/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Photino/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Nostrid/Resources/Fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid/Resources/Fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /Nostrid/Platforms/iOS/NotificationCounter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | 3 | namespace Nostrid; 4 | 5 | public class NotificationCounter : NotificationCounterForMobile 6 | { 7 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/Android/NotificationCounter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | 3 | namespace Nostrid; 4 | 5 | public class NotificationCounter : NotificationCounterForMobile 6 | { 7 | } -------------------------------------------------------------------------------- /Nostrid/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid; 2 | 3 | public partial class MainPage : ContentPage 4 | { 5 | public MainPage() 6 | { 7 | InitializeComponent(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Nostrid/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Windows Machine": { 4 | "commandName": "MsixPackage", 5 | "nativeDebugging": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Nostrid/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /Nostrid/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /Nostrid.Core/Interfaces/IClipboardService.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Interfaces 2 | { 3 | public interface IClipboardService 4 | { 5 | Task CopyAsync(string content); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Nostrid.Core/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @inject NavigationManager navigationManager 3 | @code 4 | { 5 | protected override void OnInitialized() { navigationManager.NavigateTo("/feed"); } 6 | } -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Web/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Web/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /Nostrid.Photino/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Photino/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /Nostrid.Core/Model/NostrNip.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public static class NostrNip 4 | { 5 | public const int NostrNipGenericTag = 12; 6 | public const int NostrNipSearch = 50; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Reaction.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class Reaction 4 | { 5 | public string Content { get; set; } 6 | 7 | public string ReactorId { get; set; } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Nostrid.Photino/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapulpeta/Nostrid/HEAD/Nostrid.Photino/wwwroot/css/bootstrap-icons-1.10.3/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /Nostrid.Core/Model/TextInput.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Nostrid.Model; 4 | 5 | public class TextInput 6 | { 7 | public string Text { get; set; } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Nostrid.Web/NotificationCounter.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid; 2 | 3 | public class NotificationCounter : INotificationCounter 4 | { 5 | public void SetNotificationCount(int count) 6 | { 7 | // TODO: Implement this 8 | } 9 | } -------------------------------------------------------------------------------- /Nostrid.Core/Model/NostrKindClass.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public enum NostrKindClass 4 | { 5 | Other = 0, 6 | Replaceable = 1, 7 | Ephemeral = 2, 8 | ReplaceableParams = 3, 9 | } 10 | 11 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/IdType.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Nostrid.Model 4 | { 5 | public enum IdType 6 | { 7 | Unknown, 8 | Account, 9 | Event, 10 | Channel, 11 | } 12 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/IDbFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Model; 2 | 3 | namespace Nostrid.Data.Relays; 4 | 5 | public interface IDbFilter 6 | { 7 | public IQueryable ApplyDbFilter(IQueryable events); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Nostrid/Platforms/MacCatalyst/NotificationCounter.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid; 2 | 3 | public class NotificationCounter : INotificationCounter 4 | { 5 | public void SetNotificationCount(int count) 6 | { 7 | // TODO: Implement this 8 | } 9 | } -------------------------------------------------------------------------------- /Nostrid.Photino/Misc/NotificationCounter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid; 2 | 3 | internal class NotificationCounter : INotificationCounter 4 | { 5 | public void SetNotificationCount(int count) 6 | { 7 | // TODO: Implement this 8 | } 9 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/RelayFilters/IRelayFilter.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | 3 | namespace Nostrid.Data.Relays; 4 | 5 | public interface IRelayFilter 6 | { 7 | public NostrSubscriptionFilter[] GetFiltersForRelay(long relayId); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Mention.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class Mention 4 | { 5 | public int Index { get; set; } 6 | 7 | public char Type { get; set; } 8 | 9 | public string MentionId { get; set; } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/EventSeen.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class EventSeen 4 | { 5 | public int Id { get; set; } 6 | 7 | public string EventId { get; set; } 8 | 9 | public long RelayId { get; set; } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/Follow.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class Follow 4 | { 5 | public int Id { get; set; } 6 | 7 | public string AccountId { get; set; } 8 | 9 | public string FollowId { get; set; } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/LimitFilterData.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Data.Relays; 2 | 3 | public class LimitFilterData 4 | { 5 | public int? Limit { get; set; } 6 | public DateTimeOffset? Since { get; set; } 7 | public DateTimeOffset? Until { get; set; } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Lud06Data.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class Lud06Data 4 | { 5 | public string Callback { get; set; } 6 | 7 | public long MinSendableMsat { get; set; } 8 | 9 | public long MaxSendableMsat { get; set; } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Nostrid.Core/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Components.Forms 3 | @using Microsoft.AspNetCore.Components.Routing 4 | @using Microsoft.AspNetCore.Components.Web 5 | @using Microsoft.JSInterop 6 | @using Nostrid 7 | @using Nostrid.Shared 8 | -------------------------------------------------------------------------------- /Nostrid.Core.Test/Utils/TestUtils.cs: -------------------------------------------------------------------------------- 1 | internal static class TestUtils 2 | { 3 | 4 | public static string GetRandomNostrId() 5 | { 6 | byte[] bytes = new byte[32]; 7 | Random.Shared.NextBytes(bytes); 8 | return Convert.ToHexString(bytes).ToLower(); 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/Channel.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class Channel 4 | { 5 | public string Id { get; set; } 6 | 7 | public ChannelDetails? Details { get; set; } 8 | 9 | public string? CreatorId { get; set; } 10 | 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Nostrid/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4356AE 4 | #212B57 5 | #212B57 6 | -------------------------------------------------------------------------------- /Nostrid/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace Nostrid; 4 | 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | -------------------------------------------------------------------------------- /Nostrid/Platforms/MacCatalyst/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace Nostrid; 4 | 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | -------------------------------------------------------------------------------- /Nostrid/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "cdnjs", 4 | "libraries": [ 5 | { 6 | "library": "popper.js@2.11.6", 7 | "destination": "wwwroot/lib/popper.js/", 8 | "files": [ 9 | "umd/popper.min.js" 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/Mute.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class Mute 4 | { 5 | public int Id { get; set; } 6 | 7 | public string AccountId { get; set; } 8 | 9 | public string MuteId { get; set; } 10 | 11 | public bool IsPrivate { get; set; } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /Nostrid.Core/Interfaces/ITreeItem.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Model; 2 | 3 | namespace Nostrid.Interfaces 4 | { 5 | public interface ITreeItem 6 | { 7 | NoteTree Tree { get; } 8 | 9 | IEnumerable GetTreeItems(); 10 | 11 | Task IsVisibleAsync(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }); 5 | -------------------------------------------------------------------------------- /Nostrid.Photino/Misc/ClipboardService.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Interfaces; 2 | using System.Threading.Tasks; 3 | 4 | internal class ClipboardService : IClipboardService 5 | { 6 | public async Task CopyAsync(string content) 7 | { 8 | await TextCopy.ClipboardService.SetTextAsync(content); 9 | } 10 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/MacCatalyst/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | -------------------------------------------------------------------------------- /Nostrid/Misc/DbConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Misc 2 | { 3 | public static class DbConstants 4 | { 5 | public const string DatabaseFilename = "Nostr.sqlite.db"; 6 | 7 | public static string DatabasePath => 8 | Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Nostrid/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Components.Forms 3 | @using Microsoft.AspNetCore.Components.Routing 4 | @using Microsoft.AspNetCore.Components.Web 5 | @using Microsoft.AspNetCore.Components.Web.Virtualization 6 | @using Microsoft.JSInterop 7 | @using Nostrid 8 | @using Nostrid.Shared 9 | -------------------------------------------------------------------------------- /Nostrid.Photino/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "cdnjs", 4 | "libraries": [ 5 | { 6 | "provider": "cdnjs", 7 | "library": "popper.js@2.11.6", 8 | "destination": "wwwroot/lib/popper.js/", 9 | "files": [ 10 | "umd/popper.min.js" 11 | ] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/FeedSource.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace Nostrid.Model; 4 | 5 | public class FeedSource 6 | { 7 | public long Id { get; set; } 8 | 9 | public string OwnerId { get; set; } 10 | 11 | public List Hashtags { get; set; } = new(); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /Nostrid/Misc/ClipboardService.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Interfaces; 2 | 3 | namespace Nostrid.Misc 4 | { 5 | public class ClipboardService : IClipboardService 6 | { 7 | public async Task CopyAsync(string content) 8 | { 9 | await Clipboard.Default.SetTextAsync(content); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Nostrid.Core/Externals/IMediaService.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Externals 2 | { 3 | public interface IMediaService 4 | { 5 | public string Name { get; } 6 | 7 | public int MaxSize { get; } 8 | 9 | public Task UploadFile(Stream data, string filename, string mimeType, Action progress); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Nostrid/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/DmPair.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class DmPair 4 | { 5 | public int Id { get; set; } 6 | 7 | public string AccountL { get; set; } 8 | 9 | public string AccountH { get; set; } 10 | 11 | public DateTime LastReadL { get; set; } 12 | 13 | public DateTime LastReadH { get; set; } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/CustomErrorBoundary.razor: -------------------------------------------------------------------------------- 1 | @inherits ErrorBoundary 2 | 3 | @ChildContent 4 | 5 | @code { 6 | [Parameter] 7 | public EventCallback OnError { get; set; } 8 | 9 | protected override Task OnErrorAsync(Exception exception) 10 | { 11 | OnError.InvokeAsync(exception); 12 | return base.OnErrorAsync(exception); 13 | } 14 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/Tizen/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Maui; 3 | using Microsoft.Maui.Hosting; 4 | 5 | namespace Nostrid; 6 | 7 | class Program : MauiApplication 8 | { 9 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 10 | 11 | static void Main(string[] args) 12 | { 13 | var app = new Program(); 14 | app.Run(args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/EventViewer.razor.css: -------------------------------------------------------------------------------- 1 | .note-content-wrapper { 2 | max-height: 50vh; 3 | overflow: hidden; 4 | transition: background ease-in-out 3s; 5 | } 6 | 7 | .note-content-wrapper.expanded { 8 | max-height: initial; 9 | } 10 | 11 | .note-content-wrapper.to-expand { 12 | -webkit-mask: linear-gradient(#000 calc(100% - 3rem), #0000); 13 | } 14 | -------------------------------------------------------------------------------- /Nostrid.Photino/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Authorization 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.JSInterop 9 | @using Microsoft.Extensions.Logging 10 | -------------------------------------------------------------------------------- /Nostrid/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | 4 | namespace Nostrid; 5 | 6 | [Application] 7 | public class MainApplication : MauiApplication 8 | { 9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) 10 | : base(handle, ownership) 11 | { 12 | } 13 | 14 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 15 | } 16 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/ISigner.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | 3 | namespace Nostrid.Data 4 | { 5 | public interface ISigner 6 | { 7 | public Task GetPubKey(); 8 | 9 | public Task Sign(NostrEvent ev); 10 | 11 | public Task EncryptNip04(string pubkey, string content); 12 | 13 | public Task DecryptNip04(string pubkey, string content); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/ChannelWithInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class ChannelWithInfo : Channel 4 | { 5 | public int MessageCount { get; set; } 6 | 7 | public ChannelWithInfo() 8 | { 9 | } 10 | 11 | public ChannelWithInfo(Channel channel) 12 | { 13 | Id = channel.Id; 14 | Details = channel.Details; 15 | CreatorId = channel.CreatorId; 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Nostrid.Web/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Nostrid 10 | @using Nostrid.Shared 11 | -------------------------------------------------------------------------------- /Nostrid/Platforms/MacCatalyst/Program.cs: -------------------------------------------------------------------------------- 1 | using ObjCRuntime; 2 | using UIKit; 3 | 4 | namespace Nostrid; 5 | 6 | public class Program 7 | { 8 | // This is the main entry point of the application. 9 | static void Main(string[] args) 10 | { 11 | // if you want to use a different Application Delegate class from "AppDelegate" 12 | // you can specify it here. 13 | UIApplication.Main(args, null, typeof(AppDelegate)); 14 | } 15 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using ObjCRuntime; 2 | using UIKit; 3 | 4 | namespace Nostrid; 5 | 6 | public class Program 7 | { 8 | // This is the main entry point of the application. 9 | static void Main(string[] args) 10 | { 11 | // if you want to use a different Application Delegate class from "AppDelegate" 12 | // you can specify it here. 13 | UIApplication.Main(args, null, typeof(AppDelegate)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/Account.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class Account 4 | { 5 | public string Id { get; set; } 6 | 7 | public string? PrivKey { get; set; } 8 | 9 | public AccountDetails? Details { get; set; } 10 | 11 | public DateTimeOffset? FollowsLastUpdate { get; set; } 12 | 13 | public DateTimeOffset? MutesLastUpdate { get; set; } 14 | 15 | public DateTime LastNotificationRead { get; set; } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Nostrid.Photino/Misc/DbConstants.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System; 3 | 4 | internal static class DbConstants 5 | { 6 | public const string DatabaseFilename = "Nostr.sqlite.db"; 7 | public const string AppName = "Nostrid"; 8 | 9 | public static string DatabasePath => 10 | Path.Combine( 11 | Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 12 | AppName, 13 | DatabaseFilename); 14 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Android.OS; 4 | 5 | namespace Nostrid; 6 | 7 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] 8 | public class MainActivity : MauiAppCompatActivity 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/TagData.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class TagData 4 | { 5 | public Event Event { get; set; } 6 | 7 | public int Id { get; set; } 8 | 9 | public int TagIndex { get; set; } 10 | 11 | public int DataCount { get; set; } 12 | 13 | public string? Data0 { get; set; } 14 | 15 | public string? Data1 { get; set; } 16 | 17 | public string? Data2 { get; set; } 18 | 19 | public string? Data3 { get; set; } 20 | } -------------------------------------------------------------------------------- /Nostrid.Core/Shared/GlobalFeed.razor: -------------------------------------------------------------------------------- 1 | @using Nostrid.Model; 2 | @using Nostrid.Data.Relays; 3 | 4 | @inject AllSubscriptionFilterFactory allSubscriptionFilterFactory; 5 | 6 | 7 | 8 | @code { 9 | private SubscriptionFilter? filter; 10 | 11 | protected override void OnParametersSet() 12 | { 13 | filter = allSubscriptionFilterFactory.Create(); 14 | } 15 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/RelayFilters/AllSubscriptionFilterFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Data.Relays; 2 | 3 | public class AllSubscriptionFilterFactory 4 | { 5 | private EventDatabase eventDatabase; 6 | 7 | public AllSubscriptionFilterFactory(EventDatabase eventDatabase) 8 | { 9 | this.eventDatabase = eventDatabase; 10 | } 11 | 12 | public AllSubscriptionFilter Create() 13 | { 14 | return new AllSubscriptionFilter(eventDatabase); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Nostrid.Core/Misc/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Nostrid 4 | { 5 | public static class Extensions 6 | { 7 | public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) 8 | { 9 | return string.IsNullOrEmpty(value); 10 | } 11 | 12 | public static bool IsNotNullOrEmpty([NotNullWhen(true)] this string? value) 13 | { 14 | return !string.IsNullOrEmpty(value); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/NoteTreeShowMore.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class NoteTreeShowMore : NoteTreeNode 4 | { 5 | public int ShowMoreChildIndex { get; set; } 6 | 7 | public string ParentEventId { get; set; } 8 | 9 | public bool Start { get; set; } 10 | 11 | public NoteTreeShowMore(int showMoreChildIndex, bool start, string parentEventId) 12 | { 13 | ShowMoreChildIndex = showMoreChildIndex; 14 | Start = start; 15 | ParentEventId = parentEventId; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nostrid.Web", 3 | "short_name": "Nostrid.Web", 4 | "start_url": "./", 5 | "display": "standalone", 6 | "background_color": "#ffffff", 7 | "theme_color": "#03173d", 8 | "prefer_related_applications": false, 9 | "icons": [ 10 | { 11 | "src": "icon-512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | }, 15 | { 16 | "src": "icon-192.png", 17 | "type": "image/png", 18 | "sizes": "192x192" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /Nostrid/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Nostrid.Core/Main.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Sorry, there's nothing at this address.

9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /Nostrid.Web/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "cdnjs", 4 | "libraries": [ 5 | { 6 | "library": "bootstrap@5.3.0-alpha1", 7 | "destination": "wwwroot/lib/bootstrap/", 8 | "files": [ 9 | "css/bootstrap.css", 10 | "css/bootstrap.min.css", 11 | "js/bootstrap.js", 12 | "js/bootstrap.min.js" 13 | ] 14 | }, 15 | { 16 | "library": "popper.js@2.11.6", 17 | "destination": "wwwroot/lib/popper.js/", 18 | "files": [ 19 | "umd/popper.min.js" 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /Doc/HowToSetupProxy.md: -------------------------------------------------------------------------------- 1 | # How to configure a proxy 2 | 3 | Nostrid can route its traffic through a proxy, including through Tor. 4 | 5 | Follow these steps to configure a proxy: 6 | 7 | 1. In the main menu, go to `Settings` 8 | 2. Go to `Proxy` 9 | 3. Enter proxy URI, for example for Tor it should be `socks5://127.0.0.1:9150` 10 | 4. Optionally enter username and password if required by proxy 11 | 5. Press `Apply` 12 | 13 | ![Proxy](https://raw.githubusercontent.com/lapulpeta/Nostrid-media/main/proxy1.jpg) 14 | ![Proxy](https://raw.githubusercontent.com/lapulpeta/Nostrid-media/main/proxy2.jpg) -------------------------------------------------------------------------------- /Nostrid.Web/Services/ClipboardService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using Nostrid.Interfaces; 3 | 4 | namespace Nostrid.Web.Services 5 | { 6 | public class ClipboardService : IClipboardService 7 | { 8 | private readonly IJSRuntime jsInterop; 9 | 10 | public ClipboardService(IJSRuntime jsInterop) 11 | { 12 | this.jsInterop = jsInterop; 13 | } 14 | 15 | public async Task CopyAsync(string content) 16 | { 17 | await jsInterop.InvokeVoidAsync("navigator.clipboard.writeText", content); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /NNostr.Client/WebSocketExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | using System.Text; 3 | 4 | namespace NNostr.Client 5 | { 6 | public static class WebSocketExtensions 7 | { 8 | public static async Task SendMessageAsync(this WebSocket socket, string message, CancellationToken cancellationToken) 9 | { 10 | if (socket.State != WebSocketState.Open) 11 | return; 12 | var buffer = Encoding.UTF8.GetBytes(message); 13 | await socket.SendAsync(new Memory(buffer), WebSocketMessageType.Text, true, cancellationToken); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Nostrid.Core/Pages/Compose.razor: -------------------------------------------------------------------------------- 1 | @page "/compose" 2 | 3 | @using Nostrid.Data; 4 | 5 | @inject AccountService accountService 6 | 7 |

Compose

8 | @if (accountService.MainAccount != null) 9 | { 10 | 11 | } 12 | else 13 | { 14 |
15 | 16 |

No account selected.

17 |

To send notes select an account first.

18 |
19 | } 20 | 21 | @code { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Nostrid.Core/Pages/Long.razor: -------------------------------------------------------------------------------- 1 | @page "/long" 2 | 3 | @using Nostrid.Data 4 | @using Nostrid.Data.Relays; 5 | @using Nostrid.Model; 6 | @inject AccountService accountService 7 | @inject FeedService feedService 8 | @inject NavigationManager navigationManager 9 | 10 |

Blogs

11 | 12 | 13 | @code { 14 | private SubscriptionFilter? filter; 15 | private string[]? hashtags; 16 | private long id; 17 | 18 | protected override void OnInitialized() 19 | { 20 | filter = new LongContentSubscriptionFilter(); 21 | } 22 | } -------------------------------------------------------------------------------- /Nostrid/MainPage.xaml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/EventDetailsCount.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace Nostrid.Model; 4 | 5 | public class EventDetailsCount 6 | { 7 | public ConcurrentDictionary ReactionGroups = new(); 8 | 9 | public int Reposts; 10 | 11 | public int Zaps; 12 | 13 | public void Add(EventDetailsCount delta) 14 | { 15 | Interlocked.Add(ref Reposts, delta.Reposts); 16 | Interlocked.Add(ref Zaps, delta.Zaps); 17 | foreach (var (reaction, count) in delta.ReactionGroups) 18 | { 19 | ReactionGroups.AddOrUpdate(reaction, count, (_, oldv) => oldv + count); 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/LongContentSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | using Nostrid.Model; 3 | 4 | namespace Nostrid.Data.Relays; 5 | 6 | public class LongContentSubscriptionFilter : SubscriptionFilter 7 | { 8 | public override NostrSubscriptionFilter[] GetFilters() 9 | { 10 | return new[] { 11 | new NostrSubscriptionFilter() { Kinds = new[]{ NostrKind.LongContent }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 12 | }; 13 | } 14 | 15 | public override SubscriptionFilter Clone() 16 | { 17 | return new LongContentSubscriptionFilter(); 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Nostrid.Core/Externals/MediaServiceProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Externals 2 | { 3 | public class MediaServiceProvider 4 | { 5 | private List _mediaServices = new(); 6 | 7 | public MediaServiceProvider(IEnumerable mediaServices) 8 | { 9 | _mediaServices = new(mediaServices); 10 | } 11 | 12 | public void RegisterMediaService(IMediaService service) 13 | { 14 | _mediaServices.Add(service); 15 | } 16 | 17 | public IEnumerable GetMediaServices() 18 | { 19 | return _mediaServices.AsReadOnly(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/file.js: -------------------------------------------------------------------------------- 1 | export function mountAndInitializeDb() { 2 | window.Module.FS.mkdir('/database'); 3 | window.Module.FS.mount(window.Module.FS.filesystems.IDBFS, {}, '/database'); 4 | return syncDatabase(true); 5 | } 6 | 7 | export function syncDatabase(populate) { 8 | 9 | return new Promise((resolve, reject) => { 10 | window.Module.FS.syncfs(populate, (err) => { 11 | if (err) { 12 | console.log('syncfs failed. Error:', err); 13 | reject(err); 14 | } 15 | else { 16 | console.log('synced successfull.'); 17 | resolve(); 18 | } 19 | }); 20 | }); 21 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/PastFollowsSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | using Nostrid.Model; 3 | 4 | namespace Nostrid.Data.Relays; 5 | 6 | public class PastFollowsSubscriptionFilter : SubscriptionFilter 7 | { 8 | private readonly string id; 9 | 10 | public PastFollowsSubscriptionFilter(string id) 11 | { 12 | this.id = id; 13 | } 14 | 15 | public override NostrSubscriptionFilter[] GetFilters() 16 | { 17 | return new[] { 18 | new NostrSubscriptionFilter() { Authors = new[] { id }, Kinds = new[]{ NostrKind.Contacts } } 19 | }; 20 | } 21 | 22 | public override SubscriptionFilter Clone() 23 | { 24 | return new PastFollowsSubscriptionFilter(id); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Nostrid/Resources/Raw/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories). Deployment of the asset to your application 3 | is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. 4 | 5 | 6 | 7 | These files will be deployed with you package and will be accessible using Essentials: 8 | 9 | async Task LoadMauiAsset() 10 | { 11 | using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); 12 | using var reader = new StreamReader(stream); 13 | 14 | var contents = reader.ReadToEnd(); 15 | } 16 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/Config.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class Config 4 | { 5 | public long Id { get; set; } 6 | 7 | public bool ShowDifficulty { get; set; } 8 | 9 | public int MinDiffIncoming { get; set; } 10 | 11 | public bool StrictDiffCheck { get; set; } 12 | 13 | public int TargetDiffOutgoing { get; set; } 14 | 15 | public string? MainAccountId { get; set; } 16 | 17 | public string? Theme { get; set; } 18 | 19 | public bool ManualRelayManagement { get; set; } 20 | 21 | public int MaxAutoRelays { get; set; } 22 | 23 | public string? ProxyUri { get; set; } 24 | 25 | public string? ProxyUser { get; set; } 26 | 27 | public string? ProxyPassword { get; set; } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/Relay.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace Nostrid.Model; 4 | 5 | public class Relay 6 | { 7 | public long Id { get; set; } 8 | 9 | public string Uri { get; set; } 10 | 11 | public int Priority { get; set; } 12 | 13 | public bool Read { get; set; } 14 | 15 | public bool Write { get; set; } 16 | 17 | public bool IsPaid { get; set; } 18 | 19 | [NotMapped] 20 | public List SupportedNips { get; set; } = new(); 21 | 22 | public override bool Equals(object obj) 23 | { 24 | return obj is Relay relay && 25 | Id == relay.Id; 26 | } 27 | 28 | public override int GetHashCode() 29 | { 30 | return HashCode.Combine(Id); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Nip11Response.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Nostrid.Model 4 | { 5 | public class Nip11Response 6 | { 7 | [JsonProperty("name")] 8 | public string Name { get; set; } 9 | 10 | [JsonProperty("description")] 11 | public string Description { get; set; } 12 | 13 | [JsonProperty("pubkey")] 14 | public string Pubkey { get; set; } 15 | 16 | [JsonProperty("contact")] 17 | public string Contact { get; set; } 18 | 19 | [JsonProperty("supported_nips")] 20 | public List SupportedNips { get; set; } 21 | 22 | [JsonProperty("software")] 23 | public string Software { get; set; } 24 | 25 | [JsonProperty("version")] 26 | public string Version { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /Nostrid.Core/Model/NostrKind.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public static class NostrKind 4 | { 5 | public const int Metadata = 0; 6 | public const int Text = 1; 7 | public const int Relay = 2; 8 | public const int Contacts = 3; 9 | public const int DM = 4; 10 | public const int Deletion = 5; 11 | public const int Repost = 6; 12 | public const int Reaction = 7; 13 | public const int ChannelCreation = 40; 14 | public const int ChannelMetadata = 41; 15 | public const int ChannelMessage = 42; 16 | public const int ChannelHideMessage = 43; 17 | public const int ChannelMuteUser = 44; 18 | public const int Zap = 9735; 19 | public const int Mutes = 10000; 20 | public const int Badge = 30009; 21 | public const int LongContent = 30023; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Nostrid/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Nostrid/Platforms/Tizen/tizen-manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | maui-appicon-placeholder 7 | 8 | 9 | 10 | 11 | http://tizen.org/privilege/internet 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Nostrid/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | // To learn more about WinUI, the WinUI project structure, 4 | // and more about our project templates, see: http://aka.ms/winui-project-info. 5 | 6 | namespace Nostrid.WinUI; 7 | 8 | /// 9 | /// Provides application-specific behavior to supplement the default Application class. 10 | /// 11 | public partial class App : MauiWinUIApplication 12 | { 13 | /// 14 | /// Initializes the singleton application object. This is the first line of authored code 15 | /// executed, and as such is the logical equivalent of main() or WinMain(). 16 | /// 17 | public App() 18 | { 19 | this.InitializeComponent(); 20 | } 21 | 22 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Nostrid/Misc/NotificationCounterForMobile.cs: -------------------------------------------------------------------------------- 1 | using Plugin.LocalNotification; 2 | 3 | namespace Nostrid.Misc; 4 | 5 | public class NotificationCounterForMobile : INotificationCounter 6 | { 7 | public void SetNotificationCount(int count) 8 | { 9 | Task.Run(async () => 10 | { 11 | if (!await LocalNotificationCenter.Current.AreNotificationsEnabled()) 12 | { 13 | await LocalNotificationCenter.Current.RequestNotificationPermission(); 14 | } 15 | 16 | LocalNotificationCenter.Current.Clear(); 17 | 18 | if (count == 0) 19 | return; 20 | 21 | var notification = new NotificationRequest 22 | { 23 | NotificationId = 100, 24 | Title = "Nostrid", 25 | Description = $"You have {count} unread notification(s)", 26 | }; 27 | await LocalNotificationCenter.Current.Show(notification); 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/FollowersSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | using Nostrid.Model; 3 | 4 | namespace Nostrid.Data.Relays; 5 | 6 | public class FollowersSubscriptionFilter : SubscriptionFilter 7 | { 8 | private readonly string id; 9 | 10 | public FollowersSubscriptionFilter(string id) 11 | { 12 | this.id = id; 13 | } 14 | 15 | public override NostrSubscriptionFilter[] GetFilters() 16 | { 17 | return new[] { 18 | new NostrSubscriptionFilter() { PublicKey = new[] { id }, Kinds = new[]{ NostrKind.Metadata, NostrKind.Contacts }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until } 19 | }; 20 | } 21 | 22 | public override SubscriptionFilter Clone() 23 | { 24 | return new FollowersSubscriptionFilter(id); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Nostrid.Core.Test/Utils/TempDb.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.Sqlite; 2 | 3 | internal class TempDb : IDisposable 4 | { 5 | private readonly SqliteConnection keepAliveConnection; 6 | private readonly string dbname; 7 | 8 | private bool disposedValue; 9 | 10 | public string DbName => dbname; 11 | 12 | public TempDb() 13 | { 14 | dbname = $"nostr.{Random.Shared.Next()}.test;mode=memory;cache=shared"; 15 | keepAliveConnection = new SqliteConnection($"DataSource={dbname}"); 16 | keepAliveConnection.Open(); 17 | } 18 | 19 | protected virtual void Dispose(bool disposing) 20 | { 21 | if (!disposedValue) 22 | { 23 | if (disposing) 24 | { 25 | keepAliveConnection.Dispose(); 26 | } 27 | 28 | disposedValue = true; 29 | } 30 | } 31 | 32 | public void Dispose() 33 | { 34 | Dispose(disposing: true); 35 | GC.SuppressFinalize(this); 36 | } 37 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/Windows/NotificationCounter.cs: -------------------------------------------------------------------------------- 1 | using Windows.Data.Xml.Dom; 2 | using Windows.UI.Notifications; 3 | 4 | namespace Nostrid; 5 | 6 | public class NotificationCounter : INotificationCounter 7 | { 8 | public NotificationCounter() 9 | { 10 | SetNotificationCount(0); 11 | } 12 | 13 | public void SetNotificationCount(int count) 14 | { 15 | var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication(); 16 | if (count <= 0) 17 | { 18 | badgeUpdater.Clear(); 19 | } 20 | else 21 | { 22 | var badgeXml = BadgeUpdateManager.GetTemplateContent(BadgeTemplateType.BadgeNumber); 23 | 24 | var badgeElement = badgeXml.SelectSingleNode("/badge") as XmlElement; 25 | badgeElement?.SetAttribute("value", count.ToString()); 26 | 27 | var badge = new BadgeNotification(badgeXml); 28 | badgeUpdater.Update(badge); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/ChannelListSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | /// 8 | /// Returns all channels 9 | /// 10 | public class ChannelListSubscriptionFilter : SubscriptionFilter 11 | { 12 | public ChannelListSubscriptionFilter() 13 | { 14 | } 15 | 16 | public override NostrSubscriptionFilter[] GetFilters() 17 | { 18 | return new[] { 19 | new NostrSubscriptionFilter() { Kinds = new[]{ NostrKind.ChannelCreation, NostrKind.ChannelMetadata }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 20 | }; 21 | } 22 | 23 | public override SubscriptionFilter Clone() 24 | { 25 | return new ChannelListSubscriptionFilter(); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/signer.js: -------------------------------------------------------------------------------- 1 | export async function getPublicKey() { 2 | if (!window.nostr || !window.nostr.getPublicKey) 3 | return null; 4 | return await window.nostr.getPublicKey(); 5 | } 6 | 7 | export async function signEvent(eventStr) { 8 | event = JSON.parse(eventStr) 9 | event = await window.nostr.signEvent(event); 10 | return JSON.stringify(event); 11 | } 12 | 13 | export async function decryptNip04(pubkey, plaintext) { 14 | if (!window.nostr || !window.nostr.nip04 || !window.nostr.nip04.decrypt) 15 | return null; 16 | return await window.nostr.nip04.decrypt(pubkey, plaintext); 17 | } 18 | 19 | export async function encryptNip04(pubkey, plaintext) { 20 | if (!window.nostr || !window.nostr.nip04 || !window.nostr.nip04.encrypt) 21 | return null; 22 | return await window.nostr.nip04.encrypt(pubkey, plaintext); 23 | } -------------------------------------------------------------------------------- /NNostr.Client/NostrEventTag.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NNostr.Client.JsonConverters; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace NNostr.Client 6 | { 7 | [JsonConverter(typeof(NostrEventTagJsonConverter))] 8 | public class NostrEventTag 9 | { 10 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 11 | public string Id { get; set; } 12 | 13 | public string EventId { get; set; } 14 | 15 | public string TagIdentifier { get; set; } 16 | 17 | public List Data { get; set; } = new(); 18 | 19 | [JsonIgnore] 20 | public NostrEvent Event { get; set; } 21 | 22 | public override string ToString() 23 | { 24 | var d = TagIdentifier is null ? Data : Data.Prepend(TagIdentifier); 25 | return JsonConvert.SerializeObject(d); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Nostrid.Core.Test/Nostrid.Core.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/FollowsAndDetailsSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | using Nostrid.Model; 3 | 4 | namespace Nostrid.Data.Relays; 5 | 6 | public class FollowsAndDetailsSubscriptionFilter : SubscriptionFilter 7 | { 8 | private readonly string id; 9 | 10 | public FollowsAndDetailsSubscriptionFilter(string id) 11 | { 12 | this.id = id; 13 | } 14 | 15 | public override NostrSubscriptionFilter[] GetFilters() 16 | { 17 | return new[] { 18 | new NostrSubscriptionFilter() { Authors = new[] { id }, Kinds = new[]{ NostrKind.Metadata }, Limit = 1 }, // Get the latest details 19 | new NostrSubscriptionFilter() { Authors = new[] { id }, Kinds = new[]{ NostrKind.Contacts }, Limit = 1 } // Get the latest follows 20 | }; 21 | } 22 | 23 | public override SubscriptionFilter Clone() 24 | { 25 | return new FollowsAndDetailsSubscriptionFilter(id); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Nostrid.Core/Misc/IdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CodeDom.Compiler; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Nostrid.Misc 9 | { 10 | internal class IdGenerator 11 | { 12 | private static Random random = new Random(); 13 | private const string characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 14 | 15 | private static string GenerateRandomString(int length) 16 | { 17 | // Use the Random class to generate a random number for each character 18 | return new string( 19 | Enumerable.Repeat(characters, length) 20 | .Select(s => s[random.Next(s.Length)]) 21 | .ToArray()); 22 | } 23 | 24 | public static string Generate() 25 | { 26 | return GenerateRandomString(20); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/MentionSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | public class MentionSubscriptionFilter : SubscriptionFilter 8 | { 9 | private readonly string[] ids; 10 | 11 | public MentionSubscriptionFilter(string id) : this(new[] { id }) 12 | { 13 | } 14 | 15 | public MentionSubscriptionFilter(string[] ids) 16 | { 17 | this.ids = ids; 18 | } 19 | 20 | public override NostrSubscriptionFilter[] GetFilters() 21 | { 22 | return new[] { 23 | new NostrSubscriptionFilter() { PublicKey = ids, Kinds = new[]{ NostrKind.Text, NostrKind.Deletion, NostrKind.Repost }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 24 | }; 25 | } 26 | 27 | public override SubscriptionFilter Clone() 28 | { 29 | return new MentionSubscriptionFilter(ids); 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/MainAccountSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | public class MainAccountSubscriptionFilter : SubscriptionFilter 8 | { 9 | private readonly string[] ids; 10 | 11 | public MainAccountSubscriptionFilter(string id) 12 | { 13 | ids = new[] { id }; 14 | } 15 | 16 | public override NostrSubscriptionFilter[] GetFilters() 17 | { 18 | return new[] { 19 | // No need to limit to 1 becase these kinds are replaceable 20 | new NostrSubscriptionFilter() { Authors = ids, Kinds = new[]{ NostrKind.Metadata , NostrKind.Contacts, NostrKind.Mutes } }, 21 | new NostrSubscriptionFilter() { Authors = ids, Kinds = new[]{ NostrKind.Deletion } } 22 | }; 23 | } 24 | 25 | public override SubscriptionFilter Clone() 26 | { 27 | return new MainAccountSubscriptionFilter(ids[0]); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/ChannelDetails.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Nostrid.Model; 5 | 6 | public class ChannelDetails 7 | { 8 | #region Json 9 | [JsonPropertyName("name")] 10 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 11 | public string? Name { get; set; } 12 | 13 | [JsonPropertyName("about")] 14 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 15 | public string? About { get; set; } 16 | 17 | [JsonPropertyName("picture")] 18 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 19 | public string? PictureUrl { get; set; } 20 | #endregion 21 | 22 | [JsonIgnore] 23 | [ForeignKey("Channel")] 24 | public string Id { get; set; } 25 | 26 | [JsonIgnore] 27 | public Channel Channel { get; set; } 28 | 29 | [JsonIgnore] 30 | public DateTime DetailsLastUpdate { get; set; } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/MessageWrapperViewer.razor.css: -------------------------------------------------------------------------------- 1 | .triangle { 2 | --triangle-width: 10px; 3 | --triangle-height: 10px; 4 | position: relative; 5 | } 6 | 7 | .triangle:before { 8 | content: ''; 9 | position: absolute; 10 | top: 0; 11 | border-bottom: solid var(--triangle-height) transparent; 12 | } 13 | 14 | .triangle.triangle-start { 15 | border-top-left-radius: 0 !important; 16 | } 17 | 18 | .triangle.triangle-start:before { 19 | left: calc((var(--triangle-width)-1)*-1); 20 | border-right: solid var(--triangle-width) rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity)); 21 | } 22 | 23 | .triangle.triangle-end { 24 | border-top-right-radius: 0 !important; 25 | } 26 | 27 | .triangle.triangle-end:before { 28 | right: calc((var(--triangle-width)-1)*-1); 29 | border-left: solid var(--triangle-width) var(--bs-success-bg-subtle); 30 | } 31 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/AuthorSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | public class AuthorSubscriptionFilter : SubscriptionFilter 8 | { 9 | private readonly string[] ids; 10 | 11 | public AuthorSubscriptionFilter(string id) : this(new[] { id }) 12 | { 13 | } 14 | 15 | public AuthorSubscriptionFilter(string[] ids) 16 | { 17 | this.ids = ids; 18 | } 19 | 20 | public override NostrSubscriptionFilter[] GetFilters() 21 | { 22 | return new[] { 23 | new NostrSubscriptionFilter() { Authors = ids, Kinds = new[]{ NostrKind.Text, NostrKind.Contacts, NostrKind.Deletion, NostrKind.Repost, NostrKind.Reaction }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until } 24 | }; 25 | } 26 | 27 | public override SubscriptionFilter Clone() 28 | { 29 | return new AuthorSubscriptionFilter(ids); 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Nostrid.Photino/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nostrid 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |
19 | Loading... 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Nostrid/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nostrid 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |
19 | Loading... 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/EventSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | public class EventSubscriptionFilter : SubscriptionFilter 8 | { 9 | private readonly string[] ids; 10 | 11 | public EventSubscriptionFilter(string id) : this(new[] { id }) 12 | { 13 | } 14 | 15 | public EventSubscriptionFilter(string[] ids) 16 | { 17 | this.ids = ids; 18 | } 19 | 20 | public override NostrSubscriptionFilter[] GetFilters() 21 | { 22 | return new[] { 23 | new NostrSubscriptionFilter() { Ids = ids, Kinds = new[]{ NostrKind.Text, NostrKind.ChannelMessage, NostrKind.LongContent } }, 24 | new NostrSubscriptionFilter() { EventId = ids, Kinds = new[]{ NostrKind.Text, NostrKind.Deletion, NostrKind.Repost, NostrKind.Reaction, NostrKind.Zap } } 25 | }; 26 | } 27 | 28 | public override SubscriptionFilter Clone() 29 | { 30 | return new EventSubscriptionFilter(ids); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/UpdatabableElement.razor: -------------------------------------------------------------------------------- 1 | @using Nostrid.Misc; 2 | 3 | @code{ 4 | [Parameter, EditorRequired] 5 | public RenderFragment ChildContent { get; set; } 6 | 7 | [Parameter] 8 | public int MinSecBetweenUpdates { get; set; } = 3; 9 | } 10 | 11 | @ChildContent 12 | 13 | @code 14 | { 15 | private DateTime lastUpdate; 16 | private Task? updateTask; 17 | 18 | private readonly object lockObj = new(); 19 | 20 | public void Update() 21 | { 22 | lock (lockObj) 23 | { 24 | if (updateTask != null) 25 | { 26 | return; 27 | } 28 | double waitFor = Utils.Between((lastUpdate.AddSeconds(MinSecBetweenUpdates) - DateTime.UtcNow).TotalSeconds, min: 0, max: MinSecBetweenUpdates); 29 | updateTask = Task.Delay(TimeSpan.FromSeconds(waitFor)).ContinueWith((_) => 30 | { 31 | lastUpdate = DateTime.UtcNow; 32 | InvokeAsync(() => StateHasChanged()); 33 | updateTask = null; 34 | }); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Nostrid.Core/Shared/AccountFeed.razor: -------------------------------------------------------------------------------- 1 | @using Nostrid.Data; 2 | @using Nostrid.Model; 3 | @using Nostrid.Data.Relays; 4 | 5 | @implements IDisposable 6 | 7 | @inject RelayService relayService 8 | 9 | @code{ 10 | [Parameter] 11 | public string AccountId { get; set; } 12 | } 13 | 14 | 15 | 16 | @code { 17 | private SubscriptionFilter? filter; 18 | 19 | protected override void OnParametersSet() 20 | { 21 | filter = new AuthorSubscriptionFilter(AccountId); 22 | } 23 | 24 | #region Dispose 25 | private bool _disposed; 26 | 27 | public void Dispose() => Dispose(true); 28 | 29 | protected virtual void Dispose(bool disposing) 30 | { 31 | if (!_disposed) 32 | { 33 | if (disposing) 34 | { 35 | Cleanup(); 36 | } 37 | 38 | _disposed = true; 39 | } 40 | } 41 | 42 | private void Cleanup() 43 | { 44 | relayService.DeleteFilters(filter); 45 | } 46 | #endregion 47 | } -------------------------------------------------------------------------------- /Nostrid.Web/Nostrid.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | service-worker-assets.js 8 | true 9 | -lidbfs.js 10 | 134217728 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Nostrid.Photino/Nostrid.Photino.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net7.0 6 | favicon.ico 7 | false 8 | Nostrid 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | 29 | Always 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 Nostrid Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Nostrid.Core/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .display-active { 2 | display: none; 3 | } 4 | 5 | .active .display-active { 6 | display: initial; 7 | } 8 | 9 | .active .display-inactive { 10 | display: none; 11 | } 12 | 13 | .offcanvas { 14 | max-width: 75vw; 15 | } 16 | 17 | @media (min-width: 768px) { 18 | .offcanvas { 19 | position: inherit; 20 | visibility: visible; 21 | transform: translate(0px); 22 | transition: none; 23 | min-width: 218px; 24 | max-width: 218px; 25 | } 26 | } 27 | 28 | .nav-pills { 29 | margin-top: calc(var(--bs-nav-link-padding-y)*-1) !important; 30 | } 31 | 32 | .nav-pills .nav-item { 33 | margin-left: -1rem !important; 34 | margin-bottom: .25rem; 35 | } 36 | 37 | ::deep .nav-pills .nav-link { 38 | border-top-left-radius: 0 !important; 39 | border-top-right-radius: 999px !important; 40 | border-bottom-left-radius: 0 !important; 41 | border-bottom-right-radius: 999px !important; 42 | } 43 | 44 | .account-name { 45 | text-overflow: ellipsis; 46 | } 47 | 48 | ::deep .account-name wbr { 49 | display: none; 50 | } -------------------------------------------------------------------------------- /NNostr.Client/NNostr.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 10 8 | true 9 | 0.0.17 10 | Nostr Client 11 | A client for Nostr 12 | MIT 13 | https://github.com/Kukks/NNostr 14 | https://raw.githubusercontent.com/Kukks/NNostr/master/LICENSE 15 | MIT 16 | https://github.com/Kukks/NNostr 17 | Nostr 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /NNostr.Client/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using NBitcoin.Secp256k1; 3 | 4 | namespace NNostr.Client 5 | { 6 | public static class StringExtensions 7 | { 8 | public static byte[] DecodHexData(this string encoded) 9 | { 10 | return Convert.FromHexString(encoded); 11 | } 12 | 13 | public static string ComputeSignature(this string rawData, ECPrivKey privKey) 14 | { 15 | var bytes = rawData.ComputeSha256Hash(); 16 | var buf = new byte[64]; 17 | privKey.SignBIP340(bytes).WriteToSpan(buf); 18 | return Convert.ToHexString(buf).ToLower(); 19 | } 20 | 21 | public static byte[] ComputeSha256Hash(this string rawData) 22 | { 23 | // Create a SHA256 24 | using var sha256Hash = System.Security.Cryptography.SHA256.Create(); 25 | // ComputeHash - returns byte array 26 | return sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData)); 27 | } 28 | 29 | public static string ToHex(this byte[] bytes) 30 | { 31 | return Convert.ToHexString(bytes).ToLower(); 32 | } 33 | 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/MacCatalyst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UIRequiredDeviceCapabilities 11 | 12 | arm64 13 | 14 | UISupportedInterfaceOrientations 15 | 16 | UIInterfaceOrientationPortrait 17 | UIInterfaceOrientationLandscapeLeft 18 | UIInterfaceOrientationLandscapeRight 19 | 20 | UISupportedInterfaceOrientations~ipad 21 | 22 | UIInterfaceOrientationPortrait 23 | UIInterfaceOrientationPortraitUpsideDown 24 | UIInterfaceOrientationLandscapeLeft 25 | UIInterfaceOrientationLandscapeRight 26 | 27 | XSAppIconAssets 28 | Assets.xcassets/appicon.appiconset 29 | 30 | 31 | -------------------------------------------------------------------------------- /NNostr.Client/JsonConverters/UnixTimestampSecondsJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace NNostr.Client.JsonConverters 4 | { 5 | public class UnixTimestampSecondsJsonConverter : JsonConverter 6 | { 7 | public override DateTimeOffset? ReadJson(JsonReader reader, Type objectType, DateTimeOffset? existingValue, bool hasExistingValue, JsonSerializer serializer) 8 | { 9 | if (reader.TokenType == JsonToken.Null) 10 | { 11 | return null; 12 | } 13 | 14 | if (reader.TokenType != JsonToken.Integer) 15 | { 16 | throw new JsonException("datetime was not in number format"); 17 | } 18 | 19 | return DateTimeOffset.FromUnixTimeSeconds((long)reader.Value); 20 | } 21 | 22 | public override void WriteJson(JsonWriter writer, DateTimeOffset? value, JsonSerializer serializer) 23 | { 24 | if (value is null) 25 | { 26 | writer.WriteNull(); 27 | } 28 | else 29 | { 30 | writer.WriteValue(value.Value.ToUnixTimeSeconds()); 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSRequiresIPhoneOS 6 | 7 | UIDeviceFamily 8 | 9 | 1 10 | 2 11 | 12 | UIRequiredDeviceCapabilities 13 | 14 | arm64 15 | 16 | UISupportedInterfaceOrientations 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationLandscapeLeft 20 | UIInterfaceOrientationLandscapeRight 21 | 22 | UISupportedInterfaceOrientations~ipad 23 | 24 | UIInterfaceOrientationPortrait 25 | UIInterfaceOrientationPortraitUpsideDown 26 | UIInterfaceOrientationLandscapeLeft 27 | UIInterfaceOrientationLandscapeRight 28 | 29 | XSAppIconAssets 30 | Assets.xcassets/appicon.appiconset 31 | 32 | 33 | -------------------------------------------------------------------------------- /Nostrid/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Data; 2 | 3 | namespace Nostrid; 4 | 5 | public partial class App : Application 6 | { 7 | private readonly RelayService relayService; 8 | private readonly FeedService feedService; 9 | private readonly AccountService accountService; 10 | 11 | public App(RelayService relayService, FeedService feedService, AccountService accountService) 12 | { 13 | InitializeComponent(); 14 | 15 | MainPage = new MainPage(); 16 | this.relayService = relayService; 17 | this.feedService = feedService; 18 | this.accountService = accountService; 19 | } 20 | 21 | protected override Window CreateWindow(IActivationState activationState) 22 | { 23 | Window window = base.CreateWindow(activationState); 24 | 25 | window.Created += (s, e) => 26 | { 27 | relayService.StartNostrClients(); 28 | accountService.StartDetailsUpdater(); 29 | feedService.StartDetailsUpdater(); 30 | }; 31 | 32 | window.Destroying += (s, e) => 33 | { 34 | relayService.StopNostrClients(); 35 | accountService.StopDetailsUpdater(); 36 | feedService.StopDetailsUpdater(); 37 | }; 38 | 39 | return window; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Nostrid.Core/Migrations/20230209163414_RelayFilters.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Nostrid.Migrations 6 | { 7 | /// 8 | public partial class RelayFilters : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsPaid", 15 | table: "Relays", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: false); 19 | 20 | migrationBuilder.AddColumn( 21 | name: "MaxAutoRelays", 22 | table: "Configs", 23 | type: "INTEGER", 24 | nullable: false, 25 | defaultValue: 0); 26 | } 27 | 28 | /// 29 | protected override void Down(MigrationBuilder migrationBuilder) 30 | { 31 | migrationBuilder.DropColumn( 32 | name: "IsPaid", 33 | table: "Relays"); 34 | 35 | migrationBuilder.DropColumn( 36 | name: "MaxAutoRelays", 37 | table: "Configs"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Nostrid.Web/AesSubtleCrypto.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using NNostr.Client; 3 | using System.Security.Cryptography; 4 | 5 | internal class AesSubtleCrypto : IAesEncryptor 6 | { 7 | private readonly Lazy> moduleTask; 8 | 9 | public AesSubtleCrypto(IJSRuntime jsRuntime) 10 | { 11 | moduleTask = new(() => jsRuntime.InvokeAsync("import", "./scripts.js").AsTask()); 12 | } 13 | public async Task Decrypt(string cipherText, string iv, byte[] key) 14 | { 15 | var module = await moduleTask.Value; 16 | var cipherTextBytes = Convert.FromBase64String(cipherText); 17 | var ivBytes = Convert.FromBase64String(iv); 18 | var ret = await module.InvokeAsync("decryptAes", cipherTextBytes, key, ivBytes); 19 | return ret; 20 | } 21 | 22 | public async Task<(string cipherText, string iv)> Encrypt(string plainText, byte[] key) 23 | { 24 | var module = await moduleTask.Value; 25 | var ivBytes = new byte[16]; 26 | RandomNumberGenerator.Create().GetBytes(ivBytes); 27 | var ret = await module.InvokeAsync("encryptAes", plainText, key, ivBytes); 28 | return (Convert.ToBase64String(ret), Convert.ToBase64String(ivBytes)); 29 | } 30 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/TagSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | public class TagSubscriptionFilter : SubscriptionFilter 8 | { 9 | private readonly string[] tags; 10 | 11 | public TagSubscriptionFilter(string tag) : this(new[] { tag }) 12 | { 13 | } 14 | 15 | public TagSubscriptionFilter(string[] tags) 16 | { 17 | this.tags = tags; 18 | } 19 | 20 | public override NostrSubscriptionFilter[] GetFilters() 21 | { 22 | return new[] { 23 | new NostrSubscriptionFilter(NostrNip.NostrNipGenericTag) { ExtensionData = new Dictionary(){["#t"] = ConvertStringArrayToJsonElement(tags) }, 24 | Kinds = new[]{ NostrKind.Text }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 25 | new NostrSubscriptionFilter(NostrNip.NostrNipSearch) { Search = string.Join(" ", tags), 26 | Kinds = new[]{ NostrKind.Text }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 27 | }; 28 | } 29 | 30 | public override SubscriptionFilter Clone() 31 | { 32 | return new TagSubscriptionFilter(tags); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/ModalContainer.razor: -------------------------------------------------------------------------------- 1 | @implements IAsyncDisposable 2 | 3 | @code { 4 | [Parameter, EditorRequired] 5 | public RenderFragment ChildContent { get; set; } = null!; 6 | 7 | [CascadingParameter] 8 | public Scripts? Scripts { get; set; } 9 | } 10 | 11 |
12 | @ChildContent 13 |
14 | 15 | @code { 16 | 17 | private ElementReference container; 18 | 19 | public void Show() 20 | { 21 | Scripts?.InvokeVoid("showModal", container); 22 | } 23 | 24 | public void Hide() 25 | { 26 | Scripts?.InvokeVoid("hideModal", container); 27 | } 28 | 29 | public bool IsVisible() 30 | { 31 | return Scripts?.Invoke("isModalShown", container) ?? false; 32 | } 33 | 34 | #region Dispose 35 | private bool _disposed; 36 | 37 | async ValueTask IAsyncDisposable.DisposeAsync() => await Dispose(true); 38 | 39 | protected virtual async Task Dispose(bool disposing) 40 | { 41 | if (!_disposed) 42 | { 43 | if (disposing) 44 | { 45 | if (Scripts is not null) 46 | { 47 | await Scripts.InvokeVoidAsync("hideModal", container); 48 | } 49 | } 50 | 51 | _disposed = true; 52 | } 53 | } 54 | #endregion 55 | } 56 | -------------------------------------------------------------------------------- /Nostrid.Core/Migrations/20230126090204_AddAccountLastDetailsReceived.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Nostrid.Migrations 6 | { 7 | /// 8 | public partial class AddAccountLastDetailsReceived : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "DetailsLastReceived", 15 | table: "AccountDetails", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: 0L); 19 | 20 | migrationBuilder.CreateIndex( 21 | name: "IX_AccountDetails_Id_DetailsLastReceived", 22 | table: "AccountDetails", 23 | columns: new[] { "Id", "DetailsLastReceived" }); 24 | } 25 | 26 | /// 27 | protected override void Down(MigrationBuilder migrationBuilder) 28 | { 29 | migrationBuilder.DropIndex( 30 | name: "IX_AccountDetails_Id_DetailsLastReceived", 31 | table: "AccountDetails"); 32 | 33 | migrationBuilder.DropColumn( 34 | name: "DetailsLastReceived", 35 | table: "AccountDetails"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Nostrid.Core/Pages/ChannelViewer.razor: -------------------------------------------------------------------------------- 1 | @page "/channel/{id}" 2 | 3 | @code { 4 | [Parameter, EditorRequired] 5 | public string Id { get; set; } = null!; 6 | } 7 | 8 | @using Nostrid.Data.Relays; 9 | @using Nostrid.Data; 10 | @using Nostrid.Interfaces; 11 | @using Nostrid.Model; 12 | 13 | @inject IClipboardService clipboardService 14 | @inject ChannelService channelService 15 | 16 |
17 |
@channel?.Details?.Name
18 | @*
19 | @Id 20 | 24 |
*@ 25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 | @code { 34 | private SubscriptionFilter? filter; 35 | private Channel? channel; 36 | 37 | protected override void OnParametersSet() 38 | { 39 | channel = channelService.GetChannel(Id); 40 | filter = new ChannelSubscriptionFilter(Id); 41 | } 42 | } -------------------------------------------------------------------------------- /NNostr.Client/NostrEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NNostr.Client.JsonConverters; 3 | 4 | namespace NNostr.Client 5 | { 6 | public class NostrEvent: IEqualityComparer 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | [JsonProperty("pubkey")] 12 | public string PublicKey { get; set; } 13 | 14 | [JsonProperty("created_at")] 15 | [JsonConverter(typeof(UnixTimestampSecondsJsonConverter))] 16 | public DateTimeOffset? CreatedAt { get; set; } 17 | 18 | [JsonProperty("kind")] 19 | public int Kind { get; set; } 20 | 21 | [JsonProperty("content")] 22 | public string Content { get; set; } 23 | 24 | [JsonProperty("tags")] 25 | public List Tags { get; set; } 26 | 27 | [JsonProperty("sig")] 28 | public string Signature { get; set; } 29 | 30 | [JsonIgnore] 31 | public bool Deleted { get; set; } 32 | 33 | public bool Equals(NostrEvent? x, NostrEvent? y) 34 | { 35 | if (ReferenceEquals(x, y)) return true; 36 | if (x?.GetType() != y?.GetType()) return false; 37 | return x?.Id == y?.Id; 38 | } 39 | 40 | public int GetHashCode(NostrEvent obj) 41 | { 42 | return obj.Id.GetHashCode(); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Nostrid/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | #4356AE 12 | White 13 | 14 | 18 | 19 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/TimeStamp.razor: -------------------------------------------------------------------------------- 1 | @code{ 2 | [Parameter, EditorRequired] 3 | public long DateSeconds { get; set; } 4 | } 5 | 6 | @implements IDisposable 7 | 8 | @using Nostrid.Misc; 9 | 10 | @inject NotificationService notificationService 11 | 12 | @formattedTimeStamp 13 | 14 | @code 15 | { 16 | private string? formattedTimeStamp; 17 | 18 | protected override void OnParametersSet() 19 | { 20 | formattedTimeStamp = Utils.FormatDate(DateSeconds); 21 | notificationService.TimerPeriod += TimerPeriod; 22 | } 23 | 24 | private void TimerPeriod(object? sender, EventArgs _) 25 | { 26 | var newFormattedTimeStamp = Utils.FormatDate(DateSeconds); 27 | if (newFormattedTimeStamp != formattedTimeStamp) 28 | { 29 | formattedTimeStamp = newFormattedTimeStamp; 30 | InvokeAsync(StateHasChanged); 31 | } 32 | } 33 | 34 | #region Dispose 35 | private bool _disposed; 36 | 37 | public void Dispose() => Dispose(true); 38 | 39 | protected virtual void Dispose(bool disposing) 40 | { 41 | if (!_disposed) 42 | { 43 | if (disposing) 44 | { 45 | Cleanup(); 46 | } 47 | 48 | _disposed = true; 49 | } 50 | } 51 | 52 | private void Cleanup() 53 | { 54 | notificationService.TimerPeriod -= TimerPeriod; 55 | } 56 | #endregion 57 | } -------------------------------------------------------------------------------- /Nostrid.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:22311", 7 | "sslPort": 44397 8 | } 9 | }, 10 | "profiles": { 11 | "http": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "http://localhost:5215", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 26 | "applicationUrl": "https://localhost:7071;http://localhost:5215", 27 | "environmentVariables": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | } 30 | }, 31 | "IIS Express": { 32 | "commandName": "IISExpress", 33 | "launchBrowser": true, 34 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 35 | "environmentVariables": { 36 | "ASPNETCORE_ENVIRONMENT": "Development" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/ChannelName.razor: -------------------------------------------------------------------------------- 1 | @code { 2 | [Parameter, EditorRequired] 3 | public string Id { get; set; } = null!; 4 | } 5 | 6 | @implements IDisposable 7 | 8 | @using Nostrid.Data; 9 | @using Nostrid.Misc; 10 | @using Nostrid.Model; 11 | 12 | @inject ChannelService _channelService 13 | 14 | @_details?.Name 15 | 16 | @code 17 | { 18 | private ChannelDetails? _details; 19 | 20 | protected override void OnParametersSet() 21 | { 22 | base.OnParametersSet(); 23 | 24 | _details = _channelService.GetChannel(Id)?.Details; 25 | _channelService.ChannelDetailsChanged += ChannelDetailsChanged; 26 | } 27 | 28 | private void ChannelDetailsChanged(object? sender, (string channelId, ChannelDetails details) data) 29 | { 30 | if (data.channelId == Id) 31 | { 32 | _details = data.details; 33 | InvokeAsync(() => StateHasChanged()); 34 | } 35 | } 36 | 37 | #region Dispose 38 | private bool _disposed; 39 | 40 | public void Dispose() => Dispose(true); 41 | 42 | protected virtual void Dispose(bool disposing) 43 | { 44 | if (!_disposed) 45 | { 46 | if (disposing) 47 | { 48 | Cleanup(); 49 | } 50 | 51 | _disposed = true; 52 | } 53 | } 54 | 55 | private void Cleanup() 56 | { 57 | _channelService.ChannelDetailsChanged -= ChannelDetailsChanged; 58 | } 59 | #endregion 60 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/ReplaceableEventSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | public class ReplaceableEventSubscriptionFilter : SubscriptionFilter 8 | { 9 | private readonly string[] rids; 10 | 11 | public ReplaceableEventSubscriptionFilter(string rid) : this(new[] { rid }) 12 | { 13 | } 14 | 15 | public ReplaceableEventSubscriptionFilter(string[] rids) 16 | { 17 | this.rids = rids; 18 | } 19 | 20 | public override NostrSubscriptionFilter[] GetFilters() 21 | { 22 | return rids 23 | .Select(EventExtension.ExplodeReplaceableId) 24 | .Where(naddr => naddr.HasValue) 25 | .Select(naddr => 26 | new NostrSubscriptionFilter(NostrNip.NostrNipGenericTag) 27 | { 28 | Kinds = new[] { naddr!.Value.kind }, 29 | Authors = new[] { naddr!.Value.pubkey }, 30 | ExtensionData = new Dictionary() { ["#d"] = ConvertStringArrayToJsonElement(naddr!.Value.dstr) } 31 | }) 32 | .Union(rids 33 | .Select(naddr => 34 | new NostrSubscriptionFilter(NostrNip.NostrNipGenericTag) 35 | { 36 | ExtensionData = new Dictionary() { ["#a"] = ConvertStringArrayToJsonElement(naddr) } 37 | }) 38 | ) 39 | .ToArray(); 40 | } 41 | 42 | public override SubscriptionFilter Clone() 43 | { 44 | return new ReplaceableEventSubscriptionFilter(rids); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Nostrid.Core/Pages/HashtagFeed.razor: -------------------------------------------------------------------------------- 1 | @page "/feed/tag/{hashtags}" 2 | 3 | @code { 4 | [Parameter, EditorRequired] 5 | public string Hashtags { get; set; } = null!; 6 | } 7 | 8 | @using Nostrid.Data; 9 | @using Nostrid.Data.Relays; 10 | @using Nostrid.Model; 11 | @inject AccountService accountService 12 | @inject FeedService feedService 13 | @inject NavigationManager navigationManager 14 | 15 |
16 |

#@string.Join(" #", Hashtags.Split(','))

17 | @if (accountService.MainAccount != null) 18 | { 19 | 22 | } 23 |
24 | 25 | 26 | 27 | @code { 28 | private SubscriptionFilter? filter; 29 | 30 | protected override void OnParametersSet() 31 | { 32 | var hashtags = Hashtags.Split(","); 33 | filter = new TagSubscriptionFilter(hashtags); 34 | } 35 | 36 | private void Save() 37 | { 38 | if (accountService.MainAccount == null) return; 39 | var feedSource = new FeedSource() 40 | { 41 | Hashtags = Hashtags.Split(",").ToList(), 42 | OwnerId = accountService.MainAccount.Id, 43 | }; 44 | feedService.SaveFeedSource(feedSource); 45 | navigationManager.NavigateTo($"/feed/{feedSource.Id}"); 46 | } 47 | } -------------------------------------------------------------------------------- /Nostrid.Core/Shared/ChannelMessageCount.razor: -------------------------------------------------------------------------------- 1 | @code { 2 | [Parameter, EditorRequired] 3 | public string Id { get; set; } = null!; 4 | } 5 | 6 | @implements IDisposable 7 | 8 | @using Nostrid.Data; 9 | @using Nostrid.Misc; 10 | @using Nostrid.Model; 11 | 12 | @inject ChannelService _channelService 13 | 14 | @_messageCount 15 | 16 | @code 17 | { 18 | private int _messageCount = 0; 19 | 20 | protected override void OnParametersSet() 21 | { 22 | base.OnParametersSet(); 23 | 24 | _messageCount = _channelService.GetChannelMessagesInDb(Id); 25 | _channelService.ChannelReceivedMessage += ChannelReceivedMessage; 26 | } 27 | 28 | private void ChannelReceivedMessage(object? sender, string channelId) 29 | { 30 | if (channelId == Id) 31 | { 32 | _messageCount = _channelService.GetChannelMessagesInDb(channelId); 33 | InvokeAsync(() => StateHasChanged()); 34 | } 35 | } 36 | 37 | #region Dispose 38 | private bool _disposed; 39 | 40 | public void Dispose() => Dispose(true); 41 | 42 | protected virtual void Dispose(bool disposing) 43 | { 44 | if (!_disposed) 45 | { 46 | if (disposing) 47 | { 48 | Cleanup(); 49 | } 50 | 51 | _disposed = true; 52 | } 53 | } 54 | 55 | private void Cleanup() 56 | { 57 | _channelService.ChannelReceivedMessage -= ChannelReceivedMessage; 58 | } 59 | #endregion 60 | } -------------------------------------------------------------------------------- /Nostrid.Core/Migrations/20230209075431_ProxyConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Nostrid.Migrations 6 | { 7 | /// 8 | public partial class ProxyConfig : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "ProxyPassword", 15 | table: "Configs", 16 | type: "TEXT", 17 | nullable: true); 18 | 19 | migrationBuilder.AddColumn( 20 | name: "ProxyUri", 21 | table: "Configs", 22 | type: "TEXT", 23 | nullable: true); 24 | 25 | migrationBuilder.AddColumn( 26 | name: "ProxyUser", 27 | table: "Configs", 28 | type: "TEXT", 29 | nullable: true); 30 | } 31 | 32 | /// 33 | protected override void Down(MigrationBuilder migrationBuilder) 34 | { 35 | migrationBuilder.DropColumn( 36 | name: "ProxyPassword", 37 | table: "Configs"); 38 | 39 | migrationBuilder.DropColumn( 40 | name: "ProxyUri", 41 | table: "Configs"); 42 | 43 | migrationBuilder.DropColumn( 44 | name: "ProxyUser", 45 | table: "Configs"); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/Database/AccountDetails.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Nostrid.Model; 5 | 6 | public class AccountDetails 7 | { 8 | [JsonIgnore] 9 | [ForeignKey("Account")] 10 | public string Id { get; set; } 11 | 12 | [JsonIgnore] 13 | public Account Account { get; set; } 14 | 15 | #region Json 16 | [JsonPropertyName("name")] 17 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 18 | public string? Name { get; set; } 19 | 20 | [JsonPropertyName("about")] 21 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 22 | public string? About { get; set; } 23 | 24 | [JsonPropertyName("picture")] 25 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 26 | public string? PictureUrl { get; set; } 27 | 28 | [JsonPropertyName("nip05")] 29 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 30 | public string? Nip05Id { get; set; } 31 | 32 | [JsonPropertyName("lud16")] 33 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 34 | public string? Lud16Id { get; set; } 35 | 36 | [JsonPropertyName("lud06")] 37 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 38 | public string? Lud06Url { get; set; } 39 | #endregion 40 | 41 | [JsonIgnore] 42 | public bool Nip05Valid { get; set; } 43 | 44 | [JsonIgnore] 45 | public DateTime DetailsLastUpdate { get; set; } 46 | 47 | [JsonIgnore] 48 | public long DetailsLastReceived { get; set; } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Nostrid.Core/Pages/SavedFeed.razor: -------------------------------------------------------------------------------- 1 | @page "/feed/{id}" 2 | 3 | @code { 4 | [Parameter, EditorRequired] 5 | public string Id { get; set; } = null!; 6 | } 7 | 8 | @using Nostrid.Data 9 | @using Nostrid.Data.Relays; 10 | @using Nostrid.Model; 11 | @using Nostrid.Misc; 12 | @inject AccountService accountService 13 | @inject FeedService feedService 14 | @inject NavigationManager navigationManager 15 | 16 |
17 |

18 | @if (hashtags != null) 19 | { 20 | #@string.Join(" #", hashtags) 21 | } 22 |

23 | 26 |
27 | 28 | 29 | 30 | @code { 31 | private SubscriptionFilter? filter; 32 | private string[]? hashtags; 33 | private long id; 34 | 35 | protected override void OnParametersSet() 36 | { 37 | id = long.Parse(Id); 38 | if (accountService.MainAccount != null) 39 | { 40 | FeedSource feedSource = feedService.GetFeedSource(id); 41 | if (feedSource != null && feedSource.OwnerId == accountService.MainAccount.Id) 42 | { 43 | hashtags = feedSource.Hashtags.ToArray(); 44 | filter = new TagSubscriptionFilter(hashtags); 45 | } 46 | } 47 | } 48 | 49 | private void Delete() 50 | { 51 | feedService.DeleteFeedSource(id); 52 | navigationManager.NavigateTo($"/"); 53 | } 54 | } -------------------------------------------------------------------------------- /NNostr.Client/JsonConverters/NostrEventTagJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace NNostr.Client.JsonConverters 4 | { 5 | public class NostrEventTagJsonConverter : JsonConverter 6 | { 7 | public override NostrEventTag? ReadJson(JsonReader reader, Type objectType, NostrEventTag? existingValue, bool hasExistingValue, JsonSerializer serializer) 8 | { 9 | var result = new NostrEventTag(); 10 | if (reader.TokenType != JsonToken.StartArray) 11 | { 12 | throw new Exception("Nostr Event Tags are an array"); 13 | } 14 | 15 | reader.Read(); 16 | var i = 0; 17 | while (reader.TokenType != JsonToken.EndArray) 18 | { 19 | if (i == 0) 20 | { 21 | result.TagIdentifier = reader.Value.ToString(); 22 | } 23 | else 24 | { 25 | result.Data.Add(reader.Value.ToString()); 26 | } 27 | 28 | reader.Read(); 29 | i++; 30 | } 31 | 32 | return result; 33 | } 34 | 35 | public override void WriteJson(JsonWriter writer, NostrEventTag? value, JsonSerializer serializer) 36 | { 37 | if (value is null) 38 | { 39 | writer.WriteNull(); 40 | return; 41 | } 42 | 43 | writer.WriteStartArray(); 44 | writer.WriteValue(value.TagIdentifier); 45 | value.Data?.ForEach(writer.WriteValue); 46 | 47 | writer.WriteEndArray(); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/SubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | using Newtonsoft.Json.Linq; 5 | using Newtonsoft.Json; 6 | 7 | namespace Nostrid.Data.Relays; 8 | 9 | public abstract class SubscriptionFilter 10 | { 11 | public string Id { get; private set; } 12 | 13 | public LimitFilterData LimitFilterData { get; } = new(); 14 | 15 | /// 16 | /// Destroys the whole filter when one event is received 17 | /// 18 | public bool DestroyOnFirstEvent { get; set; } 19 | 20 | /// 21 | /// Destroy the subscription (not the whole filter) when EOSE is received 22 | /// 23 | public bool DestroyOnEose { get; set; } 24 | 25 | /// 26 | /// Destroys the whole filter at a given moment 27 | /// 28 | public DateTimeOffset? DestroyOn { get; set; } 29 | 30 | /// 31 | /// Event is not stored in DB 32 | /// 33 | public bool DontSaveInLocalCache { get; set; } 34 | 35 | /// 36 | /// Method that will be notified when one or more events are received 37 | /// 38 | public Action>? Handler { get; set; } 39 | 40 | protected SubscriptionFilter() 41 | { 42 | Id = IdGenerator.Generate(); 43 | } 44 | 45 | public abstract NostrSubscriptionFilter[] GetFilters(); 46 | 47 | public abstract SubscriptionFilter Clone(); 48 | 49 | protected static JToken ConvertStringArrayToJsonElement(params string[] array) 50 | { 51 | string jsonString = JsonConvert.SerializeObject(array); 52 | return JToken.Parse(jsonString).Root; 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/AccountAbout.razor: -------------------------------------------------------------------------------- 1 | @code{ 2 | [Parameter, EditorRequired] 3 | public string Id { get; set; } = null!; 4 | } 5 | 6 | @implements IDisposable 7 | 8 | @using Nostrid.Data; 9 | @using Nostrid.Model; 10 | 11 | @inject AccountService accountService 12 | 13 | @if (details?.About.IsNotNullOrEmpty() ?? false) 14 | { 15 |
@details?.About
16 | } 17 | 18 | @code 19 | { 20 | private AccountDetails? details; 21 | 22 | protected override void OnInitialized() 23 | { 24 | accountService.AccountDetailsChanged += AccountDetailsChanged; 25 | } 26 | 27 | protected override void OnParametersSet() 28 | { 29 | if (Id == null) return; 30 | accountService.AddDetailsNeeded(Id); 31 | details = accountService.GetAccountDetails(Id); 32 | } 33 | 34 | private void AccountDetailsChanged(object? sender, (string accountId, AccountDetails details) data) 35 | { 36 | if (data.accountId == Id) 37 | { 38 | details = data.details; 39 | InvokeAsync(() => StateHasChanged()); 40 | } 41 | } 42 | 43 | #region Dispose 44 | private bool disposed; 45 | 46 | public void Dispose() => Dispose(true); 47 | 48 | protected virtual void Dispose(bool disposing) 49 | { 50 | if (!disposed) 51 | { 52 | if (disposing) 53 | { 54 | Cleanup(); 55 | } 56 | 57 | disposed = true; 58 | } 59 | } 60 | 61 | private void Cleanup() 62 | { 63 | accountService.AccountDetailsChanged -= AccountDetailsChanged; 64 | } 65 | #endregion 66 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/AccountDetailsSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using NNostr.Client; 3 | using System; 4 | using Nostrid.Model; 5 | 6 | namespace Nostrid.Data.Relays; 7 | 8 | public class AccountDetailsSubscriptionFilter : SubscriptionFilter 9 | { 10 | private readonly string[] ids; 11 | 12 | public AccountDetailsSubscriptionFilter(string id) : this(new[] { id }) 13 | { 14 | } 15 | 16 | public AccountDetailsSubscriptionFilter(string[] ids) 17 | { 18 | this.ids = ids; 19 | } 20 | 21 | public override NostrSubscriptionFilter[] GetFilters() 22 | { 23 | return new[] { 24 | new NostrSubscriptionFilter() { Authors = ids, Kinds = new[]{ NostrKind.Metadata }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until } 25 | }; 26 | } 27 | 28 | public static List CreateInBatch(string[] ids, int batchSize = 150, bool destroyOnEose = false, DateTimeOffset? destroyOn = null) 29 | { 30 | int index = 0; 31 | var segment = new ArraySegment(ids); 32 | var ret = new List(); 33 | while (index < ids.Length) 34 | { 35 | ret.Add(new AccountDetailsSubscriptionFilter(segment.Slice(index, Math.Min(batchSize, ids.Length - index)).ToArray()) 36 | { 37 | DestroyOnEose = destroyOnEose, 38 | DestroyOn = destroyOn, 39 | }); 40 | index += batchSize; 41 | } 42 | return ret; 43 | } 44 | 45 | public override SubscriptionFilter Clone() 46 | { 47 | return new AccountDetailsSubscriptionFilter(ids); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Nostrid.Core/Externals/NostrBuildMediaService.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostrid.Misc; 3 | using System.Net.Http.Headers; 4 | 5 | namespace Nostrid.Externals 6 | { 7 | public class NostrBuildMediaService : IMediaService 8 | { 9 | public string Name => "nostr.build"; 10 | 11 | public int MaxSize { get => 50 * 1024 * 1024; } 12 | 13 | public async Task UploadFile(Stream data, string filename, string mimeType, Action progress) 14 | { 15 | using var httpClient = new HttpClient(); 16 | using var progressStream = new ProgressStream(data); 17 | progressStream.UpdateProgress += (s, e) => progress(e); 18 | using var httpContent = new MultipartFormDataContent(); 19 | using var fileContent = new StreamContent(progressStream); 20 | fileContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); 21 | httpContent.Add(fileContent, "fileToUpload", filename); 22 | httpContent.Add(new StringContent("Upload Image"), "submit"); 23 | var response = await httpClient.PostAsync("https://nostr.build/api/upload/nostrid.php", httpContent); 24 | if (response.IsSuccessStatusCode) 25 | { 26 | var responseText = await response.Content.ReadAsStringAsync(); 27 | if (responseText.IsNotNullOrEmpty()) 28 | { 29 | var link = JsonConvert.DeserializeObject(responseText); 30 | if (Uri.IsWellFormedUriString(link, UriKind.Absolute)) 31 | { 32 | return new Uri(link); 33 | } 34 | } 35 | } 36 | return null; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/Subscription.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | using Nostrid.Misc; 3 | 4 | namespace Nostrid.Data.Relays; 5 | 6 | public class Subscription : IDisposable 7 | { 8 | private readonly NostrClient client; 9 | private readonly NostrSubscriptionFilter[] nostrFilters; 10 | private readonly string filterId; 11 | private readonly string id; 12 | 13 | private bool subscribed; 14 | private bool disposedValue; 15 | 16 | public string SubscriptionId => id; 17 | public string FilterId => filterId; 18 | 19 | public Subscription(NostrClient client, NostrSubscriptionFilter[] nostrFilters, string filterId) 20 | { 21 | this.client = client; 22 | this.nostrFilters = nostrFilters; 23 | this.filterId = filterId; 24 | id = IdGenerator.Generate(); 25 | } 26 | 27 | public async void Subscribe() 28 | { 29 | await client.CreateSubscription(SubscriptionId, nostrFilters); 30 | subscribed = true; 31 | } 32 | 33 | public async void Unsubscribe() 34 | { 35 | if (subscribed) 36 | { 37 | await client.CloseSubscription(SubscriptionId); 38 | subscribed = false; 39 | } 40 | } 41 | 42 | protected virtual void Dispose(bool disposing) 43 | { 44 | if (!disposedValue) 45 | { 46 | if (disposing) 47 | { 48 | Unsubscribe(); 49 | } 50 | 51 | // TODO: set large fields to null 52 | disposedValue = true; 53 | } 54 | } 55 | 56 | public void Dispose() 57 | { 58 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 59 | Dispose(disposing: true); 60 | GC.SuppressFinalize(this); 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/UnreadDmCounter.razor: -------------------------------------------------------------------------------- 1 | @code { 2 | [Parameter, EditorRequired] 3 | public string AccountId { get; set; } 4 | 5 | [Parameter, EditorRequired] 6 | public string OtherAccountId { get; set; } 7 | 8 | [Parameter, EditorRequired] 9 | public RenderFragment ChildContent { get; set; } 10 | } 11 | 12 | @implements IDisposable 13 | 14 | @using Nostrid.Data; 15 | @using Nostrid.Misc; 16 | @using Nostrid.Model; 17 | 18 | @inject DmService dmService 19 | 20 | @if (unreadCounter > 0) 21 | { 22 | @ChildContent(unreadCounter) 23 | } 24 | 25 | @code 26 | { 27 | private int unreadCounter = 0; 28 | 29 | protected override void OnParametersSet() 30 | { 31 | base.OnParametersSet(); 32 | 33 | unreadCounter = dmService.GetUnreadCount(AccountId, OtherAccountId); 34 | dmService.NewDm += NewDm; 35 | } 36 | 37 | private void NewDm(object? sender, (string senderId, string receiverId) data) 38 | { 39 | if ((data.senderId == AccountId && data.receiverId == OtherAccountId) || (data.senderId == OtherAccountId && data.receiverId == AccountId)) 40 | { 41 | unreadCounter = dmService.GetUnreadCount(AccountId, OtherAccountId); 42 | InvokeAsync(() => StateHasChanged()); 43 | } 44 | } 45 | 46 | #region Dispose 47 | private bool _disposed; 48 | 49 | public void Dispose() => Dispose(true); 50 | 51 | protected virtual void Dispose(bool disposing) 52 | { 53 | if (!_disposed) 54 | { 55 | if (disposing) 56 | { 57 | Cleanup(); 58 | } 59 | 60 | _disposed = true; 61 | } 62 | } 63 | 64 | private void Cleanup() 65 | { 66 | dmService.NewDm -= NewDm; 67 | } 68 | #endregion 69 | } -------------------------------------------------------------------------------- /Nostrid/Resources/AppIcon/appiconfg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NNostr.Client/NostrSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using NNostr.Client.JsonConverters; 4 | 5 | namespace NNostr.Client 6 | { 7 | public class NostrSubscriptionFilter 8 | { 9 | public NostrSubscriptionFilter(params int[] requiredNips) 10 | { 11 | RequiredNips = new(requiredNips); 12 | } 13 | 14 | [JsonIgnore] public List RequiredNips { get; set; } 15 | 16 | [JsonProperty("ids")] public string[]? Ids { get; set; } 17 | [JsonProperty("authors")] public string[]? Authors { get; set; } 18 | [JsonProperty("kinds")] public int[]? Kinds { get; set; } 19 | [JsonProperty("#e")] public string[]? EventId { get; set; } 20 | [JsonProperty("#p")] public string[]? PublicKey { get; set; } 21 | [JsonProperty("since")][JsonConverter(typeof(UnixTimestampSecondsJsonConverter))] public DateTimeOffset? Since { get; set; } 22 | [JsonProperty("until")][JsonConverter(typeof(UnixTimestampSecondsJsonConverter))] public DateTimeOffset? Until { get; set; } 23 | [JsonProperty("limit")] public int? Limit { get; set; } 24 | [JsonProperty("search")] public string? Search { get; set; } 25 | 26 | [JsonExtensionData] 27 | public IDictionary? ExtensionData { get; set; } 28 | 29 | public Dictionary GetAdditionalTagFilters() 30 | { 31 | var tagFilters = ExtensionData?.Where(pair => pair.Key.StartsWith("#") && pair.Value.Type == JTokenType.Array); 32 | return tagFilters?.ToDictionary(tagFilter => tagFilter.Key.Substring(1), 33 | tagFilter => tagFilter.Value.Select(element => element.Value()) 34 | .ToArray())! ?? new Dictionary(); 35 | } 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/ChannelSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using NNostr.Client; 3 | using Nostrid.Model; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | /// 8 | /// Returns all events for a given channel Id 9 | /// 10 | public class ChannelSubscriptionFilter : SubscriptionFilter 11 | { 12 | private readonly string[] ids; 13 | 14 | public ChannelSubscriptionFilter(string id) : this(new[] {id }) 15 | { 16 | } 17 | 18 | public ChannelSubscriptionFilter(string[] ids) 19 | { 20 | this.ids = ids; 21 | } 22 | 23 | public override NostrSubscriptionFilter[] GetFilters() 24 | { 25 | return new[] { 26 | new NostrSubscriptionFilter() { EventId = ids, Kinds = new[]{ NostrKind.ChannelMessage }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 27 | }; 28 | } 29 | 30 | public static List CreateInBatch(string[] ids, int batchSize = 150, int? limit = null, DateTimeOffset? destroyOn = null, bool destroyEose = false) 31 | { 32 | int index = 0; 33 | var segment = new ArraySegment(ids); 34 | var ret = new List(); 35 | while (index < ids.Length) 36 | { 37 | var filter = new ChannelSubscriptionFilter(segment.Slice(index, Math.Min(batchSize, ids.Length - index)).ToArray()); 38 | filter.LimitFilterData.Limit = limit; 39 | filter.DestroyOn = destroyOn; 40 | filter.DestroyOnEose = destroyEose; 41 | ret.Add(filter); 42 | index += batchSize; 43 | } 44 | return ret; 45 | } 46 | 47 | public override SubscriptionFilter Clone() 48 | { 49 | return new ChannelSubscriptionFilter(ids); 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/ElementObserver.razor: -------------------------------------------------------------------------------- 1 | @implements IAsyncDisposable 2 | 3 | @code{ 4 | [Parameter, EditorRequired] 5 | public EventHandler OnIntersecting { get; set; } = null!; 6 | 7 | [Parameter] 8 | public string Margin { get; set; } = "0px"; 9 | 10 | [CascadingParameter] 11 | public Scripts? Scripts { get; set; } 12 | } 13 | 14 | 15 | 16 | @code { 17 | private ElementReference element; 18 | private IJSObjectReference? observer; 19 | 20 | protected override void OnAfterRender(bool firstRender) 21 | { 22 | if (Scripts != null && firstRender) 23 | { 24 | Scripts.InvokeAfterRender(async () => 25 | { 26 | if (!_disposed) 27 | { 28 | observer = await Scripts.InvokeAsync( 29 | "createIntersectionObserver", element, DotNetObjectReference.Create(this), "Intersect", Margin); 30 | } 31 | }); 32 | } 33 | } 34 | 35 | [JSInvokable] 36 | public void Intersect(bool intersecting) 37 | { 38 | this.OnIntersecting?.Invoke(this, intersecting); 39 | } 40 | 41 | #region Dispose 42 | private bool _disposed; 43 | 44 | async ValueTask IAsyncDisposable.DisposeAsync() => await Dispose(true); 45 | 46 | protected virtual async Task Dispose(bool disposing) 47 | { 48 | if (!_disposed) 49 | { 50 | if (disposing) 51 | { 52 | if (observer != null) 53 | { 54 | await observer.InvokeVoidAsync("dispose"); 55 | await observer.DisposeAsync(); 56 | } 57 | } 58 | 59 | _disposed = true; 60 | } 61 | } 62 | #endregion 63 | } -------------------------------------------------------------------------------- /Nostrid.Core/Data/ConfigService.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using Nostrid.Model; 3 | using System.Net; 4 | 5 | namespace Nostrid.Data; 6 | 7 | public class ConfigService 8 | { 9 | private readonly EventDatabase eventDatabase; 10 | 11 | private readonly IWebProxy DefaultProxy; 12 | 13 | public ConfigService(EventDatabase eventDatabase) 14 | { 15 | this.eventDatabase = eventDatabase; 16 | DefaultProxy = HttpClient.DefaultProxy; 17 | ConfigureProxy(); 18 | } 19 | 20 | private Config mainConfig; 21 | public Config MainConfig 22 | { 23 | get 24 | { 25 | return mainConfig ??= eventDatabase.GetConfig(); 26 | } 27 | set 28 | { 29 | mainConfig = value; 30 | eventDatabase.SaveConfig(mainConfig); 31 | ConfigureProxy(); 32 | } 33 | } 34 | 35 | public void Save() 36 | { 37 | eventDatabase.SaveConfig(mainConfig); 38 | } 39 | 40 | private void ConfigureProxy() 41 | { 42 | if (Utils.IsWasm() || MainConfig.ProxyUri.IsNullOrEmpty()) 43 | { 44 | HttpClient.DefaultProxy = DefaultProxy; 45 | return; 46 | } 47 | 48 | try 49 | { 50 | NetworkCredential? credentials = null; 51 | if (MainConfig.ProxyUser.IsNotNullOrEmpty()) 52 | { 53 | credentials = new NetworkCredential(MainConfig.ProxyUser, MainConfig.ProxyPassword); 54 | } 55 | var webProxy = new WebProxy(new Uri(MainConfig.ProxyUri.Trim())) 56 | { 57 | Credentials = credentials 58 | }; 59 | 60 | HttpClient.DefaultProxy = webProxy; 61 | } 62 | catch 63 | { 64 | HttpClient.DefaultProxy = DefaultProxy; 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/Alert.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | 3 |
4 | @foreach (var alert in alerts) 5 | { 6 | 14 | } 15 |
16 | 17 | @code { 18 | private List<(string, string, DateTime)> alerts = new(); 19 | private Timer? timer; 20 | 21 | public enum Type 22 | { 23 | Primary, 24 | Secondary, 25 | Success, 26 | Warning, 27 | Danger, 28 | Info 29 | } 30 | 31 | protected override void OnInitialized() 32 | { 33 | base.OnInitialized(); 34 | timer = new Timer(new TimerCallback((_) => 35 | { 36 | var count = alerts.RemoveAll(m => m.Item3 < DateTime.UtcNow); 37 | if (count > 0) 38 | { 39 | InvokeAsync(() => StateHasChanged()); 40 | } 41 | }), null, 0, 1000); 42 | } 43 | 44 | public void Show(string text, Type type = Type.Info, int expiringSeconds = 5) 45 | { 46 | var message = (text, $"text-bg-{type.ToString().ToLower()}", DateTime.UtcNow.AddSeconds(expiringSeconds)); 47 | alerts.Add(message); 48 | InvokeAsync(() => StateHasChanged()); 49 | } 50 | 51 | #region Dispose 52 | private bool _disposed; 53 | 54 | public void Dispose() => Dispose(true); 55 | 56 | protected virtual void Dispose(bool disposing) 57 | { 58 | if (!_disposed) 59 | { 60 | if (disposing) 61 | { 62 | timer?.Dispose(); 63 | } 64 | 65 | _disposed = true; 66 | } 67 | } 68 | #endregion 69 | } -------------------------------------------------------------------------------- /Nostrid.Core/Migrations/20230128154625_ManualRelayManagement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Nostrid.Migrations 6 | { 7 | /// 8 | public partial class ManualRelayManagement : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "Read", 15 | table: "Relays", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: false); 19 | 20 | migrationBuilder.AddColumn( 21 | name: "Write", 22 | table: "Relays", 23 | type: "INTEGER", 24 | nullable: false, 25 | defaultValue: false); 26 | 27 | migrationBuilder.AddColumn( 28 | name: "ManualRelayManagement", 29 | table: "Configs", 30 | type: "INTEGER", 31 | nullable: false, 32 | defaultValue: false); 33 | 34 | // Set all reads and writes to true, and switch priority (greater number is now higher priority) 35 | migrationBuilder.Sql("UPDATE Relays SET Priority = 10 - Priority, Read = 1, Write = 1"); 36 | } 37 | 38 | /// 39 | protected override void Down(MigrationBuilder migrationBuilder) 40 | { 41 | migrationBuilder.DropColumn( 42 | name: "Read", 43 | table: "Relays"); 44 | 45 | migrationBuilder.DropColumn( 46 | name: "Write", 47 | table: "Relays"); 48 | 49 | migrationBuilder.DropColumn( 50 | name: "ManualRelayManagement", 51 | table: "Configs"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/MentionsFeed.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | 3 | @using Nostrid.Data 4 | @using Nostrid.Data.Relays; 5 | @using Nostrid.Model; 6 | @using System.Collections.Concurrent; 7 | 8 | @inject AccountService accountService 9 | @inject FeedService feedService 10 | @inject NavigationManager navigationManager 11 | 12 | 13 | 14 | @code { 15 | private SubscriptionFilter? filter; 16 | 17 | protected override void OnInitialized() 18 | { 19 | accountService.MainAccountChanged += MainAccountChanged; 20 | SetFilter(); 21 | } 22 | 23 | private void OnLoaded(IEnumerable notesFeed) 24 | { 25 | if (notesFeed.Any()) 26 | { 27 | var newestNoteDate = DateTimeOffset.FromUnixTimeSeconds(notesFeed.Max(n => n.Note.CreatedAtCurated)).UtcDateTime; 28 | accountService.SetLastRead(newestNoteDate); 29 | } 30 | } 31 | 32 | private void MainAccountChanged(object? sender, EventArgs args) 33 | { 34 | if (accountService.MainAccount == null) 35 | { 36 | navigationManager.NavigateTo("/feed"); 37 | return; 38 | } 39 | SetFilter(); 40 | InvokeAsync(() => StateHasChanged()); 41 | } 42 | 43 | private void SetFilter() 44 | { 45 | filter = new MentionSubscriptionFilter(accountService.MainAccount?.Id); 46 | } 47 | 48 | #region Dispose 49 | private bool disposed; 50 | 51 | public void Dispose() => Dispose(true); 52 | 53 | protected virtual void Dispose(bool disposing) 54 | { 55 | if (!disposed) 56 | { 57 | if (disposing) 58 | { 59 | accountService.MainAccountChanged -= MainAccountChanged; 60 | } 61 | 62 | disposed = true; 63 | } 64 | } 65 | #endregion 66 | } 67 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/PayButton.razor: -------------------------------------------------------------------------------- 1 | @code{ 2 | [Parameter, EditorRequired] 3 | public string Id { get; set; } = null!; 4 | 5 | [Parameter] 6 | public RenderFragment? ChildContent { get; set; } 7 | } 8 | 9 | @implements IDisposable 10 | 11 | @using Nostrid.Data; 12 | @using Nostrid.Misc; 13 | @using Nostrid.Model; 14 | 15 | @inject AccountService accountService 16 | 17 | @if (details != null && (details.Lud06Url.IsNotNullOrEmpty() || details.Lud16Id.IsNotNullOrEmpty())) 18 | { 19 | @ChildContent 20 | } 21 | 22 | @code 23 | { 24 | private AccountDetails? details; 25 | 26 | protected override void OnInitialized() 27 | { 28 | accountService.AccountDetailsChanged += AccountDetailsChanged; 29 | } 30 | 31 | protected override void OnParametersSet() 32 | { 33 | if (Id == null) return; 34 | accountService.AddDetailsNeeded(Id); 35 | details = accountService.GetAccountDetails(Id); 36 | } 37 | 38 | private void AccountDetailsChanged(object? sender, (string accountId, AccountDetails details) data) 39 | { 40 | if (data.accountId == Id) 41 | { 42 | details = data.details; 43 | InvokeAsync(() => StateHasChanged()); // TODO: this change will be ignored because of ShouldRender=false 44 | } 45 | } 46 | 47 | protected override bool ShouldRender() 48 | { 49 | return false; 50 | } 51 | 52 | #region Dispose 53 | private bool disposed; 54 | 55 | public void Dispose() => Dispose(true); 56 | 57 | protected virtual void Dispose(bool disposing) 58 | { 59 | if (!disposed) 60 | { 61 | if (disposing) 62 | { 63 | Cleanup(); 64 | } 65 | 66 | disposed = true; 67 | } 68 | } 69 | 70 | private void Cleanup() 71 | { 72 | accountService.AccountDetailsChanged -= AccountDetailsChanged; 73 | } 74 | #endregion 75 | } -------------------------------------------------------------------------------- /NNostr.Client/AesEncryptor.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Versioning; 2 | using System.Security.Cryptography; 3 | 4 | namespace NNostr.Client; 5 | public class AesEncryptor : IAesEncryptor 6 | { 7 | 8 | [UnsupportedOSPlatform("browser")] 9 | public Task<(string cipherText, string iv)> Encrypt(string plainText, byte[] key) 10 | { 11 | byte[] cipherData; 12 | Aes aes = Aes.Create(); 13 | aes.Key = key; 14 | aes.GenerateIV(); 15 | aes.Mode = CipherMode.CBC; 16 | ICryptoTransform cipher = aes.CreateEncryptor(aes.Key, aes.IV); 17 | 18 | using (MemoryStream ms = new MemoryStream()) 19 | { 20 | using (CryptoStream cs = new CryptoStream(ms, cipher, CryptoStreamMode.Write)) 21 | { 22 | using (StreamWriter sw = new StreamWriter(cs)) 23 | { 24 | sw.Write(plainText); 25 | } 26 | } 27 | 28 | cipherData = ms.ToArray(); 29 | } 30 | 31 | return Task.FromResult((Convert.ToBase64String(cipherData), Convert.ToBase64String(aes.IV))); 32 | } 33 | 34 | public Task Decrypt(string cipherText, string iv, byte[] key) 35 | { 36 | string plainText; 37 | Aes aes = Aes.Create(); 38 | aes.Key = key; 39 | aes.IV = Convert.FromBase64String(iv); 40 | aes.Mode = CipherMode.CBC; 41 | ICryptoTransform decipher = aes.CreateDecryptor(aes.Key, aes.IV); 42 | 43 | using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(cipherText))) 44 | { 45 | using (CryptoStream cs = new CryptoStream(ms, decipher, CryptoStreamMode.Read)) 46 | { 47 | using (StreamReader sr = new StreamReader(cs)) 48 | { 49 | plainText = sr.ReadToEnd(); 50 | } 51 | } 52 | 53 | return Task.FromResult(plainText); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Nostrid.Core/Shared/ChannelPicture.razor: -------------------------------------------------------------------------------- 1 | @code { 2 | [Parameter, EditorRequired] 3 | public string Id { get; set; } = null!; 4 | 5 | [Parameter] 6 | public int Size { get; set; } = 48; 7 | } 8 | 9 | @implements IDisposable 10 | 11 | @using Nostrid.Data; 12 | @using Nostrid.Misc; 13 | @using Nostrid.Model; 14 | 15 | @inject ChannelService _channelService 16 | 17 | @if (_details != null && _details.PictureUrl.IsNotNullOrEmpty() && Uri.IsWellFormedUriString(_details.PictureUrl, UriKind.Absolute)) 18 | { 19 | 20 | } 21 | else if (Id.IsNotNullOrEmpty()) 22 | { 23 |
24 | @((MarkupString)Utils.ToSvgIdenticon(Id, Size)) 25 |
26 | } 27 | 28 | @code 29 | { 30 | private ChannelDetails? _details; 31 | 32 | protected override void OnParametersSet() 33 | { 34 | base.OnParametersSet(); 35 | 36 | _details = _channelService.GetChannel(Id)?.Details; 37 | _channelService.ChannelDetailsChanged += ChannelDetailsChanged; 38 | } 39 | 40 | private void ChannelDetailsChanged(object? sender, (string channelId, ChannelDetails details) data) 41 | { 42 | if (data.channelId == Id) 43 | { 44 | _details = data.details; 45 | InvokeAsync(() => StateHasChanged()); 46 | } 47 | } 48 | 49 | #region Dispose 50 | private bool _disposed; 51 | 52 | public void Dispose() => Dispose(true); 53 | 54 | protected virtual void Dispose(bool disposing) 55 | { 56 | if (!_disposed) 57 | { 58 | if (disposing) 59 | { 60 | Cleanup(); 61 | } 62 | 63 | _disposed = true; 64 | } 65 | } 66 | 67 | private void Cleanup() 68 | { 69 | _channelService.ChannelDetailsChanged -= ChannelDetailsChanged; 70 | } 71 | #endregion 72 | } -------------------------------------------------------------------------------- /Nostrid/Platforms/Windows/Package.appxmanifest: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $placeholder$ 15 | User Name 16 | $placeholder$.png 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Logging; 2 | @using Nostrid.Data; 3 | @using Nostrid.Model; 4 | @using Nostrid.Pages; 5 | @using System.Diagnostics; 6 | 7 | @inject AccountService accountService 8 | @inject ConfigService configService 9 | @inject LocalSignerFactory localSignerFactory 10 | @inject ILogger logger 11 | 12 | @inherits LayoutComponentBase 13 | 14 | 15 | 16 | 17 | 18 |
19 | @Body 20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | @code{ 30 | private Scripts? scripts; 31 | private Alert? alert; 32 | 33 | protected override void OnInitialized() 34 | { 35 | base.OnInitialized(); 36 | 37 | if (!string.IsNullOrEmpty(configService.MainConfig.MainAccountId)) 38 | { 39 | var account = accountService.GetAccount(configService.MainConfig.MainAccountId); 40 | 41 | if (accountService.HasSigner(account.Id)) 42 | { 43 | accountService.SetMainAccount(account); 44 | } 45 | else if (localSignerFactory.TryFromPrivKey(account.PrivKey, out var signer)) 46 | { 47 | accountService.SetMainAccount(account, signer); 48 | } 49 | } 50 | } 51 | 52 | protected override void OnAfterRender(bool firstRender) 53 | { 54 | if (scripts != null && firstRender) 55 | { 56 | scripts.InvokeAfterRender(async () => 57 | { 58 | await scripts.InvokeVoidAsync("setTheme", configService.MainConfig.Theme ?? ConfigPage.DARK_THEME); 59 | }); 60 | } 61 | } 62 | 63 | private void OnError(Exception e) 64 | { 65 | alert?.Show("An unhandled error has occurred. Click here to reload the application.", Alert.Type.Danger); 66 | #if DEBUG 67 | scripts?.JSRuntime?.InvokeVoidAsync("console.error", e.ToString()); 68 | #endif 69 | } 70 | } -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nostrid 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Nostrid.Core/Migrations/20230305143822_ReplaceableEvents.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Nostrid.Migrations 6 | { 7 | /// 8 | public partial class ReplaceableEvents : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "ReplaceableId", 15 | table: "Events", 16 | type: "TEXT", 17 | nullable: true); 18 | 19 | migrationBuilder.CreateIndex( 20 | name: "IX_Events_ReplaceableId", 21 | table: "Events", 22 | column: "ReplaceableId", 23 | unique: true); 24 | 25 | migrationBuilder.CreateIndex( 26 | name: "IX_Events_ReplaceableId_CreatedAt", 27 | table: "Events", 28 | columns: new[] { "ReplaceableId", "CreatedAt" }); 29 | 30 | // Remove all events that have an 'a' tag because we might have to recalculate their replyTo 31 | migrationBuilder.Sql("DELETE FROM Events WHERE Id IN (SELECT EventId FROM TagDatas WHERE Data0='a')"); 32 | migrationBuilder.Sql("DELETE FROM TagDatas WHERE Data0='a'"); 33 | 34 | // Remove all replaceable events because we need to recalculate their replaceableId 35 | migrationBuilder.Sql("DELETE FROM Events WHERE Kind>=10000 AND Kind<40000"); 36 | } 37 | 38 | /// 39 | protected override void Down(MigrationBuilder migrationBuilder) 40 | { 41 | migrationBuilder.DropIndex( 42 | name: "IX_Events_ReplaceableId", 43 | table: "Events"); 44 | 45 | migrationBuilder.DropIndex( 46 | name: "IX_Events_ReplaceableId_CreatedAt", 47 | table: "Events"); 48 | 49 | migrationBuilder.DropColumn( 50 | name: "ReplaceableId", 51 | table: "Events"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/ConfigSection.razor: -------------------------------------------------------------------------------- 1 | @code 2 | { 3 | [Parameter, EditorRequired] 4 | public RenderFragment Header { get; set; } = null!; 5 | 6 | [Parameter, EditorRequired] 7 | public RenderFragment Body { get; set; } = null!; 8 | 9 | [Parameter] 10 | public bool AlwaysExpanded { get; set; } 11 | 12 | [Parameter] 13 | public IEnumerable? ConfigSections { get; set; } 14 | } 15 | 16 | @if (_isVisible) 17 | { 18 |
19 |
    20 |
  • 21 |
    22 | @if (!AlwaysExpanded && _isExpanded) 23 | { 24 | 25 | } 26 |
    @Header
    27 | @if (!AlwaysExpanded && !_isExpanded) 28 | { 29 | 30 | } 31 |
    32 |
  • 33 | @if (AlwaysExpanded || _isExpanded) 34 | { 35 | @Body 36 | } 37 |
38 |
39 | } 40 | 41 | @code 42 | { 43 | private bool _isVisible = true; 44 | private bool _isExpanded = false; 45 | 46 | public void ToggleExpanded() 47 | { 48 | if (AlwaysExpanded) return; 49 | var newValue = !_isExpanded; 50 | ConfigSections?.ToList().ForEach(s => 51 | { 52 | if (s != this) 53 | { 54 | s.ToggleVisible(!newValue); 55 | } 56 | }); 57 | _isExpanded = newValue; 58 | } 59 | 60 | public void ToggleVisible(bool value) 61 | { 62 | _isVisible = value; 63 | _isExpanded = false; 64 | StateHasChanged(); 65 | } 66 | } -------------------------------------------------------------------------------- /Nostrid.Core/Pages/Explore.razor: -------------------------------------------------------------------------------- 1 | @page "/explore" 2 | 3 | @implements IDisposable 4 | 5 | @using Nostrid.Data; 6 | 7 | @inject AccountService accountService 8 | @inject FeedService feedService 9 | @inject NavigationManager navigationManager 10 | 11 |

Explore

12 |
13 | 14 |
15 | 16 | 17 | @if (accountService.MainAccount != null) 18 | { 19 | var feedSources = feedService.GetFeedSources(accountService.MainAccount.Id); 20 | if (feedSources.Count > 0) 21 | { 22 |
Saved hashtags
23 | @foreach (var feedSource in feedSources) 24 | { 25 |
27 |
28 | @string.Join(", ", feedSource.Hashtags.ToArray()) 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | } 37 | } 38 | } 39 |
40 | 41 | @code { 42 | 43 | private UpdatabableElement? savedFeedSources; 44 | 45 | protected override void OnInitialized() 46 | { 47 | accountService.MainAccountChanged += MainAccountChanged; 48 | } 49 | 50 | private void MainAccountChanged(object? sender, EventArgs args) 51 | { 52 | InvokeAsync(() => StateHasChanged()); 53 | } 54 | 55 | private void RemoveSavedFeed(long id) 56 | { 57 | feedService.DeleteFeedSource(id); 58 | } 59 | 60 | #region Dispose 61 | private bool disposed; 62 | 63 | public void Dispose() => Dispose(true); 64 | 65 | protected virtual void Dispose(bool disposing) 66 | { 67 | if (!disposed) 68 | { 69 | if (disposing) 70 | { 71 | accountService.MainAccountChanged -= MainAccountChanged; 72 | } 73 | 74 | disposed = true; 75 | } 76 | } 77 | #endregion 78 | } 79 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/RelayFilters/AllSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | using Nostrid.Model; 3 | using System.Linq; 4 | 5 | namespace Nostrid.Data.Relays; 6 | 7 | public class AllSubscriptionFilter : SubscriptionFilter, IRelayFilter, IDbFilter 8 | { 9 | private readonly int[] validKinds = new[] { NostrKind.Text, NostrKind.Relay, NostrKind.Deletion, NostrKind.Repost, NostrKind.Reaction, NostrKind.Badge }; 10 | 11 | private readonly EventDatabase eventDatabase; 12 | 13 | private Lazy db; 14 | 15 | public AllSubscriptionFilter(EventDatabase eventDatabase) 16 | { 17 | this.eventDatabase = eventDatabase; 18 | db = new(() => eventDatabase.CreateContext()); 19 | } 20 | 21 | public override NostrSubscriptionFilter[] GetFilters() 22 | { 23 | return new[] { new NostrSubscriptionFilter() { Kinds = validKinds, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until } }; 24 | } 25 | 26 | public override SubscriptionFilter Clone() 27 | { 28 | return new AllSubscriptionFilter(eventDatabase); 29 | } 30 | 31 | public NostrSubscriptionFilter[] GetFiltersForRelay(long relayId) 32 | { 33 | if (db.Value.Relays.Any(r => r.Id == relayId && r.IsPaid)) 34 | { 35 | return new[] { new NostrSubscriptionFilter() { Kinds = validKinds, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until } }; 36 | } 37 | return Array.Empty(); 38 | } 39 | 40 | public IQueryable ApplyDbFilter(IQueryable events) 41 | { 42 | var query = events.Where(e => validKinds.Contains(e.Kind)); 43 | if (LimitFilterData.Since.HasValue) 44 | { 45 | var since = LimitFilterData.Since.Value.ToUnixTimeSeconds(); 46 | query = query.Where(e => e.CreatedAtCurated >= since); 47 | } 48 | if (LimitFilterData.Until.HasValue) 49 | { 50 | var until = LimitFilterData.Until.Value.ToUnixTimeSeconds(); 51 | query = query.Where(e => e.CreatedAtCurated <= until); 52 | } 53 | return query; 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Nostrid.Core/Externals/VoidCatMediaService.cs: -------------------------------------------------------------------------------- 1 | using Nostrid.Misc; 2 | using System.IO; 3 | using System.Net.Http.Headers; 4 | using System.Security.Cryptography; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace Nostrid.Externals 8 | { 9 | public partial class VoidCatMediaService : IMediaService 10 | { 11 | private static Regex _linkRegex = LinkRegex(); 12 | 13 | public string Name => "void.cat"; 14 | 15 | public int MaxSize { get => 5 * 1024 * 1024; } 16 | 17 | public async Task UploadFile(Stream data, string filename, string mimeType, Action progress) 18 | { 19 | var sha256 = Convert.ToHexString(SHA256.HashData(data)); 20 | data.Position = 0; 21 | 22 | using var httpClient = new HttpClient(); 23 | using var progressStream = new ProgressStream(data); 24 | progressStream.UpdateProgress += (s, e) => progress(e); 25 | using var fileContent = new StreamContent(progressStream); 26 | fileContent.Headers.Add("V-Full-Digest", sha256); 27 | fileContent.Headers.Add("V-Filename", filename); 28 | fileContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); 29 | using var response = await httpClient.PostAsync("https://void.cat/upload?cli=true", fileContent); 30 | 31 | if (response.IsSuccessStatusCode) 32 | { 33 | var responseText = await response.Content.ReadAsStringAsync(); 34 | if (responseText.IsNotNullOrEmpty()) 35 | { 36 | var match = _linkRegex.Match(responseText); 37 | if (match.Success) 38 | { 39 | var link = $"https://{match.Groups["link"].Value}"; 40 | if (Uri.IsWellFormedUriString(link, UriKind.Absolute)) 41 | { 42 | return new Uri(link); 43 | } 44 | } 45 | } 46 | } 47 | return null; 48 | } 49 | 50 | [GeneratedRegex("(http(s?):\\/\\/(?[^ ]*))")] 51 | private static partial Regex LinkRegex(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Nostrid.Web/DatabaseService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace Nostrid; 4 | 5 | public class DatabaseService 6 | { 7 | #if RELEASE 8 | public const string FileName = "/database/nostrid.sqlite.db"; 9 | private readonly Lazy> _moduleTask; 10 | #else 11 | public const string FileName = "nostrid.sqlite.db"; 12 | #endif 13 | 14 | 15 | 16 | #if DEBUG 17 | public DatabaseService() 18 | { 19 | } 20 | #else 21 | public DatabaseService(IJSRuntime jsRuntime) 22 | { 23 | if (jsRuntime == null) throw new ArgumentNullException(nameof(jsRuntime)); 24 | 25 | _moduleTask = new(() => jsRuntime.InvokeAsync( 26 | "import", "./file.js").AsTask()); 27 | } 28 | #endif 29 | 30 | public async Task InitDatabaseAsync() 31 | { 32 | #if RELEASE 33 | var module = await _moduleTask.Value; 34 | await module.InvokeVoidAsync("mountAndInitializeDb"); 35 | if (!File.Exists(FileName)) 36 | { 37 | File.Create(FileName).Close(); 38 | } 39 | #endif 40 | } 41 | 42 | private static bool saving, mustResave; 43 | private static object lockObj = new(); 44 | 45 | public void StartSyncDatabase() 46 | { 47 | #if RELEASE 48 | Task.Run(async () => 49 | { 50 | bool oldSaving; 51 | lock (lockObj) 52 | { 53 | oldSaving = saving; 54 | if (saving) 55 | mustResave = true; 56 | else 57 | saving = true; 58 | } 59 | 60 | if (!oldSaving) 61 | { 62 | while (true) 63 | { 64 | var module = await _moduleTask.Value; 65 | await module.InvokeVoidAsync("syncDatabase"); 66 | 67 | lock (lockObj) 68 | { 69 | if (!mustResave) 70 | { 71 | saving = false; 72 | return; 73 | } 74 | mustResave = false; 75 | } 76 | } 77 | } 78 | }); 79 | #endif 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/FollowsFeed.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | 3 | @using Nostrid.Data 4 | @using Nostrid.Data.Relays; 5 | @using Nostrid.Model; 6 | @using Nostrid.Misc; 7 | 8 | @inject AccountService accountService 9 | @inject EventDatabase eventDatabase 10 | @inject NavigationManager navigationManager 11 | 12 | @if (follows.Count == 0) 13 | { 14 |
15 | 16 |

No follows yet.

17 |

If you recently restored your account then your follows may take a few minutes to be discovered.

18 |
19 | } 20 | else 21 | { 22 | 23 | } 24 | 25 | @code { 26 | private SubscriptionFilter? filter; 27 | private List follows = new(); 28 | 29 | protected override void OnInitialized() 30 | { 31 | accountService.MainAccountChanged += MainAccountChanged; 32 | SetFilter(); 33 | } 34 | 35 | private void MainAccountChanged(object? sender, EventArgs args) 36 | { 37 | SetFilter(); 38 | InvokeAsync(() => StateHasChanged()); 39 | } 40 | 41 | private void SetFilter() 42 | { 43 | if (accountService.MainAccount == null) 44 | { 45 | navigationManager.NavigateTo("/feed"); 46 | return; 47 | } 48 | follows = eventDatabase.GetFollowIds(accountService.MainAccount.Id); 49 | if (follows.Count == 0) 50 | { 51 | filter = null; 52 | } 53 | else 54 | { 55 | filter = new AuthorSubscriptionFilter(follows.ToArray()); 56 | } 57 | } 58 | 59 | #region Dispose 60 | private bool disposed; 61 | 62 | public void Dispose() => Dispose(true); 63 | 64 | protected virtual void Dispose(bool disposing) 65 | { 66 | if (!disposed) 67 | { 68 | if (disposing) 69 | { 70 | accountService.MainAccountChanged -= MainAccountChanged; 71 | } 72 | 73 | disposed = true; 74 | } 75 | } 76 | #endregion 77 | } -------------------------------------------------------------------------------- /Nostrid.Core/Misc/ProgressStream.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Misc 2 | { 3 | public class ProgressStream : Stream 4 | { 5 | private Stream m_input; 6 | 7 | public event EventHandler? UpdateProgress; 8 | 9 | public ProgressStream(Stream input) 10 | { 11 | m_input = input; 12 | } 13 | public override void Flush() 14 | { 15 | throw new NotImplementedException(); 16 | } 17 | 18 | public override long Seek(long offset, SeekOrigin origin) 19 | { 20 | return m_input.Seek(offset, origin); 21 | } 22 | 23 | public override void SetLength(long value) 24 | { 25 | throw new NotImplementedException(); 26 | } 27 | 28 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 29 | { 30 | int n = await m_input.ReadAsync(buffer, offset, count, cancellationToken); 31 | UpdateProgress?.Invoke(this, (1.0f * Position) / Length); 32 | return n; 33 | } 34 | 35 | public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) 36 | { 37 | int n = await m_input.ReadAsync(buffer, cancellationToken); 38 | UpdateProgress?.Invoke(this, (1.0f * Position) / Length); 39 | return n; 40 | } 41 | 42 | public override int Read(byte[] buffer, int offset, int count) 43 | { 44 | int n = m_input.Read(buffer, offset, count); 45 | UpdateProgress?.Invoke(this, (1.0f * Position) / Length); 46 | return n; 47 | } 48 | 49 | public override void Write(byte[] buffer, int offset, int count) 50 | { 51 | throw new NotImplementedException(); 52 | } 53 | 54 | public override bool CanRead => true; 55 | public override bool CanSeek => true; 56 | public override bool CanWrite => false; 57 | public override long Length => m_input.Length; 58 | public override long Position 59 | { 60 | get { return m_input.Position; } 61 | set { m_input.Position = value; } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Nostrid.Core/Migrations/20230310163955_Muting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Nostrid.Migrations 7 | { 8 | /// 9 | public partial class Muting : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AddColumn( 15 | name: "MutesLastUpdate", 16 | table: "Accounts", 17 | type: "TEXT", 18 | nullable: true); 19 | 20 | migrationBuilder.CreateTable( 21 | name: "Mutes", 22 | columns: table => new 23 | { 24 | Id = table.Column(type: "INTEGER", nullable: false) 25 | .Annotation("Sqlite:Autoincrement", true), 26 | AccountId = table.Column(type: "TEXT", nullable: false), 27 | MuteId = table.Column(type: "TEXT", nullable: false), 28 | IsPrivate = table.Column(type: "INTEGER", nullable: false) 29 | }, 30 | constraints: table => 31 | { 32 | table.PrimaryKey("PK_Mutes", x => x.Id); 33 | }); 34 | 35 | migrationBuilder.CreateIndex( 36 | name: "IX_Mutes_AccountId", 37 | table: "Mutes", 38 | column: "AccountId"); 39 | 40 | migrationBuilder.CreateIndex( 41 | name: "IX_Mutes_AccountId_MuteId", 42 | table: "Mutes", 43 | columns: new[] { "AccountId", "MuteId" }); 44 | 45 | migrationBuilder.CreateIndex( 46 | name: "IX_Mutes_MuteId", 47 | table: "Mutes", 48 | column: "MuteId"); 49 | } 50 | 51 | /// 52 | protected override void Down(MigrationBuilder migrationBuilder) 53 | { 54 | migrationBuilder.DropTable( 55 | name: "Mutes"); 56 | 57 | migrationBuilder.DropColumn( 58 | name: "MutesLastUpdate", 59 | table: "Accounts"); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Nostrid.Web/SerialQueue.cs: -------------------------------------------------------------------------------- 1 | internal class SerialQueue 2 | { 3 | readonly object _locker = new object(); 4 | readonly WeakReference _lastTask = new WeakReference(null); 5 | 6 | public Task Enqueue(Action action) 7 | { 8 | return Enqueue(() => 9 | { 10 | action(); 11 | return true; 12 | }); 13 | } 14 | 15 | public Task Enqueue(Func function) 16 | { 17 | lock (_locker) 18 | { 19 | Task lastTask; 20 | Task resultTask; 21 | 22 | if (_lastTask.TryGetTarget(out lastTask)) 23 | { 24 | resultTask = lastTask.ContinueWith(_ => function(), TaskContinuationOptions.ExecuteSynchronously); 25 | } 26 | else 27 | { 28 | resultTask = Task.Run(function); 29 | } 30 | 31 | _lastTask.SetTarget(resultTask); 32 | 33 | return resultTask; 34 | } 35 | } 36 | 37 | public Task Enqueue(Func asyncAction) 38 | { 39 | lock (_locker) 40 | { 41 | Task lastTask; 42 | Task resultTask; 43 | 44 | if (_lastTask.TryGetTarget(out lastTask)) 45 | { 46 | resultTask = lastTask.ContinueWith(_ => asyncAction(), TaskContinuationOptions.ExecuteSynchronously).Unwrap(); 47 | } 48 | else 49 | { 50 | resultTask = Task.Run(asyncAction); 51 | } 52 | 53 | _lastTask.SetTarget(resultTask); 54 | 55 | return resultTask; 56 | } 57 | } 58 | 59 | public Task Enqueue(Func> asyncFunction) 60 | { 61 | lock (_locker) 62 | { 63 | Task lastTask; 64 | Task resultTask; 65 | 66 | if (_lastTask.TryGetTarget(out lastTask)) 67 | { 68 | resultTask = lastTask.ContinueWith(_ => asyncFunction(), TaskContinuationOptions.ExecuteSynchronously).Unwrap(); 69 | } 70 | else 71 | { 72 | resultTask = Task.Run(asyncFunction); 73 | } 74 | 75 | _lastTask.SetTarget(resultTask); 76 | 77 | return resultTask; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Doc/HowToUseFollowsEditor.md: -------------------------------------------------------------------------------- 1 | # How to use the Follows editor 2 | 3 | It is fairly common for Nostr users to lose their follows. The reason for this is that follows are set all at once. 4 | That is, when you follow someone, you send all your follow list to the network. Normally this works fine when 5 | you use a single client, but things may go awry if you use multiple clients. For example, suppose you follow 6 | account A in client X, then go and follow account B in client Y _before_ Y knows that you followed A in client X. 7 | In this case you lose follow A from client X as this change is overwritten by Y. 8 | 9 | Nostrid has a Follows editor that can help you. This tool shows you the accounts that you currently follow and the 10 | accounts that can be seen that you followed at some point in the past. This data is taken from relays and also 11 | from the local database that Nostrid has in your device. Most relays discard old follows, but it may happen that 12 | some relays still have your old follows. Also Nostrid never deletes your old follows (unless you manually clean the 13 | database), so if some other client has overwritten your follows, they will for sure be visible in the Follows editor. 14 | 15 | To open the Follows editor, do this: 16 | 17 | 1. Log in 18 | 2. In the main menu, go to `Me` (that's your profile) 19 | 20 | ![Nostrid Android](https://raw.githubusercontent.com/lapulpeta/Nostrid-media/main/follows-editor1.jpeg) 21 | 22 | 3. Select 'Edit follows' 23 | 24 | ![Nostrid Android](https://raw.githubusercontent.com/lapulpeta/Nostrid-media/main/follows-editor2.jpeg) 25 | 26 | Now you should see the Follows editor. Here you can see your current and past follows (if available). Select the 27 | accounts that you want to follow and unselect any that you don't want to follow. Once you are ready, just 28 | press `Update`. This will send your new list to the network. 29 | 30 | If there are still missing follows, try connecting to other relays and maybe they will have an old copy of your 31 | follows. If they do then they will be visible in the Follows editor. 32 | 33 | You can repeat this process as many times as you need, even from different devices. This is important because 34 | sometimes you have Nostrid in your main device and also a copy in some secondary device, and this one may have 35 | an old copy of your follows. -------------------------------------------------------------------------------- /Nostrid.Core/Pages/Accounts.razor: -------------------------------------------------------------------------------- 1 | @page "/accounts" 2 | @using Nostrid.Data.Relays; 3 | @using Nostrid.Data; 4 | @using Nostrid.Misc; 5 | @using Nostrid.Model; 6 | @using System.Security.Cryptography; 7 | @using System.Diagnostics.CodeAnalysis; 8 | 9 | @inject ConfigService configService 10 | @inject AccountService accountService 11 | @inject EventDatabase eventDatabase 12 | @inject Lud06Service lud06Service 13 | @inject LocalSignerFactory localSignerFactory 14 | 15 |
16 |
17 |
18 | @((MarkupString)Utils.ToSvgIdenticon(PublicKey, 48)) 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 | @code { 29 | 30 | public TextInput privateKeyModel = new(); 31 | private string PublicKey 32 | { 33 | get 34 | { 35 | return LocalSigner.TryGetPubKeyFromPrivKey(privateKeyModel.Text, out var _, out var pubKey) ? pubKey : string.Empty; 36 | } 37 | } 38 | private void GeneratePrivateKey() 39 | { 40 | privateKeyModel.Text = ByteTools.PubkeyToNsec(Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLower()); 41 | } 42 | 43 | private async Task CreateNewAccount() 44 | { 45 | string privkey = null; 46 | 47 | if (Utils.IsValidNostrId(privateKeyModel.Text)) 48 | { 49 | privkey = privateKeyModel.Text; 50 | } 51 | else if (ByteTools.TryDecodeBech32(privateKeyModel.Text, out var prefix, out var hex) && prefix == "nsec" && !string.IsNullOrEmpty(hex)) 52 | { 53 | privkey = hex; 54 | } 55 | 56 | if (privkey.IsNotNullOrEmpty() && localSignerFactory.TryFromPrivKey(privkey, out var signer)) 57 | { 58 | var account = new Model.Account() 59 | { 60 | PrivKey = privkey, 61 | Id = await signer.GetPubKey(), 62 | }; 63 | eventDatabase.SaveAccount(account); 64 | } 65 | else 66 | { 67 | //TODO: mark private key as invalid 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Nostrid.Core/Nostrid.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | true 8 | Nostrid 9 | 1.33.1 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | all 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/NoteViewerSeeMore.razor: -------------------------------------------------------------------------------- 1 | @using Nostrid.Model; 2 | 3 | @inject NavigationManager navigationManager 4 | 5 | @code{ 6 | [Parameter, EditorRequired] 7 | public NoteTreeShowMore NoteTreeShowMore { get; set; } = null!; 8 | 9 | [Parameter, EditorRequired] 10 | public int[] TreeLayout { get; set; } = null!; 11 | 12 | [Parameter] 13 | public bool OnlyChild { get; set; } 14 | 15 | [Parameter] 16 | public int ChildIndex { get; set; } 17 | } 18 | 19 |
21 | 22 | @for (var i = 1; i < TreeLayout.Length; i++) 23 | { 24 | var last = i == TreeLayout.Length - 1; 25 |
26 |
27 | @if (TreeLayout[i] > 1) 28 | { 29 |
30 | } 31 | else if (!OnlyChild && TreeLayout[i] == 1 && last) 32 | { 33 |
34 | } 35 | else 36 | { 37 |
38 | } 39 |
40 | @if (!OnlyChild && last) 41 | { 42 |
43 | } 44 |
45 |
46 | } 47 | 48 |
49 |
50 | @if (NoteTreeShowMore.Start) 51 | { 52 | 53 | } 54 | else 55 | { 56 | 57 | } 58 |
59 |
See more...
60 |
61 | 62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /Nostrid.Core/Externals/NostrcheckMediaService.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostrid.Misc; 3 | 4 | namespace Nostrid.Externals 5 | { 6 | public partial class NostrcheckMediaService : IMediaService 7 | { 8 | public string Name => "nostrcheck.me"; 9 | 10 | public int MaxSize => 5 * 1024 * 1024; 11 | 12 | public async Task UploadFile(Stream data, string filename, string mimeType, Action progress) 13 | { 14 | using var progressStream = new ProgressStream(data); 15 | progressStream.UpdateProgress += (s, e) => progress(e); 16 | 17 | using var httpContent = new MultipartFormDataContent 18 | { 19 | { new StreamContent(progressStream), "publicgallery", filename }, 20 | { new StringContent("26d075787d261660682fb9d20dbffa538c708b1eda921d0efa2be95fbef4910a"), "apikey" } 21 | }; 22 | 23 | using var httpClient = new HttpClient(); 24 | var response = await httpClient.PostAsync("https://nostrcheck.me/api/media.php", httpContent); 25 | 26 | if (response.IsSuccessStatusCode) 27 | { 28 | var responseText = await response.Content.ReadAsStringAsync(); 29 | if (responseText.IsNotNullOrEmpty()) 30 | { 31 | var responseObj = JsonConvert.DeserializeObject(responseText); 32 | if (responseObj != null && responseObj.Status && Uri.IsWellFormedUriString(responseObj.Url, UriKind.Absolute)) 33 | { 34 | return new Uri(responseObj.Url); 35 | } 36 | } 37 | } 38 | return null; 39 | } 40 | 41 | private class NostrcheckRoot 42 | { 43 | [JsonProperty("apikey")] 44 | public string? ApiKey { get; set; } 45 | 46 | [JsonProperty("request")] 47 | public string? Request { get; set; } 48 | 49 | [JsonProperty("filesize")] 50 | public int FileSize { get; set; } 51 | 52 | [JsonProperty("message")] 53 | public string? Message { get; set; } 54 | 55 | [JsonProperty("status")] 56 | public bool Status { get; set; } 57 | 58 | [JsonProperty("URL")] 59 | public string? Url { get; set; } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/NostrTvl.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using System.Text; 3 | 4 | namespace Nostrid.Model; 5 | 6 | public enum NostrTvlType 7 | { 8 | Special = 0, 9 | Relay = 1, 10 | Author = 2, 11 | Kind = 3 12 | } 13 | 14 | public abstract class TvlEntity 15 | { 16 | public abstract List<(NostrTvlType, byte[])> GetTvl(); 17 | } 18 | 19 | // NIP-19: https://github.com/nostr-protocol/nips/blob/master/19.md 20 | 21 | public class Nevent : TvlEntity 22 | { 23 | public readonly string EventId; 24 | 25 | public Nevent(List<(NostrTvlType, byte[])> tvl) 26 | { 27 | EventId = Convert.ToHexString(tvl.FirstOrDefault(t => t.Item1 == NostrTvlType.Special).Item2).ToLower(); 28 | } 29 | 30 | public override List<(NostrTvlType, byte[])> GetTvl() 31 | { 32 | return new() { (NostrTvlType.Special, Convert.FromHexString(EventId)) }; 33 | } 34 | } 35 | 36 | public class Naddr : TvlEntity 37 | { 38 | public readonly string D; 39 | public readonly string Pubkey; 40 | public readonly int Kind; 41 | 42 | public Naddr(string replaceableId) 43 | { 44 | var exploded = EventExtension.ExplodeReplaceableId(replaceableId); 45 | if (exploded == null) 46 | { 47 | return; 48 | } 49 | Pubkey = exploded.Value.pubkey; 50 | Kind = exploded.Value.kind; 51 | D = exploded.Value.dstr; 52 | } 53 | 54 | public bool IsValid => D.IsNotNullOrEmpty() && Pubkey.IsNotNullOrEmpty(); 55 | 56 | public Naddr(List<(NostrTvlType, byte[])> tvl) 57 | { 58 | D = Encoding.ASCII.GetString(tvl.FirstOrDefault(t => t.Item1 == NostrTvlType.Special).Item2); 59 | Pubkey = Convert.ToHexString(tvl.FirstOrDefault(t => t.Item1 == NostrTvlType.Author).Item2).ToLower(); 60 | Kind = (int)BinaryPrimitives.ReadUInt32BigEndian(tvl.FirstOrDefault(t => t.Item1 == NostrTvlType.Kind).Item2); 61 | } 62 | 63 | public string ReplaceableId => EventExtension.GetReplaceableId(Pubkey, Kind, D)!; 64 | 65 | public override List<(NostrTvlType, byte[])> GetTvl() 66 | { 67 | byte[] kind = new byte[sizeof(uint)]; 68 | BinaryPrimitives.WriteUInt32BigEndian(kind, (uint)Kind); 69 | return new() { 70 | (NostrTvlType.Special, Encoding.ASCII.GetBytes(D)), 71 | (NostrTvlType.Author, Convert.FromHexString(Pubkey)), 72 | (NostrTvlType.Kind, kind), 73 | }; 74 | } 75 | } -------------------------------------------------------------------------------- /Nostrid.Core/Shared/AccountName.razor: -------------------------------------------------------------------------------- 1 | @code{ 2 | [Parameter, EditorRequired] 3 | public string Id { get; set; } = null!; 4 | 5 | [Parameter] 6 | public bool OnlyText { get; set; } 7 | } 8 | 9 | @implements IDisposable 10 | 11 | @using Nostrid.Data; 12 | @using Nostrid.Misc; 13 | @using Nostrid.Model; 14 | 15 | @inject AccountService accountService 16 | 17 | 18 | @if (details != null && details.Nip05Id.IsNotNullOrEmpty() && details.Nip05Valid && 19 | Nip05.DecodeNip05(details.Nip05Id, out var domain, out var username)) 20 | { 21 | if (username != "_") 22 | { 23 | @username 24 | } 25 | 26 | @(username != "_" ? "@" : "")@domain 27 | @if (!OnlyText) 28 | { 29 | 30 | } 31 | } 32 | else if (Id != null) 33 | { 34 | @(details?.Name ?? ByteTools.PubkeyToNpub(Id, true)) 35 | } 36 | @if (!OnlyText && (details?.Lud16Id ?? details?.Lud06Url).IsNotNullOrEmpty()) 37 | { 38 | 39 | } 40 | 41 | 42 | @code 43 | { 44 | private AccountDetails? details; 45 | 46 | protected override void OnInitialized() 47 | { 48 | accountService.AccountDetailsChanged += AccountDetailsChanged; 49 | } 50 | 51 | protected override void OnParametersSet() 52 | { 53 | if (Id == null) return; 54 | accountService.AddDetailsNeeded(Id); 55 | details = accountService.GetAccountDetails(Id); 56 | } 57 | 58 | private void AccountDetailsChanged(object? sender, (string accountId, AccountDetails details) data) 59 | { 60 | if (data.accountId == Id) 61 | { 62 | details = data.details; 63 | InvokeAsync(() => StateHasChanged()); 64 | } 65 | } 66 | 67 | #region Dispose 68 | private bool disposed; 69 | 70 | public void Dispose() => Dispose(true); 71 | 72 | protected virtual void Dispose(bool disposing) 73 | { 74 | if (!disposed) 75 | { 76 | if (disposing) 77 | { 78 | Cleanup(); 79 | } 80 | 81 | disposed = true; 82 | } 83 | } 84 | 85 | private void Cleanup() 86 | { 87 | accountService.AccountDetailsChanged -= AccountDetailsChanged; 88 | } 89 | #endregion 90 | } -------------------------------------------------------------------------------- /Nostrid.Web/wwwroot/service-worker.published.js: -------------------------------------------------------------------------------- 1 | // Caution! Be sure you understand the caveats before publishing an application with 2 | // offline support. See https://aka.ms/blazor-offline-considerations 3 | 4 | self.importScripts('./service-worker-assets.js'); 5 | self.addEventListener('install', event => event.waitUntil(onInstall(event))); 6 | self.addEventListener('activate', event => event.waitUntil(onActivate(event))); 7 | self.addEventListener('fetch', event => event.respondWith(onFetch(event))); 8 | 9 | const cacheNamePrefix = 'offline-cache-'; 10 | const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; 11 | const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; 12 | const offlineAssetsExclude = [ /^service-worker\.js$/ ]; 13 | 14 | async function onInstall(event) { 15 | console.info('Service worker: Install'); 16 | 17 | // Fetch and cache all matching items from the assets manifest 18 | const assetsRequests = self.assetsManifest.assets 19 | .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) 20 | .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) 21 | .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); 22 | await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); 23 | } 24 | 25 | async function onActivate(event) { 26 | console.info('Service worker: Activate'); 27 | 28 | // Delete unused caches 29 | const cacheKeys = await caches.keys(); 30 | await Promise.all(cacheKeys 31 | .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) 32 | .map(key => caches.delete(key))); 33 | } 34 | 35 | async function onFetch(event) { 36 | let cachedResponse = null; 37 | if (event.request.method === 'GET') { 38 | // For all navigation requests, try to serve index.html from cache 39 | // If you need some URLs to be server-rendered, edit the following check to exclude those URLs 40 | const shouldServeIndexHtml = event.request.mode === 'navigate'; 41 | 42 | const request = shouldServeIndexHtml ? 'index.html' : event.request; 43 | const cache = await caches.open(cacheName); 44 | cachedResponse = await cache.match(request); 45 | } 46 | 47 | return cachedResponse || fetch(event.request); 48 | } 49 | -------------------------------------------------------------------------------- /Nostrid.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using Ganss.Xss; 2 | using Microsoft.AspNetCore.Components.Web; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | using Microsoft.JSInterop; 5 | using NNostr.Client; 6 | using Nostrid; 7 | using Nostrid.Data; 8 | using Nostrid.Data.Relays; 9 | using Nostrid.Externals; 10 | using Nostrid.Interfaces; 11 | using Nostrid.Model; 12 | using Nostrid.Web.Services; 13 | 14 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 15 | builder.RootComponents.Add
("#app"); 16 | builder.RootComponents.Add("head::after"); 17 | 18 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 19 | 20 | builder.Services.AddSingleton(); 21 | builder.Services.AddSingleton(); 22 | builder.Services.AddSingleton(); 23 | builder.Services.AddSingleton(); 24 | builder.Services.AddSingleton(); 25 | builder.Services.AddSingleton(); 26 | builder.Services.AddSingleton(); 27 | builder.Services.AddSingleton(); 28 | builder.Services.AddSingleton(); 29 | builder.Services.AddSingleton(); 30 | builder.Services.AddSingleton(); 31 | builder.Services.AddSingleton(); 32 | builder.Services.AddSingleton(); 33 | builder.Services.AddSingleton(); 34 | builder.Services.AddSingleton(); 35 | builder.Services.AddSingleton(); 36 | builder.Services.AddSingleton(); 37 | builder.Services.AddSingleton(); 38 | builder.Services.AddSingleton(); 39 | builder.Services.AddSingleton(); 40 | 41 | var host = builder.Build(); 42 | 43 | // Database 44 | var eventDatabase = host.Services.GetRequiredService(); 45 | #if RELEASE 46 | var dbService = host.Services.GetRequiredService(); 47 | await dbService.InitDatabaseAsync(); 48 | eventDatabase.DatabaseHasChanged += (_, _) => dbService.StartSyncDatabase(); 49 | #endif 50 | eventDatabase.InitDatabase(DatabaseService.FileName); 51 | 52 | // Signer 53 | var accountService = host.Services.GetRequiredService(); 54 | var jsRuntime = host.Services.GetRequiredService(); 55 | await accountService.AddSigner(new ExtensionSigner(jsRuntime)); 56 | 57 | await host.RunAsync(); 58 | -------------------------------------------------------------------------------- /Nostrid/MauiProgram.cs: -------------------------------------------------------------------------------- 1 | using Ganss.Xss; 2 | using NNostr.Client; 3 | using Nostrid.Data; 4 | using Nostrid.Data.Relays; 5 | using Nostrid.Externals; 6 | using Nostrid.Interfaces; 7 | using Nostrid.Misc; 8 | using Nostrid.Model; 9 | using Plugin.LocalNotification; 10 | 11 | namespace Nostrid; 12 | 13 | public static class MauiProgram 14 | { 15 | public static MauiApp CreateMauiApp() 16 | { 17 | var builder = MauiApp.CreateBuilder(); 18 | builder 19 | .UseMauiApp() 20 | .ConfigureFonts(fonts => 21 | { 22 | fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); 23 | }); 24 | 25 | builder.Services.AddMauiBlazorWebView(); 26 | #if DEBUG 27 | builder.Services.AddBlazorWebViewDeveloperTools(); 28 | #endif 29 | 30 | #if ANDROID || IOS 31 | builder.UseLocalNotification(); 32 | #endif 33 | 34 | builder.Services.AddSingleton(); 35 | builder.Services.AddSingleton(); 36 | builder.Services.AddSingleton(); 37 | builder.Services.AddSingleton(); 38 | builder.Services.AddSingleton(); 39 | builder.Services.AddSingleton(); 40 | builder.Services.AddSingleton(); 41 | builder.Services.AddSingleton(); 42 | builder.Services.AddSingleton(); 43 | builder.Services.AddSingleton(); 44 | builder.Services.AddSingleton(); 45 | builder.Services.AddSingleton(); 46 | builder.Services.AddSingleton(); 47 | builder.Services.AddSingleton(); 48 | builder.Services.AddSingleton(); 49 | builder.Services.AddSingleton(); 50 | builder.Services.AddSingleton(); 51 | builder.Services.AddSingleton(); 52 | builder.Services.AddSingleton(); 53 | builder.Services.AddSingleton(); 54 | builder.Services.AddSingleton(); 55 | 56 | var app = builder.Build(); 57 | 58 | var eventDatabase = app.Services.GetRequiredService(); 59 | eventDatabase.InitDatabase(DbConstants.DatabasePath); 60 | 61 | return app; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/FollowAccount.razor: -------------------------------------------------------------------------------- 1 | @using Nostrid.Data; 2 | @using Nostrid.Model; 3 | @using Nostrid.Misc; 4 | 5 | @inject AccountService accountService 6 | @inject NavigationManager navigationManager 7 | 8 | @code { 9 | [Parameter, EditorRequired] 10 | public string AccountId { get; set; } = null!; 11 | 12 | [Parameter] 13 | public bool Selectable { get; set; } 14 | 15 | [Parameter] 16 | public bool Selected 17 | { 18 | get => selected; 19 | set 20 | { 21 | if (selected != value) 22 | { 23 | selected = value; 24 | SelectedChanged?.InvokeAsync(value); 25 | } 26 | } 27 | } 28 | 29 | [Parameter] 30 | public EventCallback? SelectedChanged { get; set; } 31 | 32 | [Parameter] 33 | public RenderFragment? ChildContent { get; set; } 34 | } 35 | 36 |
37 |
38 | 39 |
40 |
41 |
@ChildContent
42 |
43 |
44 | 45 |
46 | @if (accountService.MainAccount != null && accountService.MainAccount.Id != AccountId) 47 | { 48 | if (!Selectable) 49 | { 50 | 51 | } 52 | else 53 | { 54 |
55 | 59 |
60 | } 61 | } 62 |
63 |
@ByteTools.PubkeyToNpub(AccountId)
64 |
65 |
66 | 67 | @code 68 | { 69 | private bool selected; 70 | 71 | private void Click() 72 | { 73 | if (Selectable) 74 | { 75 | Selected = !Selected; 76 | } 77 | else 78 | { 79 | navigationManager.NavigateTo($"account/{AccountId}"); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /Nostrid.Core/Shared/Scripts.razor: -------------------------------------------------------------------------------- 1 | @using Nostrid.Data; 2 | @inject IJSRuntime jsRuntime 3 | @implements IAsyncDisposable 4 | 5 | @code { 6 | public IJSRuntime? JSRuntime { get => jsRuntime; } 7 | public IJSObjectReference? JSModule { get; private set; } 8 | public bool rendered = false; 9 | public Queue queue = new(); 10 | 11 | protected override async Task OnAfterRenderAsync(bool firstRender) 12 | { 13 | await base.OnAfterRenderAsync(firstRender); 14 | 15 | if (firstRender) 16 | { 17 | JSModule = await jsRuntime.InvokeAsync("import", "../scripts.js"); 18 | rendered = true; 19 | while (queue.TryDequeue(out Action? action)) action?.Invoke(); 20 | } 21 | } 22 | 23 | public async Task InvokeVoidAsync(string identifier, params object[] args) 24 | { 25 | if (JSModule != null) 26 | { 27 | await JSModule.InvokeVoidAsync(identifier, args); 28 | } 29 | } 30 | 31 | public void InvokeVoid(string identifier, params object[] args) 32 | { 33 | if (JSModule == null) 34 | { 35 | return; 36 | } 37 | Task.Run(async () => await JSModule.InvokeVoidAsync(identifier, args)); 38 | } 39 | 40 | public async Task InvokeAsync(string identifier, params object[] args) 41 | { 42 | if (JSModule == null) 43 | { 44 | return await Task.FromResult(default(T)); 45 | } 46 | return await JSModule.InvokeAsync(identifier, args); 47 | } 48 | 49 | public T? Invoke(string identifier, params object[] args) 50 | { 51 | if (JSModule == null) 52 | { 53 | return default(T); 54 | } 55 | var task = Task.Run(async () => await JSModule.InvokeAsync(identifier, args)); 56 | task.Wait(); 57 | return task.Result; 58 | } 59 | 60 | public void InvokeAfterRender(Action action) 61 | { 62 | if (!rendered) 63 | { 64 | queue.Enqueue(action); 65 | return; 66 | } 67 | action(); 68 | } 69 | 70 | #region Dispose 71 | private bool _disposed; 72 | 73 | async ValueTask IAsyncDisposable.DisposeAsync() => await Dispose(true); 74 | 75 | protected virtual async Task Dispose(bool disposing) 76 | { 77 | if (!_disposed) 78 | { 79 | if (disposing) 80 | { 81 | if (JSModule is not null) 82 | { 83 | await JSModule.DisposeAsync(); 84 | } 85 | } 86 | 87 | _disposed = true; 88 | } 89 | } 90 | #endregion 91 | } 92 | -------------------------------------------------------------------------------- /Nostrid.Core/Model/NoteTree.cs: -------------------------------------------------------------------------------- 1 | namespace Nostrid.Model; 2 | 3 | public class NoteTreeNode 4 | { 5 | } 6 | 7 | public class NoteTree : NoteTreeNode 8 | { 9 | public NoteTree? Parent { get; set; } 10 | 11 | public Event Note { get; set; } 12 | 13 | public List Children { get; set; } = new(); 14 | 15 | 16 | public int RenderLevel => Parent == null ? 0 : // If no parent then level 0 17 | Parent.RenderLevel + (Parent.Children.Count > 1 ? 1 : 0); // Else we have one level more than the parent, unless we're only child 18 | 19 | public NoteTree(Event note, NoteTree? parent = null) 20 | { 21 | Note = note; 22 | Parent = parent; 23 | } 24 | 25 | public NoteTree? Find(string id) 26 | { 27 | return Find(id, out _); 28 | } 29 | 30 | public NoteTree? Find(string id, out bool exceededMax, int startChildIndex = 0, int? maxChildAllowed = null) 31 | { 32 | var isReplaceableId = EventExtension.IsReplaceableId(id); 33 | if ((!isReplaceableId && Note.Id == id) || (isReplaceableId && Note.ReplaceableId == id)) 34 | { 35 | exceededMax = false; 36 | return this; 37 | } 38 | return Children.Find(id, out exceededMax, startChildIndex, maxChildAllowed); 39 | } 40 | 41 | public bool Exists(string id) 42 | { 43 | var isReplaceableId = EventExtension.IsReplaceableId(id); 44 | return (!isReplaceableId && Note.Id == id) || (isReplaceableId && Note.ReplaceableId == id) || Children.Exists(id); 45 | } 46 | 47 | public List AllNotes() 48 | { 49 | return new[] { Note }.Union(Children.AllNotes()).ToList(); 50 | } 51 | } 52 | 53 | public static class NoteTreeExtensions 54 | { 55 | public static bool Exists(this List chain, string id) 56 | { 57 | return chain.Any(c => c.Exists(id)); 58 | } 59 | 60 | public static NoteTree? Find(this List chain, string id) 61 | { 62 | return Find(chain, id, out _); 63 | } 64 | 65 | public static NoteTree? Find(this List chain, string id, out bool exceededMax, int startChildIndex = 0, int? maxChildAllowed = null) 66 | { 67 | exceededMax = false; 68 | for (int i = startChildIndex; i < chain.Count; i++) 69 | { 70 | var ret = chain[i].Find(id, out exceededMax); 71 | if (maxChildAllowed.HasValue) 72 | exceededMax |= i - startChildIndex >= maxChildAllowed; 73 | if (ret != null) return ret; 74 | } 75 | return null; 76 | } 77 | 78 | public static List AllNotes(this List chain) 79 | { 80 | return chain.SelectMany(child => child.AllNotes()).ToList(); 81 | } 82 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Nostrid.Core/Shared/FollowsCounter.razor: -------------------------------------------------------------------------------- 1 | @code{ 2 | [Parameter] 3 | public string AccountId { get; set; } 4 | 5 | [Parameter] 6 | public bool CountFollowers { get; set; } /* If false then count follows */ 7 | } 8 | 9 | @using Nostrid.Data; 10 | @using Nostrid.Misc; 11 | @using Nostrid.Model; 12 | 13 | @implements IDisposable 14 | 15 | @inject EventDatabase eventDatabase 16 | @inject AccountService accountService 17 | 18 | 19 | @(CountFollowers ? eventDatabase.GetFollowerCount(AccountId) : eventDatabase.GetFollowCount(AccountId)) 20 | 21 | 22 | @code 23 | { 24 | private Task? updateAfterChange; 25 | private const int StateHasChangedAfterSeconds = 3; 26 | 27 | protected override void OnParametersSet() 28 | { 29 | base.OnParametersSet(); 30 | 31 | accountService.AccountFollowsChanged += AccountFollowsChanged; 32 | accountService.AccountFollowersChanged += AccountFollowersChanged; 33 | } 34 | 35 | private static object lockobj = new(); 36 | 37 | private void AccountFollowsChanged(object? sender, (string accountId, List follows) data) 38 | { 39 | if (_disposed) 40 | return; 41 | lock (lockobj) 42 | { 43 | if (updateAfterChange != null) 44 | return; 45 | updateAfterChange = Task.Delay(TimeSpan.FromSeconds(StateHasChangedAfterSeconds)).ContinueWith((_) => 46 | { 47 | updateAfterChange = null; 48 | InvokeAsync(() => StateHasChanged()); 49 | }); 50 | } 51 | } 52 | 53 | private void AccountFollowersChanged(object? sender, string accountId) 54 | { 55 | if (_disposed) 56 | return; 57 | lock (lockobj) 58 | { 59 | if (updateAfterChange != null) 60 | return; 61 | updateAfterChange = Task.Delay(TimeSpan.FromSeconds(StateHasChangedAfterSeconds)).ContinueWith((_) => 62 | { 63 | updateAfterChange = null; 64 | InvokeAsync(() => StateHasChanged()); 65 | }); 66 | } 67 | } 68 | 69 | #region Dispose 70 | private bool _disposed; 71 | 72 | public void Dispose() => Dispose(true); 73 | 74 | protected virtual void Dispose(bool disposing) 75 | { 76 | if (!_disposed) 77 | { 78 | if (disposing) 79 | { 80 | Cleanup(); 81 | } 82 | 83 | _disposed = true; 84 | } 85 | } 86 | 87 | private void Cleanup() 88 | { 89 | accountService.AccountFollowsChanged -= AccountFollowsChanged; 90 | accountService.AccountFollowersChanged -= AccountFollowersChanged; 91 | } 92 | #endregion 93 | } 94 | -------------------------------------------------------------------------------- /Nostrid.Core/Data/Relays/DmSubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | using NNostr.Client; 2 | using Nostrid.Model; 3 | 4 | namespace Nostrid.Data.Relays; 5 | 6 | public class DmSubscriptionFilter : SubscriptionFilter, IDbFilter 7 | { 8 | private readonly string account1; 9 | private readonly string? account2; 10 | 11 | public DmSubscriptionFilter(string account1, string? account2) 12 | { 13 | this.account1 = account1; 14 | this.account2 = account2; 15 | } 16 | 17 | public DmSubscriptionFilter(string account1) 18 | { 19 | this.account1 = account1; 20 | } 21 | 22 | public override NostrSubscriptionFilter[] GetFilters() 23 | { 24 | if (account2 == null) 25 | { 26 | return new[] { 27 | new NostrSubscriptionFilter() { Authors = new[]{ account1 }, Kinds = new[]{ NostrKind.DM }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 28 | new NostrSubscriptionFilter() { PublicKey = new[]{ account1 } , Kinds = new[]{ NostrKind.DM }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 29 | }; 30 | } 31 | 32 | return new[] { 33 | new NostrSubscriptionFilter() { Authors = new[]{ account1 }, PublicKey = new[]{ account2 } , Kinds = new[]{ NostrKind.DM }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 34 | new NostrSubscriptionFilter() { Authors = new[]{ account2 }, PublicKey = new[]{ account1 } , Kinds = new[]{ NostrKind.DM }, Limit = LimitFilterData?.Limit, Since = LimitFilterData?.Since, Until = LimitFilterData?.Until }, 35 | }; 36 | } 37 | 38 | public override SubscriptionFilter Clone() 39 | { 40 | return new DmSubscriptionFilter(account1, account2); 41 | } 42 | 43 | public IQueryable ApplyDbFilter(IQueryable events) 44 | { 45 | var query = events; 46 | if (LimitFilterData.Since.HasValue) 47 | { 48 | var since = LimitFilterData.Since.Value.ToUnixTimeSeconds(); 49 | query = query.Where(e => e.CreatedAtCurated >= since); 50 | } 51 | if (LimitFilterData.Until.HasValue) 52 | { 53 | var until = LimitFilterData.Until.Value.ToUnixTimeSeconds(); 54 | query = query.Where(e => e.CreatedAtCurated <= until); 55 | } 56 | if (account2 == null) 57 | { 58 | return query.Where(e => e.Kind == NostrKind.DM && (e.PublicKey == account1 || e.DmToId == account1)); 59 | } 60 | return query.Where(e => e.Kind == NostrKind.DM && ((e.PublicKey == account1 && e.DmToId == account2) || (e.PublicKey == account2 && e.DmToId == account1))); 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /Nostrid.Core/Migrations/20230215113014_DirectMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Nostrid.Migrations 7 | { 8 | /// 9 | public partial class DirectMessages : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AddColumn( 15 | name: "DmToId", 16 | table: "Events", 17 | type: "TEXT", 18 | nullable: true); 19 | 20 | migrationBuilder.CreateTable( 21 | name: "DmPairs", 22 | columns: table => new 23 | { 24 | Id = table.Column(type: "INTEGER", nullable: false) 25 | .Annotation("Sqlite:Autoincrement", true), 26 | AccountL = table.Column(type: "TEXT", nullable: false), 27 | AccountH = table.Column(type: "TEXT", nullable: false), 28 | LastReadL = table.Column(type: "TEXT", nullable: false), 29 | LastReadH = table.Column(type: "TEXT", nullable: false) 30 | }, 31 | constraints: table => 32 | { 33 | table.PrimaryKey("PK_DmPairs", x => x.Id); 34 | }); 35 | 36 | migrationBuilder.CreateIndex( 37 | name: "IX_Events_Kind_DmToId", 38 | table: "Events", 39 | columns: new[] { "Kind", "DmToId" }); 40 | 41 | migrationBuilder.CreateIndex( 42 | name: "IX_DmPairs_AccountH", 43 | table: "DmPairs", 44 | column: "AccountH"); 45 | 46 | migrationBuilder.CreateIndex( 47 | name: "IX_DmPairs_AccountL", 48 | table: "DmPairs", 49 | column: "AccountL"); 50 | 51 | migrationBuilder.CreateIndex( 52 | name: "IX_DmPairs_AccountL_AccountH", 53 | table: "DmPairs", 54 | columns: new[] { "AccountL", "AccountH" }, 55 | unique: true); 56 | 57 | migrationBuilder.Sql("DELETE FROM Events WHERE Kind = 4"); 58 | } 59 | 60 | /// 61 | protected override void Down(MigrationBuilder migrationBuilder) 62 | { 63 | migrationBuilder.DropTable( 64 | name: "DmPairs"); 65 | 66 | migrationBuilder.DropIndex( 67 | name: "IX_Events_Kind_DmToId", 68 | table: "Events"); 69 | 70 | migrationBuilder.DropColumn( 71 | name: "DmToId", 72 | table: "Events"); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Nostrid.Core/Migrations/20230129160606_FollowsTable.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Nostrid.Migrations 6 | { 7 | /// 8 | public partial class FollowsTable : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.DropColumn( 14 | name: "FollowList", 15 | table: "Accounts"); 16 | 17 | migrationBuilder.DropColumn( 18 | name: "FollowerList", 19 | table: "Accounts"); 20 | 21 | migrationBuilder.CreateTable( 22 | name: "Follows", 23 | columns: table => new 24 | { 25 | Id = table.Column(type: "INTEGER", nullable: false) 26 | .Annotation("Sqlite:Autoincrement", true), 27 | AccountId = table.Column(type: "TEXT", nullable: false), 28 | FollowId = table.Column(type: "TEXT", nullable: false) 29 | }, 30 | constraints: table => 31 | { 32 | table.PrimaryKey("PK_Follows", x => x.Id); 33 | }); 34 | 35 | migrationBuilder.CreateIndex( 36 | name: "IX_Follows_AccountId", 37 | table: "Follows", 38 | column: "AccountId"); 39 | 40 | migrationBuilder.CreateIndex( 41 | name: "IX_Follows_AccountId_FollowId", 42 | table: "Follows", 43 | columns: new[] { "AccountId", "FollowId" }); 44 | 45 | migrationBuilder.CreateIndex( 46 | name: "IX_Follows_FollowId", 47 | table: "Follows", 48 | column: "FollowId"); 49 | 50 | // Delete all follows and start again 51 | migrationBuilder.DeleteData("Events", "Kind", "3"); 52 | migrationBuilder.Sql("UPDATE Accounts SET FollowsLastUpdate=NULL"); 53 | } 54 | 55 | /// 56 | protected override void Down(MigrationBuilder migrationBuilder) 57 | { 58 | migrationBuilder.DropTable( 59 | name: "Follows"); 60 | 61 | migrationBuilder.AddColumn( 62 | name: "FollowList", 63 | table: "Accounts", 64 | type: "TEXT", 65 | nullable: false, 66 | defaultValue: ""); 67 | 68 | migrationBuilder.AddColumn( 69 | name: "FollowerList", 70 | table: "Accounts", 71 | type: "TEXT", 72 | nullable: false, 73 | defaultValue: ""); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Nostrid.Photino/Program.cs: -------------------------------------------------------------------------------- 1 | using Ganss.Xss; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using NNostr.Client; 4 | using Nostrid.Data; 5 | using Nostrid.Data.Relays; 6 | using Nostrid.Externals; 7 | using Nostrid.Interfaces; 8 | using Nostrid.Model; 9 | using Photino.Blazor; 10 | using System; 11 | using System.IO; 12 | using System.Threading.Tasks; 13 | 14 | namespace Nostrid.Photino 15 | { 16 | class Program 17 | { 18 | [STAThread] 19 | static void Main(string[] args) 20 | { 21 | var builder = PhotinoBlazorAppBuilder.CreateDefault(args); 22 | 23 | builder.Services 24 | .AddLogging(); 25 | 26 | // register root component and selector 27 | builder.RootComponents.Add
("#app"); 28 | 29 | builder.Services.AddSingleton(); 30 | builder.Services.AddSingleton(); 31 | builder.Services.AddSingleton(); 32 | builder.Services.AddSingleton(); 33 | builder.Services.AddSingleton(); 34 | builder.Services.AddSingleton(); 35 | builder.Services.AddSingleton(); 36 | builder.Services.AddSingleton(); 37 | builder.Services.AddSingleton(); 38 | builder.Services.AddSingleton(); 39 | builder.Services.AddSingleton(); 40 | builder.Services.AddSingleton(); 41 | builder.Services.AddSingleton(); 42 | builder.Services.AddSingleton(); 43 | builder.Services.AddSingleton(); 44 | builder.Services.AddSingleton(); 45 | builder.Services.AddSingleton(); 46 | builder.Services.AddSingleton(); 47 | builder.Services.AddSingleton(); 48 | builder.Services.AddSingleton(); 49 | builder.Services.AddSingleton(); 50 | 51 | var app = builder.Build(); 52 | 53 | var eventDatabase = app.Services.GetRequiredService(); 54 | eventDatabase.InitDatabase(DbConstants.DatabasePath); 55 | 56 | 57 | // customize window 58 | app.MainWindow 59 | .SetIconFile("favicon.ico") 60 | .SetTitle("Nostrid"); 61 | 62 | AppDomain.CurrentDomain.UnhandledException += (sender, error) => 63 | { 64 | app.MainWindow.OpenAlertWindow("Fatal exception", error.ExceptionObject.ToString()); 65 | }; 66 | 67 | app.Run(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Nostrid.Core/Externals/NostrImgMediaService.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Nostrid.Misc; 3 | using System.Net.Http.Headers; 4 | 5 | namespace Nostrid.Externals 6 | { 7 | public class NostrImgMediaService : IMediaService 8 | { 9 | public string Name => "nostrimg.com"; 10 | 11 | public int MaxSize { get => 5 * 1024 * 1024; } 12 | 13 | public async Task UploadFile(Stream data, string filename, string mimeType, Action progress) 14 | { 15 | using var httpClient = new HttpClient(); 16 | using var progressStream = new ProgressStream(data); 17 | progressStream.UpdateProgress += (s, e) => progress(e); 18 | using var httpContent = new MultipartFormDataContent(); 19 | using var fileContent = new StreamContent(progressStream); 20 | fileContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); 21 | httpContent.Add(fileContent, "image", filename); 22 | var response = await httpClient.PostAsync("https://nostrimg.com/api/upload", httpContent); 23 | if (response.IsSuccessStatusCode) 24 | { 25 | var responseText = await response.Content.ReadAsStringAsync(); 26 | if (responseText.IsNotNullOrEmpty()) 27 | { 28 | var responseObj = JsonConvert.DeserializeObject(responseText); 29 | if (responseObj != null && responseObj.Success && Uri.IsWellFormedUriString(responseObj.ImageUrl, UriKind.Absolute)) 30 | { 31 | return new Uri(responseObj.ImageUrl); 32 | } 33 | } 34 | } 35 | return null; 36 | } 37 | } 38 | 39 | public class NostrImgData 40 | { 41 | [JsonProperty("link")] 42 | public string Link { get; set; } 43 | } 44 | 45 | public class NostrImgRoot 46 | { 47 | [JsonProperty("data")] 48 | public NostrImgData Data { get; set; } 49 | 50 | [JsonProperty("route")] 51 | public string Route { get; set; } 52 | 53 | [JsonProperty("url")] 54 | public string Url { get; set; } 55 | 56 | [JsonProperty("imageUrl")] 57 | public string ImageUrl { get; set; } 58 | 59 | [JsonProperty("fileName")] 60 | public string FileName { get; set; } 61 | 62 | [JsonProperty("fileID")] 63 | public string FileID { get; set; } 64 | 65 | [JsonProperty("message")] 66 | public string Message { get; set; } 67 | 68 | [JsonProperty("lightningDestination")] 69 | public string LightningDestination { get; set; } 70 | 71 | [JsonProperty("lightningPaymentLink")] 72 | public string LightningPaymentLink { get; set; } 73 | 74 | [JsonProperty("success")] 75 | public bool Success { get; set; } 76 | 77 | [JsonProperty("status")] 78 | public int Status { get; set; } 79 | } 80 | } 81 | --------------------------------------------------------------------------------