<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <updated></updated>
  <generator>https://nostr.ae</generator>

  <title>Nostr notes by </title>
  <author>
    <name></name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://nostr.ae/npub127u525u3l674p6l40c72u4ptxvumhfa0hu4pyjztnrw7vpppmvrqun5r64.rss" />
  <link href="https://nostr.ae/npub127u525u3l674p6l40c72u4ptxvumhfa0hu4pyjztnrw7vpppmvrqun5r64" />
  <id>https://nostr.ae/npub127u525u3l674p6l40c72u4ptxvumhfa0hu4pyjztnrw7vpppmvrqun5r64</id>
  <icon></icon>
  <logo></logo>




  <entry>
    <id>https://nostr.ae/nevent1qqs8w8kltpqh82yhe474vv3klvav69unky8czpqwmvx5dy8cdte32nszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvjxu7yx</id>
    
      <title type="html">Build manifest for get_file_hash v0.4.7</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs8w8kltpqh82yhe474vv3klvav69unky8czpqwmvx5dy8cdte32nszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvjxu7yx" />
    <content type="html">
      In reply to &lt;a href=&#39;/nevent1qqsqdvqnj92a0ds8vjmrjkgtg9xlp37mztezh8t6fhd7qq9atlt60zg7su9sv&#39;&gt;nevent1q…u9sv&lt;/a&gt;&lt;br/&gt;_________________________&lt;br/&gt;&lt;br/&gt;Build manifest for get_file_hash v0.4.7
    </content>
    <updated>2026-04-05T05:55:59Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsqdvqnj92a0ds8vjmrjkgtg9xlp37mztezh8t6fhd7qq9atlt60zgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvgn3u97</id>
    
      <title type="html">&amp;lt;?xml version=&amp;#39;1.0&amp;#39; ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsqdvqnj92a0ds8vjmrjkgtg9xlp37mztezh8t6fhd7qq9atlt60zgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvgn3u97" />
    <content type="html">
      &amp;lt;?xml version=&amp;#39;1.0&amp;#39; encoding=&amp;#39;windows-1252&amp;#39;?&amp;gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  Copyright (C) 2017 Christopher R. Field.&lt;br/&gt;&lt;br/&gt;  Licensed under the Apache License, Version 2.0 (the &amp;#34;License&amp;#34;);&lt;br/&gt;  you may not use this file except in compliance with the License.&lt;br/&gt;  You may obtain a copy of the License at&lt;br/&gt;&lt;br/&gt;  &lt;a href=&#34;http://www.apache.org/licenses/LICENSE-2.0&#34;&gt;http://www.apache.org/licenses/LICENSE-2.0&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;  Unless required by applicable law or agreed to in writing, software&lt;br/&gt;  distributed under the License is distributed on an &amp;#34;AS IS&amp;#34; BASIS,&lt;br/&gt;  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&lt;br/&gt;  See the License for the specific language governing permissions and&lt;br/&gt;  limitations under the License.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  The &amp;#34;cargo wix&amp;#34; subcommand provides a variety of predefined variables available&lt;br/&gt;  for customization of this template. The values for each variable are set at&lt;br/&gt;  installer creation time. The following variables are available:&lt;br/&gt;&lt;br/&gt;  TargetTriple      = The rustc target triple name.&lt;br/&gt;  TargetEnv         = The rustc target environment. This is typically either&lt;br/&gt;                      &amp;#34;msvc&amp;#34; or &amp;#34;gnu&amp;#34; depending on the toolchain downloaded and&lt;br/&gt;                      installed.&lt;br/&gt;  TargetVendor      = The rustc target vendor. This is typically &amp;#34;pc&amp;#34;, but Rust&lt;br/&gt;                      does support other vendors, like &amp;#34;uwp&amp;#34;.&lt;br/&gt;  CargoTargetBinDir = The complete path to the directory containing the&lt;br/&gt;                      binaries (exes) to include. The default would be&lt;br/&gt;                      &amp;#34;target\release\&amp;#34;. If an explicit rustc target triple is&lt;br/&gt;                      used, i.e. cross-compiling, then the default path would&lt;br/&gt;                      be &amp;#34;target\&amp;lt;CARGO_TARGET&amp;gt;\&amp;lt;CARGO_PROFILE&amp;gt;&amp;#34;,&lt;br/&gt;                      where &amp;#34;&amp;lt;CARGO_TARGET&amp;gt;&amp;#34; is replaced with the &amp;#34;CargoTarget&amp;#34;&lt;br/&gt;                      variable value and &amp;#34;&amp;lt;CARGO_PROFILE&amp;gt;&amp;#34; is replaced with the&lt;br/&gt;                      value from the &amp;#34;CargoProfile&amp;#34; variable. This can also&lt;br/&gt;                      be overridden manually with the &amp;#34;target-bin-dir&amp;#34; flag.&lt;br/&gt;  CargoTargetDir    = The path to the directory for the build artifacts, i.e.&lt;br/&gt;                      &amp;#34;target&amp;#34;.&lt;br/&gt;  CargoProfile      = The cargo profile used to build the binaries&lt;br/&gt;                      (usually &amp;#34;debug&amp;#34; or &amp;#34;release&amp;#34;).&lt;br/&gt;  Version           = The version for the installer. The default is the&lt;br/&gt;                      &amp;#34;Major.Minor.Fix&amp;#34; semantic versioning number of the Rust&lt;br/&gt;                      package.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  Please do not remove these pre-processor If-Else blocks. These are used with&lt;br/&gt;  the `cargo wix` subcommand to automatically determine the installation&lt;br/&gt;  destination for 32-bit versus 64-bit installers. Removal of these lines will&lt;br/&gt;  cause installation errors.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&amp;lt;?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?&amp;gt;&lt;br/&gt;    &amp;lt;?define PlatformProgramFilesFolder = &amp;#34;ProgramFiles64Folder&amp;#34; ?&amp;gt;&lt;br/&gt;&amp;lt;?else ?&amp;gt;&lt;br/&gt;    &amp;lt;?define PlatformProgramFilesFolder = &amp;#34;ProgramFilesFolder&amp;#34; ?&amp;gt;&lt;br/&gt;&amp;lt;?endif ?&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;Wix xmlns=&amp;#39;&lt;a href=&#34;http://schemas.microsoft.com/wix/2006/wi&amp;#39;&amp;gt&#34;&gt;http://schemas.microsoft.com/wix/2006/wi&amp;#39;&amp;gt&lt;/a&gt;;&lt;br/&gt;&lt;br/&gt;    &amp;lt;Product&lt;br/&gt;        Id=&amp;#39;*&amp;#39;&lt;br/&gt;        Name=&amp;#39;get_file_hash&amp;#39;&lt;br/&gt;        UpgradeCode=&amp;#39;DED69220-26E3-4406-B564-7F2B58C56F57&amp;#39;&lt;br/&gt;        Manufacturer=&amp;#39;gnostr admin@gnostr.org&amp;#39;&lt;br/&gt;        Language=&amp;#39;1033&amp;#39;&lt;br/&gt;        Codepage=&amp;#39;1252&amp;#39;&lt;br/&gt;        Version=&amp;#39;$(var.Version)&amp;#39;&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Package Id=&amp;#39;*&amp;#39;&lt;br/&gt;            Keywords=&amp;#39;Installer&amp;#39;&lt;br/&gt;            Description=&amp;#39;A utility crate providing a procedural macro to compute and embed file hashes at compile time.&amp;#39;&lt;br/&gt;            Manufacturer=&amp;#39;gnostr admin@gnostr.org&amp;#39;&lt;br/&gt;            InstallerVersion=&amp;#39;450&amp;#39;&lt;br/&gt;            Languages=&amp;#39;1033&amp;#39;&lt;br/&gt;            Compressed=&amp;#39;yes&amp;#39;&lt;br/&gt;            InstallScope=&amp;#39;perMachine&amp;#39;&lt;br/&gt;            SummaryCodepage=&amp;#39;1252&amp;#39;&lt;br/&gt;            /&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;MajorUpgrade&lt;br/&gt;            Schedule=&amp;#39;afterInstallInitialize&amp;#39;&lt;br/&gt;            DowngradeErrorMessage=&amp;#39;A newer version of [ProductName] is already installed. Setup will now exit.&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Media Id=&amp;#39;1&amp;#39; Cabinet=&amp;#39;media1.cab&amp;#39; EmbedCab=&amp;#39;yes&amp;#39; DiskPrompt=&amp;#39;CD-ROM #1&amp;#39;/&amp;gt;&lt;br/&gt;        &amp;lt;Property Id=&amp;#39;DiskPrompt&amp;#39; Value=&amp;#39;get_file_hash Installation&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Directory Id=&amp;#39;TARGETDIR&amp;#39; Name=&amp;#39;SourceDir&amp;#39;&amp;gt;&lt;br/&gt;            &amp;lt;Directory Id=&amp;#39;$(var.PlatformProgramFilesFolder)&amp;#39; Name=&amp;#39;PFiles&amp;#39;&amp;gt;&lt;br/&gt;                &amp;lt;Directory Id=&amp;#39;APPLICATIONFOLDER&amp;#39; Name=&amp;#39;get_file_hash&amp;#39;&amp;gt;&lt;br/&gt;                    &lt;br/&gt;                    &amp;lt;!--&lt;br/&gt;                      Enabling the license sidecar file in the installer is a four step process:&lt;br/&gt;&lt;br/&gt;                      1. Uncomment the `Component` tag and its contents.&lt;br/&gt;                      2. Change the value for the `Source` attribute in the `File` tag to a path&lt;br/&gt;                         to the file that should be included as the license sidecar file. The path&lt;br/&gt;                         can, and probably should be, relative to this file.&lt;br/&gt;                      3. Change the value for the `Name` attribute in the `File` tag to the&lt;br/&gt;                         desired name for the file when it is installed alongside the `bin` folder&lt;br/&gt;                         in the installation directory. This can be omitted if the desired name is&lt;br/&gt;                         the same as the file name.&lt;br/&gt;                      4. Uncomment the `ComponentRef` tag with the Id attribute value of &amp;#34;License&amp;#34;&lt;br/&gt;                         further down in this file.&lt;br/&gt;                    --&amp;gt;&lt;br/&gt;                    &amp;lt;!--&lt;br/&gt;                    &amp;lt;Component Id=&amp;#39;License&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                        &amp;lt;File Id=&amp;#39;LicenseFile&amp;#39; Name=&amp;#39;ChangeMe&amp;#39; DiskId=&amp;#39;1&amp;#39; Source=&amp;#39;C:\Path\To\File&amp;#39; KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                    &amp;lt;/Component&amp;gt;&lt;br/&gt;                    --&amp;gt;&lt;br/&gt;&lt;br/&gt;                    &amp;lt;Directory Id=&amp;#39;Bin&amp;#39; Name=&amp;#39;bin&amp;#39;&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;Path&amp;#39; Guid=&amp;#39;8DB39A25-8B99-4C25-8CF5-4704353C7C6E&amp;#39; KeyPath=&amp;#39;yes&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;Environment&lt;br/&gt;                                Id=&amp;#39;PATH&amp;#39;&lt;br/&gt;                                Name=&amp;#39;PATH&amp;#39;&lt;br/&gt;                                Value=&amp;#39;[Bin]&amp;#39;&lt;br/&gt;                                Permanent=&amp;#39;no&amp;#39;&lt;br/&gt;                                Part=&amp;#39;last&amp;#39;&lt;br/&gt;                                Action=&amp;#39;set&amp;#39;&lt;br/&gt;                                System=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary0&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe0&amp;#39;&lt;br/&gt;                                Name=&amp;#39;get_file_hash.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\get_file_hash.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary1&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe1&amp;#39;&lt;br/&gt;                                Name=&amp;#39;n34.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\n34.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary2&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe2&amp;#39;&lt;br/&gt;                                Name=&amp;#39;n34-relay.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\n34-relay.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary3&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe3&amp;#39;&lt;br/&gt;                                Name=&amp;#39;readme.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\readme.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                    &amp;lt;/Directory&amp;gt;&lt;br/&gt;                &amp;lt;/Directory&amp;gt;&lt;br/&gt;            &amp;lt;/Directory&amp;gt;&lt;br/&gt;        &amp;lt;/Directory&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Feature&lt;br/&gt;            Id=&amp;#39;Binaries&amp;#39;&lt;br/&gt;            Title=&amp;#39;Application&amp;#39;&lt;br/&gt;            Description=&amp;#39;Installs all binaries and the license.&amp;#39;&lt;br/&gt;            Level=&amp;#39;1&amp;#39;&lt;br/&gt;            ConfigurableDirectory=&amp;#39;APPLICATIONFOLDER&amp;#39;&lt;br/&gt;            AllowAdvertise=&amp;#39;no&amp;#39;&lt;br/&gt;            Display=&amp;#39;expand&amp;#39;&lt;br/&gt;            Absent=&amp;#39;disallow&amp;#39;&amp;gt;&lt;br/&gt;            &lt;br/&gt;            &amp;lt;!--&lt;br/&gt;              Uncomment the following `ComponentRef` tag to add the license&lt;br/&gt;              sidecar file to the installer.&lt;br/&gt;            --&amp;gt;&lt;br/&gt;            &amp;lt;!--&amp;lt;ComponentRef Id=&amp;#39;License&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary0&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary1&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary2&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary3&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;            &amp;lt;Feature&lt;br/&gt;                Id=&amp;#39;Environment&amp;#39;&lt;br/&gt;                Title=&amp;#39;PATH Environment Variable&amp;#39;&lt;br/&gt;                Description=&amp;#39;Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.&amp;#39;&lt;br/&gt;                Level=&amp;#39;1&amp;#39;&lt;br/&gt;                Absent=&amp;#39;allow&amp;#39;&amp;gt;&lt;br/&gt;                &amp;lt;ComponentRef Id=&amp;#39;Path&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;/Feature&amp;gt;&lt;br/&gt;        &amp;lt;/Feature&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;SetProperty Id=&amp;#39;ARPINSTALLLOCATION&amp;#39; Value=&amp;#39;[APPLICATIONFOLDER]&amp;#39; After=&amp;#39;CostFinalize&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the following `Icon` and `Property` tags to change the product icon.&lt;br/&gt;&lt;br/&gt;          The product icon is the graphic that appears in the Add/Remove&lt;br/&gt;          Programs control panel for the application.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;Icon Id=&amp;#39;ProductICO&amp;#39; SourceFile=&amp;#39;wix\Product.ico&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;Property Id=&amp;#39;ARPPRODUCTICON&amp;#39; Value=&amp;#39;ProductICO&amp;#39; /&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Property Id=&amp;#39;ARPHELPLINK&amp;#39; Value=&amp;#39;&lt;a href=&#34;https://github.com/gnostr-org/get_file_hash&amp;#39;/&amp;gt&#34;&gt;https://github.com/gnostr-org/get_file_hash&amp;#39;/&amp;gt&lt;/a&gt;;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;UI&amp;gt;&lt;br/&gt;            &amp;lt;UIRef Id=&amp;#39;WixUI_FeatureTree&amp;#39;/&amp;gt;&lt;br/&gt;            &lt;br/&gt;            &amp;lt;!--&lt;br/&gt;              Enabling the EULA dialog in the installer is a three step process:&lt;br/&gt;&lt;br/&gt;                1. Comment out or remove the two `Publish` tags that follow the&lt;br/&gt;                   `WixVariable` tag.&lt;br/&gt;                2. Uncomment the `&amp;lt;WixVariable Id=&amp;#39;WixUILicenseRtf&amp;#39; Value=&amp;#39;Path\to\Eula.rft&amp;#39;&amp;gt;` tag further down&lt;br/&gt;                3. Replace the `Value` attribute of the `WixVariable` tag with&lt;br/&gt;                   the path to a RTF file that will be used as the EULA and&lt;br/&gt;                   displayed in the license agreement dialog.&lt;br/&gt;            --&amp;gt;&lt;br/&gt;            &amp;lt;Publish Dialog=&amp;#39;WelcomeDlg&amp;#39; Control=&amp;#39;Next&amp;#39; Event=&amp;#39;NewDialog&amp;#39; Value=&amp;#39;CustomizeDlg&amp;#39; Order=&amp;#39;99&amp;#39;&amp;gt;1&amp;lt;/Publish&amp;gt;&lt;br/&gt;            &amp;lt;Publish Dialog=&amp;#39;CustomizeDlg&amp;#39; Control=&amp;#39;Back&amp;#39; Event=&amp;#39;NewDialog&amp;#39; Value=&amp;#39;WelcomeDlg&amp;#39; Order=&amp;#39;99&amp;#39;&amp;gt;1&amp;lt;/Publish&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;/UI&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Enabling the EULA dialog in the installer requires uncommenting&lt;br/&gt;          the following `WixUILicenseRTF` tag and changing the `Value`&lt;br/&gt;          attribute.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!-- &amp;lt;WixVariable Id=&amp;#39;WixUILicenseRtf&amp;#39; Value=&amp;#39;Relative\Path\to\Eula.rtf&amp;#39;/&amp;gt; --&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the next `WixVariable` tag to customize the installer&amp;#39;s&lt;br/&gt;          Graphical User Interface (GUI) and add a custom banner image across&lt;br/&gt;          the top of each screen. See the WiX Toolset documentation for details&lt;br/&gt;          about customization.&lt;br/&gt;&lt;br/&gt;          The banner BMP dimensions are 493 x 58 pixels.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;WixVariable Id=&amp;#39;WixUIBannerBmp&amp;#39; Value=&amp;#39;wix\Banner.bmp&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the next `WixVariable` tag to customize the installer&amp;#39;s&lt;br/&gt;          Graphical User Interface (GUI) and add a custom image to the first&lt;br/&gt;          dialog, or screen. See the WiX Toolset documentation for details about&lt;br/&gt;          customization.&lt;br/&gt;&lt;br/&gt;          The dialog BMP dimensions are 493 x 312 pixels.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;WixVariable Id=&amp;#39;WixUIDialogBmp&amp;#39; Value=&amp;#39;wix\Dialog.bmp&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;/Product&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;/Wix&amp;gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:55:44Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsxp5yuajfc0ffr8dan0e35uhezxu0upu9hp6dzkfxzn58k8rfzknqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv595ard</id>
    
      <title type="html">use std::process::Command; use std::fs; use sha2::{Digest, ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsxp5yuajfc0ffr8dan0e35uhezxu0upu9hp6dzkfxzn58k8rfzknqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv595ard" />
    <content type="html">
      use std::process::Command;&lt;br/&gt;use std::fs;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;fn calculate_sha256(file_path: &amp;amp;str) -&amp;gt; String {&lt;br/&gt;    let content = fs::read(file_path).expect(&amp;#34;Unable to read file&amp;#34;);&lt;br/&gt;    let mut hasher = Sha256::new();&lt;br/&gt;    hasher.update(&amp;amp;content);&lt;br/&gt;    hasher.finalize()&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|b| format!(&amp;#34;{:02x}&amp;#34;, b))&lt;br/&gt;        .collect()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn test_get_file_hash_binary_no_features() {&lt;br/&gt;    let output = Command::new(&amp;#34;cargo&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;run&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;--bin&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;get_file_hash&amp;#34;)&lt;br/&gt;        .output()&lt;br/&gt;        .expect(&amp;#34;Failed to execute command&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let stdout = String::from_utf8_lossy(&amp;amp;output.stdout);&lt;br/&gt;    let stderr = String::from_utf8_lossy(&amp;amp;output.stderr);&lt;br/&gt;&lt;br/&gt;    // Assert that the command ran successfully&lt;br/&gt;    assert!(output.status.success(), &amp;#34;Command failed with stderr: {}&amp;#34;, stderr);&lt;br/&gt;&lt;br/&gt;    // Manually calculate the hash of the binary&amp;#39;s source file&lt;br/&gt;    let expected_hash = calculate_sha256(&amp;#34;src/bin/get_file_hash.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // Assert that the output contains the correct hash&lt;br/&gt;    // Check for the raw hash first&lt;br/&gt;    assert!(stdout.contains(&amp;amp;expected_hash), &amp;#34;Output did not contain raw expected hash. Expected: {}, Actual: {}&amp;#34;, expected_hash, stdout);&lt;br/&gt;&lt;br/&gt;    // Then check for the formatted string, including backticks&lt;br/&gt;    // Use a regex-like check for more flexibility with newlines if needed, or refine to exact match&lt;br/&gt;    let expected_hash_line = format!(&amp;#34;*   **SHA-256 Hash:** `{}`&amp;#34;, expected_hash);&lt;br/&gt;    assert!(stdout.contains(&amp;amp;expected_hash_line), &amp;#34;Output did not contain expected hash line. Expected line: {}, Actual: {}&amp;#34;, expected_hash_line, stdout);&lt;br/&gt;&lt;br/&gt;    // Assert that the output contains &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    assert!(stdout.contains(&amp;#34;Integrity Verified.&amp;#34;), &amp;#34;Output did not contain &amp;#39;Integrity Verified.&amp;#39;. stdout: {}&amp;#34;, stdout);&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;Output from get_file_hash binary (no features):&lt;br/&gt;{}&amp;#34;, stdout);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn test_get_file_hash_binary_with_nostr_feature() {&lt;br/&gt;    let output = Command::new(&amp;#34;cargo&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;run&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;--bin&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;get_file_hash&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;--features&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;nostr&amp;#34;)&lt;br/&gt;        .output()&lt;br/&gt;        .expect(&amp;#34;Failed to execute command&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let stdout = String::from_utf8_lossy(&amp;amp;output.stdout);&lt;br/&gt;    let stderr = String::from_utf8_lossy(&amp;amp;output.stderr);&lt;br/&gt;&lt;br/&gt;    // Assert that the command ran successfully&lt;br/&gt;    assert!(output.status.success(), &amp;#34;Command failed with stderr: {}&amp;#34;, stderr);&lt;br/&gt;&lt;br/&gt;    // Manually calculate the hash of the binary&amp;#39;s source file&lt;br/&gt;    let expected_hash = calculate_sha256(&amp;#34;src/bin/get_file_hash.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // Assert that the output contains the correct hash&lt;br/&gt;    assert!(stdout.contains(&amp;amp;expected_hash), &amp;#34;Output did not contain raw expected hash. Expected: {}, Actual: {}&amp;#34;, expected_hash, stdout);&lt;br/&gt;&lt;br/&gt;    // Then check for the formatted string, including backticks&lt;br/&gt;    let expected_hash_line = format!(&amp;#34;*   **SHA-256 Hash:** `{}`&amp;#34;, expected_hash);&lt;br/&gt;    assert!(stdout.contains(&amp;amp;expected_hash_line), &amp;#34;Output did not contain expected hash line. Expected line: {}, Actual: {}&amp;#34;, expected_hash_line, stdout);&lt;br/&gt;&lt;br/&gt;    // Assert that the output contains &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    assert!(stdout.contains(&amp;#34;Integrity Verified.&amp;#34;), &amp;#34;Output did not contain &amp;#39;Integrity Verified.&amp;#39;. stdout: {}&amp;#34;, stdout);&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;Output from get_file_hash binary (with nostr feature):&lt;br/&gt;{}&amp;#34;, stdout);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:55:32Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs258xjg3h8ddwqsc2cfzvlhs6e8c4ce5fg2qeu8esllcv43yzvu4gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvt8zg83</id>
    
      <title type="html">//! A crate providing the `get_file_hash!` procedural macro. //! ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs258xjg3h8ddwqsc2cfzvlhs6e8c4ce5fg2qeu8esllcv43yzvu4gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvt8zg83" />
    <content type="html">
      //! A crate providing the `get_file_hash!` procedural macro.&lt;br/&gt;//!&lt;br/&gt;//! This macro allows you to compute the SHA-256 hash of a file at compile time,&lt;br/&gt;//! embedding the resulting hash string directly into your Rust executable.&lt;br/&gt;&lt;br/&gt;pub use get_file_hash_core::get_file_hash;&lt;br/&gt;&lt;br/&gt;/// The SHA-256 hash of this crate&amp;#39;s `build.rs` at the time of compilation.&lt;br/&gt;pub const BUILD_HASH: &amp;amp;str = env!(&amp;#34;BUILD_HASH&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// The SHA-256 hash of this crate&amp;#39;s `Cargo.toml` at the time of compilation.&lt;br/&gt;pub const CARGO_TOML_HASH: &amp;amp;str = env!(&amp;#34;CARGO_TOML_HASH&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// The SHA-256 hash of this crate&amp;#39;s `src/lib.rs` at the time of compilation.&lt;br/&gt;pub const LIB_HASH: &amp;amp;str = env!(&amp;#34;LIB_HASH&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// The name of the package as specified in Cargo.toml.&lt;br/&gt;pub const CARGO_PKG_NAME: &amp;amp;str = env!(&amp;#34;CARGO_PKG_NAME&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// The version of the package as specified in Cargo.toml.&lt;br/&gt;pub const CARGO_PKG_VERSION: &amp;amp;str = env!(&amp;#34;CARGO_PKG_VERSION&amp;#34;);&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;/// The git commit hash of the repository at the time of compilation.&lt;br/&gt;pub const GIT_COMMIT_HASH: &amp;amp;str = env!(&amp;#34;GIT_COMMIT_HASH&amp;#34;);&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;/// The git branch of the repository at the time of compilation.&lt;br/&gt;pub const GIT_BRANCH: &amp;amp;str = env!(&amp;#34;GIT_BRANCH&amp;#34;);&lt;br/&gt;&lt;br/&gt;#[cfg(test)]&lt;br/&gt;mod tests {&lt;br/&gt;    use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;    use super::*;&lt;br/&gt;&lt;br/&gt;    /// Verifies that the exported CARGO_TOML_HASH is not empty.&lt;br/&gt;    #[test]&lt;br/&gt;    fn test_injected_hash_exists() {&lt;br/&gt;        assert!(!BUILD_HASH.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified build.rs Hash:&lt;br/&gt;{}&amp;#34;, BUILD_HASH);&lt;br/&gt;&lt;br/&gt;        assert!(!CARGO_TOML_HASH.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified Cargo.toml Hash:&lt;br/&gt;{}&amp;#34;, CARGO_TOML_HASH);&lt;br/&gt;&lt;br/&gt;        assert!(!LIB_HASH.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified src/lib.rs Hash:\n{}&amp;#34;, LIB_HASH);&lt;br/&gt;&lt;br/&gt;        assert!(!CARGO_PKG_NAME.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified Package Name:\n{}&amp;#34;, CARGO_PKG_NAME);&lt;br/&gt;&lt;br/&gt;        assert!(!CARGO_PKG_VERSION.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified Package Version:\n{}&amp;#34;, CARGO_PKG_VERSION);&lt;br/&gt;&lt;br/&gt;        #[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;        {&lt;br/&gt;            assert!(!GIT_COMMIT_HASH.is_empty());&lt;br/&gt;            println!(&amp;#34;Verified Git Commit Hash:\n{}&amp;#34;, GIT_COMMIT_HASH);&lt;br/&gt;&lt;br/&gt;            assert!(!GIT_BRANCH.is_empty());&lt;br/&gt;            println!(&amp;#34;Verified Git Branch:\n{}&amp;#34;, GIT_BRANCH);&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Tests that the `get_file_hash!` macro correctly computes the SHA-256&lt;br/&gt;    /// hash of `lib.rs` and that it matches a manually computed hash of the&lt;br/&gt;    /// same file.&lt;br/&gt;    #[test]&lt;br/&gt;    fn test_get_lib_hash() {&lt;br/&gt;        let file_content = include_bytes!(&amp;#34;lib.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;        let mut hasher = Sha256::new();&lt;br/&gt;        hasher.update(file_content);&lt;br/&gt;        let expected_hash = hasher&lt;br/&gt;            .finalize()&lt;br/&gt;            .iter()&lt;br/&gt;            .map(|b| format!(&amp;#34;{:02x}&amp;#34;, b))&lt;br/&gt;            .collect::&amp;lt;String&amp;gt;();&lt;br/&gt;&lt;br/&gt;        let actual_hash = get_file_hash!(&amp;#34;lib.rs&amp;#34;);&lt;br/&gt;        assert_eq!(actual_hash, expected_hash);&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:55:11Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsxnl63cvkr5x50jwpzw5g4r9u6g0fv4jw55ylq0x5pnaxzjkfqghszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7mtqzs</id>
    
      <title type="html">Relay URL,Latitude,Longitude wot.nostr.party,36.1627,-86.7816 ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsxnl63cvkr5x50jwpzw5g4r9u6g0fv4jw55ylq0x5pnaxzjkfqghszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7mtqzs" />
    <content type="html">
      Relay URL,Latitude,Longitude&lt;br/&gt;wot.nostr.party,36.1627,-86.7816&lt;br/&gt;nostr.simplex.icu,50.1109,8.68213&lt;br/&gt;relay.snort.social,53.3498,-6.26031&lt;br/&gt;nostr.stakey.net,52.3676,4.90414&lt;br/&gt;relay.mccormick.cx,52.3563,4.95714&lt;br/&gt;nostr.overmind.lol,43.6532,-79.3832&lt;br/&gt;nostriches.club,43.6532,-79.3832&lt;br/&gt;kanagrovv-pyramid.kozow.com,43.4305,-83.9638&lt;br/&gt;relay.toastr.net,40.8054,-74.0241&lt;br/&gt;relay.primal.net,43.6532,-79.3832&lt;br/&gt;r.bitcoinhold.net,43.6532,-79.3832&lt;br/&gt;wot.dtonon.com,43.6532,-79.3832&lt;br/&gt;santo.iguanatech.net,40.8302,-74.1299&lt;br/&gt;relay2.ngengine.org,43.6532,-79.3832&lt;br/&gt;nostr.oxtr.dev,50.4754,12.3683&lt;br/&gt;simplex.icu,50.1109,8.68213&lt;br/&gt;bitchat.nostr1.com,38.6327,-90.1961&lt;br/&gt;relay.nostu.be,40.4167,-3.70329&lt;br/&gt;nostr.mifen.me,43.6532,-79.3832&lt;br/&gt;relay.vantis.ninja,43.6532,-79.3832&lt;br/&gt;nostr-kyomu-haskell.onrender.com,37.7775,-122.397&lt;br/&gt;nostr-relay.amethyst.name,39.0067,-77.4291&lt;br/&gt;relay.bitcoindistrict.org,43.6532,-79.3832&lt;br/&gt;nos.xmark.cc,50.6924,3.20113&lt;br/&gt;relay01.lnfi.network,35.6764,139.65&lt;br/&gt;nostr-dev.wellorder.net,45.5201,-122.99&lt;br/&gt;nostr.rblb.it,43.7094,10.6582&lt;br/&gt;offchain.pub,39.1585,-94.5728&lt;br/&gt;wot.nostr.place,32.7767,-96.797&lt;br/&gt;fanfares.nostr1.com,38.6327,-90.1961&lt;br/&gt;ephemeral.snowflare.cc,43.6532,-79.3832&lt;br/&gt;relay.visionfusen.org,43.6532,-79.3832&lt;br/&gt;nostr.carroarmato0.be,51.0368,3.21186&lt;br/&gt;social.amanah.eblessing.co,48.1046,11.6002&lt;br/&gt;relay.cyphernomad.com,60.4032,25.0321&lt;br/&gt;nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397&lt;br/&gt;relay.ngengine.org,43.6532,-79.3832&lt;br/&gt;relay.nosto.re,51.1792,5.89444&lt;br/&gt;rusty-uat.siberian-albacore.ts.net:8443,35.6764,139.65&lt;br/&gt;nostr.luisschwab.net,43.6532,-79.3832&lt;br/&gt;nostr.aruku.ovh,1.27994,103.849&lt;br/&gt;srtrelay.c-stellar.net,43.6532,-79.3832&lt;br/&gt;nostr.tagomago.me,3.139,101.687&lt;br/&gt;relay.threenine.services,51.5222,-0.62916&lt;br/&gt;nostr.robosats.org,64.1476,-21.9392&lt;br/&gt;nostr.2b9t.xyz,34.0549,-118.243&lt;br/&gt;strfry.apps3.slidestr.net,40.4167,-3.70329&lt;br/&gt;kitchen.zap.cooking,43.6532,-79.3832&lt;br/&gt;relay.homeinhk.xyz,45.5152,-122.678&lt;br/&gt;relay.seq1.net,43.6532,-79.3832&lt;br/&gt;relay.bornheimer.app,50.1109,8.68213&lt;br/&gt;relay.vrtmrz.net,43.6532,-79.3832&lt;br/&gt;nostr.red5d.dev,43.6532,-79.3832&lt;br/&gt;relay-freeharmonypeople.space,38.7223,-9.13934&lt;br/&gt;aaa-api.freefrom.space/v1/ws,43.6532,-79.3832&lt;br/&gt;dev.relay.edufeed.org,49.4521,11.0767&lt;br/&gt;nostr.myshosholoza.co.za,52.3913,4.66545&lt;br/&gt;nostr.plantroon.com,50.1013,8.62643&lt;br/&gt;wot.dergigi.com,64.1476,-21.9392&lt;br/&gt;npub1spxdug4m3y24hpx5crm0el4zhkk0wafs8kp6m0xu0wecygqej2xqq8gyhx.fips.network,43.6532,-79.3832&lt;br/&gt;nostrelay.circum.space,52.3676,4.90414&lt;br/&gt;relay.dreamith.to,43.6532,-79.3832&lt;br/&gt;relay.nostriches.club,43.6532,-79.3832&lt;br/&gt;nostr-relayrs.gateway.in.th,15.5163,103.194&lt;br/&gt;nostr.chrissexton.org,43.6532,-79.3832&lt;br/&gt;relay.flashapp.me,43.652,-79.3633&lt;br/&gt;nostr-relay-1.trustlessenterprise.com,43.6532,-79.3832&lt;br/&gt;strfry.shock.network,39.0438,-77.4874&lt;br/&gt;relay.snotr.nl:49999,52.0195,4.42946&lt;br/&gt;spatia-arcana.com,34.0362,-118.443&lt;br/&gt;nostr.computingcache.com,34.0356,-118.442&lt;br/&gt;nostr.self-determined.de,53.5,10.25&lt;br/&gt;relay.sharegap.net,43.6532,-79.3832&lt;br/&gt;spookstr2.nostr1.com,38.6327,-90.1961&lt;br/&gt;v-relay.d02.vrtmrz.net,34.6937,135.502&lt;br/&gt;bridge.tagomago.me,3.139,101.687&lt;br/&gt;antiprimal.net,43.6532,-79.3832&lt;br/&gt;relay.nostrdice.com,-33.8688,151.209&lt;br/&gt;purpura.cloud,43.6532,-79.3832&lt;br/&gt;espelho.girino.org,43.6532,-79.3832&lt;br/&gt;relay.mostro.network,40.8302,-74.1299&lt;br/&gt;temp.iris.to,43.6532,-79.3832&lt;br/&gt;pyramid.self-determined.de,53.5,10.25&lt;br/&gt;relay.jeffg.fyi,43.6532,-79.3832&lt;br/&gt;nostr.aruku.kro.kr,37.3589,127.115&lt;br/&gt;chat-relay.zap-work.com,43.6532,-79.3832&lt;br/&gt;relay.islandbitcoin.com,12.8498,77.6545&lt;br/&gt;nostr.zoracle.org,45.6018,-121.185&lt;br/&gt;relay-dev.satlantis.io,40.8302,-74.1299&lt;br/&gt;relay.earthly.city,34.0362,-118.443&lt;br/&gt;speakeasy.cellar.social,49.4543,11.0746&lt;br/&gt;relay.bnos.space,43.6532,-79.3832&lt;br/&gt;relay.henryxplace.eu.org:9988,31.2304,121.474&lt;br/&gt;relay.bitmacro.cloud,43.6532,-79.3832&lt;br/&gt;nostr.thebiglake.org,32.71,-96.6745&lt;br/&gt;relay.lab.rytswd.com,49.4543,11.0746&lt;br/&gt;nostr.nadajnik.org,50.1109,8.68213&lt;br/&gt;relay.evanverma.com,40.8302,-74.1299&lt;br/&gt;relay2.angor.io,48.1046,11.6002&lt;br/&gt;prl.plus,55.7628,37.5983&lt;br/&gt;myvoiceourstory.org,37.3598,-121.981&lt;br/&gt;relay.arx-ccn.com,50.4754,12.3683&lt;br/&gt;wot.sudocarlos.com,43.6532,-79.3832&lt;br/&gt;relayrs.notoshi.win,43.6532,-79.3832&lt;br/&gt;nostrcity-club.fly.dev,48.8575,2.35138&lt;br/&gt;relay.tagayasu.xyz,45.4215,-75.6972&lt;br/&gt;nostr.blankfors.se,60.1699,24.9384&lt;br/&gt;nrs-01.darkcloudarcade.com,39.1008,-94.5811&lt;br/&gt;relay.lightning.pub,39.0438,-77.4874&lt;br/&gt;nostr-02.yakihonne.com,1.32123,103.695&lt;br/&gt;relay.nostrverse.net,43.6532,-79.3832&lt;br/&gt;nostr.wecsats.io,43.6532,-79.3832&lt;br/&gt;relay.illuminodes.com,47.6062,-122.332&lt;br/&gt;api.freefrom.space/v1/ws,43.6532,-79.3832&lt;br/&gt;nostr-relay.psfoundation.info,39.0438,-77.4874&lt;br/&gt;relay.samt.st,40.8302,-74.1299&lt;br/&gt;nostr-relay.cbrx.io,43.6532,-79.3832&lt;br/&gt;inbox.mycelium.social,38.627,-90.1994&lt;br/&gt;relay.anmore.me,49.281,-123.117&lt;br/&gt;no.str.cr,10.074,-84.2155&lt;br/&gt;nstr.a0a1.space,52.3563,4.95714&lt;br/&gt;relay.typedcypher.com,51.5072,-0.127586&lt;br/&gt;relay.bitmacro.pro,43.6532,-79.3832&lt;br/&gt;relay.nostrzh.org,43.6532,-79.3832&lt;br/&gt;ynostr.yael.at,60.1699,24.9384&lt;br/&gt;nostr-relay.zeabur.app,25.0797,121.234&lt;br/&gt;dynasty.libretechsystems.xyz,55.4724,9.87335&lt;br/&gt;nostr.bitcoiner.social,47.6743,-117.112&lt;br/&gt;nostr.girino.org,43.6532,-79.3832&lt;br/&gt;nostr2.girino.org,43.6532,-79.3832&lt;br/&gt;nostr-verified.wellorder.net,45.5201,-122.99&lt;br/&gt;relay.fundstr.me,42.3601,-71.0589&lt;br/&gt;relay.mapboss.co.th,13.7234,100.784&lt;br/&gt;relay.qstr.app,51.5072,-0.127586&lt;br/&gt;nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832&lt;br/&gt;relay.klabo.world,47.674,-122.122&lt;br/&gt;relay.minibolt.info,43.6532,-79.3832&lt;br/&gt;x.kojira.io,43.6532,-79.3832&lt;br/&gt;relay-dev.gulugulu.moe,43.6532,-79.3832&lt;br/&gt;relay.nostriot.com,41.5695,-83.9786&lt;br/&gt;relayone.soundhsa.com,39.1008,-94.5811&lt;br/&gt;nr.yay.so,46.2126,6.1154&lt;br/&gt;relay.bithome.site,52.3563,4.95714&lt;br/&gt;relay.damus.io,43.6532,-79.3832&lt;br/&gt;nostr.mikoshi.de,50.1109,8.68213&lt;br/&gt;nostr.defucc.me,50.1109,8.68213&lt;br/&gt;relay.malxte.de,52.52,13.405&lt;br/&gt;relay.orangepill.ovh,49.1689,-0.358841&lt;br/&gt;bbw-nostr.xyz,41.5284,-87.4237&lt;br/&gt;kasztanowa.bieda.it,43.6532,-79.3832&lt;br/&gt;bitcoiner.social,47.6743,-117.112&lt;br/&gt;relay.lacompagniemaximus.com,45.3147,-73.8785&lt;br/&gt;relay.mostr.pub,43.6532,-79.3832&lt;br/&gt;relay.lanavault.space,60.1699,24.9384&lt;br/&gt;kotukonostr.onrender.com,37.7775,-122.397&lt;br/&gt;relay.ditto.pub,43.6532,-79.3832&lt;br/&gt;relay.erybody.com,41.4513,-81.7021&lt;br/&gt;nostr.dlcdevkit.com,40.0992,-83.1141&lt;br/&gt;ribo.us.nostria.app,41.5868,-93.625&lt;br/&gt;relay.paulstephenborile.com,49.4543,11.0746&lt;br/&gt;testnet-relay.samt.st,40.8302,-74.1299&lt;br/&gt;relay.purplefrog.cloud,35.6916,139.768&lt;br/&gt;relay.agorist.space,52.3734,4.89406&lt;br/&gt;nostr-relay.zimage.com,34.0549,-118.243&lt;br/&gt;nostr.azzamo.net,52.2633,21.0283&lt;br/&gt;strfry.elswa-dev.online,50.1109,8.68213&lt;br/&gt;wot.shaving.kiwi,43.6532,-79.3832&lt;br/&gt;okn.czas.plus,50.1109,8.68213&lt;br/&gt;bcast.seutoba.com.br,43.6532,-79.3832&lt;br/&gt;relay.sigit.io,50.4754,12.3683&lt;br/&gt;syb.lol,34.0549,-118.243&lt;br/&gt;relay.libernet.app,43.6532,-79.3832&lt;br/&gt;relay.angor.io,48.1046,11.6002&lt;br/&gt;relay.staging.commonshub.brussels,49.4543,11.0746&lt;br/&gt;strfry.atlantislabs.space,43.6532,-79.3832&lt;br/&gt;nostr.wom.wtf,43.6532,-79.3832&lt;br/&gt;nostrride.io,37.3986,-121.964&lt;br/&gt;nostr.dpinkerton.com,39.1008,-94.5811&lt;br/&gt;r.0kb.io,32.789,-96.7989&lt;br/&gt;nostr.hekster.org,37.3986,-121.964&lt;br/&gt;satsage.xyz,37.3986,-121.964&lt;br/&gt;nostr.islandarea.net,35.4669,-97.6473&lt;br/&gt;ve.agorawlc.com,50.4754,12.3683&lt;br/&gt;relay.openfarmtools.org,60.1699,24.9384&lt;br/&gt;top.testrelay.top,43.6532,-79.3832&lt;br/&gt;relay-rpi.edufeed.org,49.4521,11.0767&lt;br/&gt;pyramid.cult.cash,32.9483,-96.7299&lt;br/&gt;relay.edino.net,56.6268,47.9193&lt;br/&gt;nostr.snowbla.de,60.1699,24.9384&lt;br/&gt;relay.wavefunc.live,39.7392,-104.99&lt;br/&gt;tenex.chat,50.4754,12.3683&lt;br/&gt;relay.getsafebox.app,43.6532,-79.3832&lt;br/&gt;nostr.bond,50.1109,8.68213&lt;br/&gt;nostrelites.org,41.8781,-87.6298&lt;br/&gt;relay.plebeian.market,50.1109,8.68213&lt;br/&gt;relay.laantungir.net,-19.4692,-42.5315&lt;br/&gt;relay.decentnewsroom.com,50.4754,12.3683&lt;br/&gt;nostr-relay.nextblockvending.com,47.2343,-119.853&lt;br/&gt;relay.spacetomatoes.net,42.3601,-71.0589&lt;br/&gt;nostrbtc.com,43.6532,-79.3832&lt;br/&gt;relay.puresignal.news,43.6532,-79.3832&lt;br/&gt;relay-testnet.k8s.layer3.news,37.3387,-121.885&lt;br/&gt;relay.binaryrobot.com,43.6532,-79.3832&lt;br/&gt;relay.wavlake.com,41.2619,-95.8608&lt;br/&gt;inbox.scuba323.com,40.8218,-74.45&lt;br/&gt;nostr.spaceshell.xyz,43.6532,-79.3832&lt;br/&gt;relay.nostr.place,32.7767,-96.797&lt;br/&gt;holland-excited-charming-experiencing.trycloudflare.com,43.6532,-79.3832&lt;br/&gt;theoutpost.life,64.1476,-21.9392&lt;br/&gt;relay.fckstate.net,59.3293,18.0686&lt;br/&gt;bcast.girino.org,43.6532,-79.3832&lt;br/&gt;discovery.us.nostria.app,52.3676,4.90414&lt;br/&gt;relay.bullishbounty.com,43.6532,-79.3832&lt;br/&gt;nostr.88mph.life,51.5072,-0.127586&lt;br/&gt;nostr.tadryanom.me,43.6532,-79.3832&lt;br/&gt;nostr.sathoarder.com,48.5734,7.75211&lt;br/&gt;relay.nostr.net,43.6532,-79.3832&lt;br/&gt;zw.agorawlc.com,50.4754,12.3683&lt;br/&gt;relay.internationalright-wing.org,-22.5022,-48.7114&lt;br/&gt;nostr.vulpem.com,49.4543,11.0746&lt;br/&gt;wot.codingarena.top,50.4754,12.3683&lt;br/&gt;reraw.pbla2fish.cc,43.6532,-79.3832&lt;br/&gt;plebchain.club,43.6532,-79.3832&lt;br/&gt;orly-relay.imwald.eu,48.8575,2.35138&lt;br/&gt;relay.satnam.pub,43.6532,-79.3832&lt;br/&gt;cs-relay.nostrdev.com,50.4754,12.3683&lt;br/&gt;schnorr.me,43.6532,-79.3832&lt;br/&gt;nostr-relay.online,43.6532,-79.3832&lt;br/&gt;relay.routstr.com,43.6532,-79.3832&lt;br/&gt;relay.ohstr.com,43.6532,-79.3832&lt;br/&gt;relay.lanacoin-eternity.com,40.8302,-74.1299&lt;br/&gt;wot.nostr.net,43.6532,-79.3832&lt;br/&gt;nostr.ps1829.com,33.8851,130.883&lt;br/&gt;yabu.me,35.6092,139.73&lt;br/&gt;soloco.nl,43.6532,-79.3832&lt;br/&gt;librerelay.aaroniumii.com,43.6532,-79.3832&lt;br/&gt;relay.mmwaves.de,48.8575,2.35138&lt;br/&gt;relay.artx.market,43.6548,-79.3885&lt;br/&gt;nostr.jerrynya.fun,31.2304,121.474&lt;br/&gt;relay-arg.zombi.cloudrodion.com,1.35208,103.82&lt;br/&gt;relay.edufeed.org,49.4521,11.0767&lt;br/&gt;discovery.eu.nostria.app,52.3676,4.90414&lt;br/&gt;relay.layer.systems,49.0291,8.35695&lt;br/&gt;nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874&lt;br/&gt;relay.0xchat.com,43.6532,-79.3832&lt;br/&gt;nos.lol,50.4754,12.3683&lt;br/&gt;lightning.red,53.3498,-6.26031&lt;br/&gt;slick.mjex.me,39.0418,-77.4744&lt;br/&gt;relay.boredvictor.xyz,41.3888,2.15899&lt;br/&gt;nostr.rtvslawenia.com,49.4543,11.0746&lt;br/&gt;relay.mitchelltribe.com,39.0438,-77.4874&lt;br/&gt;nostr.4rs.nl,49.0291,8.35696&lt;br/&gt;relay.olas.app,50.4754,12.3683&lt;br/&gt;memlay.v0l.io,53.3498,-6.26031&lt;br/&gt;nostr-01.yakihonne.com,1.29524,103.79&lt;br/&gt;relay.satmaxt.xyz,43.6532,-79.3832&lt;br/&gt;nostrcheck.tnsor.network,43.6532,-79.3832&lt;br/&gt;relay.guggero.org,46.0037,8.95105&lt;br/&gt;ai.techunder.tech:56711,22.5429,114.06&lt;br/&gt;premium.primal.net,43.6532,-79.3832&lt;br/&gt;nostr.tac.lol,47.4748,-122.273&lt;br/&gt;relay.zone667.com,60.1699,24.9384&lt;br/&gt;nostr-relay.gateway.in.th,15.5163,103.194&lt;br/&gt;vault.iris.to,43.6532,-79.3832&lt;br/&gt;strfry.bonsai.com,37.8716,-122.273&lt;br/&gt;ribo.eu.nostria.app,52.3676,4.90414&lt;br/&gt;relay.wellorder.net,45.5201,-122.99&lt;br/&gt;relay.tapestry.ninja,40.8054,-74.0241&lt;br/&gt;relay.dwadziesciajeden.pl,52.2297,21.0122&lt;br/&gt;relay.satlantis.io,32.8769,-80.0114&lt;br/&gt;nostr.pbfs.io,50.4754,12.3683&lt;br/&gt;freelay.sovbit.host,64.1476,-21.9392&lt;br/&gt;articles.layer3.news,37.3387,-121.885&lt;br/&gt;nostr.na.social,43.6532,-79.3832&lt;br/&gt;relay.fountain.fm,43.6532,-79.3832&lt;br/&gt;dev.relay.stream,43.6532,-79.3832&lt;br/&gt;nostr.n7ekb.net,36.1527,-95.9902&lt;br/&gt;relay5.bitransfer.org,43.6532,-79.3832&lt;br/&gt;relay.og.coop,43.6532,-79.3832&lt;br/&gt;nostr-server-production.up.railway.app,45.5019,-73.5674&lt;br/&gt;bucket.coracle.social,37.7775,-122.397&lt;br/&gt;relay.gulugulu.moe,43.6532,-79.3832&lt;br/&gt;relay.nostr-check.me,43.6532,-79.3832&lt;br/&gt;nostr.faultables.net,43.6532,-79.3832&lt;br/&gt;strfry.openhoofd.nl,51.9229,4.40833&lt;br/&gt;nostr.rblb.it:7777,43.7094,10.6582&lt;br/&gt;relay.nostrcheck.me,43.6532,-79.3832&lt;br/&gt;0x-nostr-relay.fly.dev,48.8575,2.35138&lt;br/&gt;nostr.thalheim.io,60.1699,24.9384&lt;br/&gt;relay-nl.zombi.cloudrodion.com,50.8943,6.06237&lt;br/&gt;relay.shadowbip.com,51.5072,-0.127586&lt;br/&gt;nostr-relay.corb.net,38.8353,-104.822&lt;br/&gt;purplerelay.com,43.6532,-79.3832&lt;br/&gt;nostr-pub.wellorder.net,45.5201,-122.99&lt;br/&gt;herbstmeister.com,34.0549,-118.243&lt;br/&gt;nostrcheck.me,43.6532,-79.3832&lt;br/&gt;pyramid.nostr.technology,52.3947,4.66399&lt;br/&gt;nostr.spicyz.io,43.6532,-79.3832&lt;br/&gt;nrs-02.darkcloudarcade.com,39.9526,-75.1652&lt;br/&gt;nestr.nedao.ch,47.0151,6.98832&lt;br/&gt;nostr.nodesmap.com,59.3327,18.0656&lt;br/&gt;nittom.nostr1.com,38.6327,-90.1961&lt;br/&gt;public.crostr.com,43.6532,-79.3832&lt;br/&gt;relay.cypherflow.ai,48.8575,2.35138&lt;br/&gt;nostr.bitczat.pl,60.1699,24.9384&lt;br/&gt;relayone.geektank.ai,39.1008,-94.5811&lt;br/&gt;testrelay.era21.space,43.6532,-79.3832&lt;br/&gt;relay.npubhaus.com,43.6532,-79.3832&lt;br/&gt;relay.bitmacro.io,48.8566,2.35222&lt;br/&gt;nostr.data.haus,50.4754,12.3683&lt;br/&gt;relay.credenso.cafe,43.3601,-80.3127&lt;br/&gt;relay.ru.ac.th,13.7607,100.627&lt;br/&gt;relay-fra.zombi.cloudrodion.com,48.8566,2.35222&lt;br/&gt;nostr.chaima.info,50.1109,8.68213&lt;br/&gt;nostr.mom,50.4754,12.3683
    </content>
    <updated>2026-04-05T05:54:54Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsfg3m5pfm6sxq5z74rlhf3m22hkvg8dqst6lkanchwm3srp0egxlczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvqc4sn</id>
    
      <title type="html">#![cfg(feature = &amp;#34;nostr&amp;#34;)] use frost_secp256k1_tr as ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsfg3m5pfm6sxq5z74rlhf3m22hkvg8dqst6lkanchwm3srp0egxlczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvqc4sn" />
    <content type="html">
      #![cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;&lt;br/&gt;use frost_secp256k1_tr as frost;&lt;br/&gt;use frost::keys::PublicKeyPackage;&lt;br/&gt;use frost::round2::SignatureShare;&lt;br/&gt;use frost::SigningPackage;&lt;br/&gt;use hex;&lt;br/&gt;use rand::thread_rng;&lt;br/&gt;use std::collections::BTreeMap;&lt;br/&gt;use sha2::Sha256;&lt;br/&gt;use serde_json;&lt;br/&gt;use sha2::Digest;&lt;br/&gt;&lt;br/&gt;pub fn process_relay_share(&lt;br/&gt;    relay_payload_hex: &amp;amp;str,&lt;br/&gt;    signer_id_u16: u16,&lt;br/&gt;    _signing_package: &amp;amp;SigningPackage,&lt;br/&gt;    _pubkey_package: &amp;amp;PublicKeyPackage,&lt;br/&gt;) -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    // In a real scenario, this function would deserialize the share, perform&lt;br/&gt;    // individual verification, and store it for aggregation.&lt;br/&gt;    // For this example, we&amp;#39;ll just acknowledge receipt.&lt;br/&gt;    let _share_bytes = hex::decode(relay_payload_hex)?;&lt;br/&gt;    let _share = SignatureShare::deserialize(&amp;amp;_share_bytes)?;&lt;br/&gt;    let _identifier = frost::Identifier::try_from(signer_id_u16)?;&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;✅ Share from Signer {} processed (simplified).&amp;#34;, signer_id_u16);&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn simulate_frost_mailbox_coordinator() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    let mut rng = thread_rng();&lt;br/&gt;    let (max_signers, min_signers) = (2, 2);&lt;br/&gt;&lt;br/&gt;    let (shares, pubkey_package) = frost::keys::generate_with_dealer(&lt;br/&gt;        max_signers,&lt;br/&gt;        min_signers,&lt;br/&gt;        frost::keys::IdentifierList::Default,&lt;br/&gt;        &amp;amp;mut rng,&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    let signer1_id = frost::Identifier::try_from(1 as u16)?;&lt;br/&gt;    let key_package1: frost::keys::KeyPackage = shares[&amp;amp;signer1_id].clone().try_into()?;&lt;br/&gt;    let signer2_id = frost::Identifier::try_from(2 as u16)?;&lt;br/&gt;    let key_package2: frost::keys::KeyPackage = shares[&amp;amp;signer2_id].clone().try_into()?;&lt;br/&gt;&lt;br/&gt;    let message = b&amp;#34;BIP-64MOD: Anchor Data Proposal v1&amp;#34;;&lt;br/&gt;&lt;br/&gt;    let (nonces1, comms1) = frost::round1::commit(key_package1.signing_share(), &amp;amp;mut rng);&lt;br/&gt;    let (nonces2, comms2) = frost::round1::commit(key_package2.signing_share(), &amp;amp;mut rng);&lt;br/&gt;&lt;br/&gt;    let mut session_commitments = BTreeMap::new();&lt;br/&gt;    session_commitments.insert(signer1_id, comms1);&lt;br/&gt;    session_commitments.insert(signer2_id, comms2);&lt;br/&gt;&lt;br/&gt;    let signing_package = frost::SigningPackage::new(session_commitments.clone(), message);&lt;br/&gt;&lt;br/&gt;    let share1 = frost::round2::sign(&amp;amp;signing_package, &amp;amp;nonces1, &amp;amp;key_package1)?;&lt;br/&gt;    let share1_hex = hex::encode(share1.serialize());&lt;br/&gt;&lt;br/&gt;    let share2 = frost::round2::sign(&amp;amp;signing_package, &amp;amp;nonces2, &amp;amp;key_package2)?;&lt;br/&gt;    let share2_hex = hex::encode(share2.serialize());&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;Coordinator listening for Nostr events (simulated)...&amp;#34;);&lt;br/&gt;&lt;br/&gt;    process_relay_share(&amp;amp;share1_hex, 1_u16, &amp;amp;signing_package, &amp;amp;pubkey_package)?;&lt;br/&gt;    process_relay_share(&amp;amp;share2_hex, 2_u16, &amp;amp;signing_package, &amp;amp;pubkey_package)?;&lt;br/&gt;    println!(&amp;#34;All required shares processed. Coordinator would now aggregate.&amp;#34;);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Simulates a Signer producing a FROST signature share and preparing a Nostr event&lt;br/&gt;/// to be sent to a coordinator via a &amp;#34;mailbox&amp;#34; relay.&lt;br/&gt;///&lt;br/&gt;/// In a real ROAST setup, signers would generate their share and post it&lt;br/&gt;/// encrypted (e.g., using NIP-44) to a coordinator&amp;#39;s &amp;#34;mailbox&amp;#34; on a Nostr relay.&lt;br/&gt;/// This function demonstrates the creation of the signature share and the&lt;br/&gt;/// construction of a *simplified* Nostr event JSON.&lt;br/&gt;///&lt;br/&gt;/// # Arguments&lt;br/&gt;///&lt;br/&gt;/// * `_identifier` - The FROST identifier of the signer. (Currently unused in this specific function body).&lt;br/&gt;/// * `signing_package` - The FROST signing package received from the coordinator.&lt;br/&gt;/// * `nonces` - The signer&amp;#39;s nonces generated in Round 1.&lt;br/&gt;/// * `key_package` - The signer&amp;#39;s FROST key package.&lt;br/&gt;/// * `coordinator_pubkey` - The hex-encoded public key of the ROAST coordinator,&lt;br/&gt;///                          used to tag the Nostr event.&lt;br/&gt;///&lt;br/&gt;/// # Returns&lt;br/&gt;///&lt;br/&gt;/// A `Result` containing the JSON string of the Nostr event if successful,&lt;br/&gt;/// or a `Box&amp;lt;dyn std::error::Error&amp;gt;` if an error occurs.&lt;br/&gt;pub fn create_signer_event(&lt;br/&gt;    _identifier: frost::Identifier,&lt;br/&gt;    signing_package: &amp;amp;frost::SigningPackage,&lt;br/&gt;    nonces: &amp;amp;frost::round1::SigningNonces,&lt;br/&gt;    key_package: &amp;amp;frost::keys::KeyPackage,&lt;br/&gt;    coordinator_pubkey: &amp;amp;str, // The Hex pubkey of the ROAST coordinator&lt;br/&gt;) -&amp;gt; Result&amp;lt;String, Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;&lt;br/&gt;    // 1. Generate the partial signature share (Round 2 of FROST)&lt;br/&gt;    // This share is the core cryptographic output from the signer.&lt;br/&gt;    let share = frost::round2::sign(signing_package, nonces, key_package)?;&lt;br/&gt;    let share_bytes = share.serialize();&lt;br/&gt;    let share_hex = hex::encode(share_bytes);&lt;br/&gt;&lt;br/&gt;    // 2. Create a Session ID to tag the event&lt;br/&gt;    // This ID is derived from the signing package hash, allowing the coordinator&lt;br/&gt;    // to correlate shares belonging to the same signing session.&lt;br/&gt;    let mut hasher = Sha256::new();&lt;br/&gt;    hasher.update(signing_package.serialize()?);&lt;br/&gt;    let session_id = hex::encode(hasher.finalize());&lt;br/&gt;&lt;br/&gt;    // 3. Construct the Nostr Event JSON (Simplified)&lt;br/&gt;    // This JSON represents the event that a signer would post to a relay.&lt;br/&gt;    // In a production ROAST system, the &amp;#39;content&amp;#39; field (the signature share)&lt;br/&gt;    // would be encrypted for the coordinator using NIP-44.&lt;br/&gt;    let event = serde_json::json!({&lt;br/&gt;        &amp;#34;kind&amp;#34;: 4, // Example: Using Kind 4 (Private Message), though custom Kinds could be used for Sovereign Stack.&lt;br/&gt;        &amp;#34;pubkey&amp;#34;: hex::encode(key_package.verifying_key().serialize()?.as_slice()), // Signer&amp;#39;s public key&lt;br/&gt;        &amp;#34;created_at&amp;#34;: 1712050000, // Example timestamp&lt;br/&gt;        &amp;#34;tags&amp;#34;: [&lt;br/&gt;            [&amp;#34;p&amp;#34;, coordinator_pubkey],       // &amp;#39;p&amp;#39; tag: Directs the event to the coordinator.&lt;br/&gt;            [&amp;#34;i&amp;#34;, session_id],               // &amp;#39;i&amp;#39; tag: Provides a session identifier for filtering/requests.&lt;br/&gt;            [&amp;#34;t&amp;#34;, &amp;#34;frost-signature-share&amp;#34;]   // &amp;#39;t&amp;#39; tag: A searchable label for the event type.&lt;br/&gt;        ],&lt;br/&gt;        &amp;#34;content&amp;#34;: share_hex, // The actual signature share (would be encrypted in production).&lt;br/&gt;        &amp;#34;id&amp;#34;: &amp;#34;...&amp;#34;, // Event ID (filled by relay upon publishing)&lt;br/&gt;        &amp;#34;sig&amp;#34;: &amp;#34;...&amp;#34; // Event signature (filled by relay upon publishing)&lt;br/&gt;    });&lt;br/&gt;&lt;br/&gt;    Ok(event.to_string())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn simulate_frost_mailbox_post_signer() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    use rand::thread_rng;&lt;br/&gt;    use std::collections::BTreeMap;&lt;br/&gt;    use frost_secp256k1_tr as frost;&lt;br/&gt;&lt;br/&gt;    // This example simulates a single signer&amp;#39;s role in a ROAST mailbox post workflow.&lt;br/&gt;    // The general workflow is:&lt;br/&gt;    // 1. Coordinator sends a request for signatures (e.g., on a BIP-64MOD proposal).&lt;br/&gt;    // 2. Signers receive the proposal, perform local verification.&lt;br/&gt;    // 3. Each signer generates their signature share and posts it (encrypted) to a&lt;br/&gt;    //    Nostr relay, targeting the coordinator&amp;#39;s mailbox.&lt;br/&gt;    // 4. The coordinator collects enough shares to aggregate the final signature.&lt;br/&gt;&lt;br/&gt;    let mut rng = thread_rng();&lt;br/&gt;    // For this example, we simulate a 2-of-2 threshold for simplicity.&lt;br/&gt;    let (max_signers, min_signers) = (2, 2);&lt;br/&gt;&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // 1. Key Generation (Simulated Trusted Dealer)&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // In a real distributed setup, this would be DKG. Here, a &amp;#34;trusted dealer&amp;#34;&lt;br/&gt;    // generates the shares and public key package.&lt;br/&gt;    let (shares, _pubkey_package) = frost::keys::generate_with_dealer(&lt;br/&gt;        max_signers,&lt;br/&gt;        min_signers,&lt;br/&gt;        frost::keys::IdentifierList::Default,&lt;br/&gt;        &amp;amp;mut rng,&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    // For a 2-of-2 scheme, we have two signers. Let&amp;#39;s pick signer 1.&lt;br/&gt;    let signer1_id = frost::Identifier::try_from(1 as u16)?;&lt;br/&gt;    let key_package1: frost::keys::KeyPackage = shares[&amp;amp;signer1_id].clone().try_into()?;&lt;br/&gt;&lt;br/&gt;    let signer2_id = frost::Identifier::try_from(2 as u16)?;&lt;br/&gt;    let key_package2: frost::keys::KeyPackage = shares[&amp;amp;signer2_id].clone().try_into()?;&lt;br/&gt;&lt;br/&gt;    // The message that is to be signed (e.g., a hash of a Git commit or a Nostr event ID).&lt;br/&gt;    let message = b&amp;#34;This is a test message for ROAST mailbox post.&amp;#34;;&lt;br/&gt;&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // 2. Round 1: Commitment Phase (Signer&amp;#39;s role)&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // Each signer generates nonces and commitments.&lt;br/&gt;    let (nonces1, comms1) = frost::round1::commit(key_package1.signing_share(), &amp;amp;mut rng);&lt;br/&gt;    let (nonces2, comms2) = frost::round1::commit(key_package2.signing_share(), &amp;amp;mut rng);&lt;br/&gt;    &lt;br/&gt;    // The coordinator collects these commitments. Here, we simulate by putting them in a BTreeMap.&lt;br/&gt;    let mut session_commitments = BTreeMap::new();&lt;br/&gt;    session_commitments.insert(signer1_id, comms1);&lt;br/&gt;    session_commitments.insert(signer2_id, comms2);&lt;br/&gt;&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // 3. Signing Package Creation (Coordinator&amp;#39;s role, simulated for context)&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // The coordinator combines the collected commitments and the message to be signed&lt;br/&gt;    // into a signing package, which is then sent back to the signers.&lt;br/&gt;    let signing_package = frost::SigningPackage::new(session_commitments, message);&lt;br/&gt;&lt;br/&gt;    // Dummy coordinator public key. In a real scenario, this would be the&lt;br/&gt;    // actual public key of the ROAST coordinator, used for event tagging&lt;br/&gt;    // and encryption (NIP-44).&lt;br/&gt;    let coordinator_pubkey_hex = &amp;#34;0000000000000000000000000000000000000000000000000000000000000001&amp;#34;;&lt;br/&gt;&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // 4. Create the Signer Event (Signer&amp;#39;s role)&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // We demonstrate for signer 1. Signer 2 would perform a similar action.&lt;br/&gt;    let event_json_signer1 = create_signer_event(&lt;br/&gt;        signer1_id,&lt;br/&gt;        &amp;amp;signing_package,&lt;br/&gt;        &amp;amp;nonces1,&lt;br/&gt;        &amp;amp;key_package1,&lt;br/&gt;        coordinator_pubkey_hex,&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;Generated Nostr Event for Signer 1 Mailbox Post:&lt;br/&gt;{}&amp;#34;, event_json_signer1);&lt;br/&gt;&lt;br/&gt;    // Similarly, Signer 2 would generate their event:&lt;br/&gt;    let event_json_signer2 = create_signer_event(&lt;br/&gt;        signer2_id,&lt;br/&gt;        &amp;amp;signing_package,&lt;br/&gt;        &amp;amp;nonces2,&lt;br/&gt;        &amp;amp;key_package2,&lt;br/&gt;        coordinator_pubkey_hex,&lt;br/&gt;    )?;&lt;br/&gt;    println!(&amp;#34;Generated Nostr Event for Signer 2 Mailbox Post:&lt;br/&gt;{}&amp;#34;, event_json_signer2);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}
    </content>
    <updated>2026-04-05T05:54:22Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsw80xkcccpqhcxsznwsjx25emng77ku3vu9k3qsyr06wykn6qapvgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvu8yqjz</id>
    
      <title type="html">#[cfg(all(not(debug_assertions), feature = &amp;#34;nostr&amp;#34;))] ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsw80xkcccpqhcxsznwsjx25emng77ku3vu9k3qsyr06wykn6qapvgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvu8yqjz" />
    <content type="html">
      #[cfg(all(not(debug_assertions), feature = &amp;#34;nostr&amp;#34;))]&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() {&lt;br/&gt;    use std::fs;&lt;br/&gt;    use std::path::PathBuf;&lt;br/&gt;&lt;br/&gt;    let manifest_dir = PathBuf::from(std::env::var(&amp;#34;CARGO_MANIFEST_DIR&amp;#34;).unwrap());&lt;br/&gt;    let crate_src_path = manifest_dir.join(&amp;#34;src&amp;#34;).join(&amp;#34;online_relays_gps.csv&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // Only download if the file doesn&amp;#39;t exist or is empty&lt;br/&gt;    if !crate_src_path.exists() || fs::metadata(&amp;amp;crate_src_path).map(|m| m.len() == 0).unwrap_or(true) {&lt;br/&gt;        println!(&amp;#34;cargo:warning=Downloading online_relays_gps.csv...&amp;#34;);&lt;br/&gt;        let url = &amp;#34;&lt;a href=&#34;https://raw.githubusercontent.com/permissionlesstech/bitchat/main/relays/online_relays_gps.csv&amp;#34&#34;&gt;https://raw.githubusercontent.com/permissionlesstech/bitchat/main/relays/online_relays_gps.csv&amp;#34&lt;/a&gt;;;&lt;br/&gt;        match reqwest::get(url).await {&lt;br/&gt;            Ok(response) =&amp;gt; {&lt;br/&gt;                if response.status().is_success() {&lt;br/&gt;                    match response.text().await {&lt;br/&gt;                        Ok(content) =&amp;gt; {&lt;br/&gt;                            fs::write(&amp;amp;crate_src_path, content).expect(&amp;#34;Unable to write online_relays_gps.csv&amp;#34;);&lt;br/&gt;                            println!(&amp;#34;cargo:warning=Successfully downloaded online_relays_gps.csv to {:?}&amp;#34;, crate_src_path);&lt;br/&gt;                        },&lt;br/&gt;                        Err(e) =&amp;gt; {&lt;br/&gt;                            println!(&amp;#34;cargo:warning=Failed to get text from response: {}&amp;#34;, e);&lt;br/&gt;                        }&lt;br/&gt;                    }&lt;br/&gt;                } else {&lt;br/&gt;                    println!(&amp;#34;cargo:warning=Failed to download online_relays_gps.csv: HTTP status {}&amp;#34;, response.status());&lt;br/&gt;                }&lt;br/&gt;            },&lt;br/&gt;            Err(e) =&amp;gt; {&lt;br/&gt;                println!(&amp;#34;cargo:warning=Failed to fetch online_relays_gps.csv: {}&amp;#34;, e);&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(not(all(not(debug_assertions), feature = &amp;#34;nostr&amp;#34;)))]&lt;br/&gt;fn main() {&lt;br/&gt;    // Placeholder for when the nostr feature is not enabled or in debug mode&lt;br/&gt;    println!(&amp;#34;cargo:warning=Skipping online_relays_gps.csv download (nostr feature not enabled or debug mode)&amp;#34;);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:54:11Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsygx9utt4tk99m6ut3l8kmm6qgfp2prej4mhpuchkeeydrwqpjd4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvq8k5mm</id>
    
      <title type="html">[package] name = &amp;#34;get_file_hash_core&amp;#34; version = { ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsygx9utt4tk99m6ut3l8kmm6qgfp2prej4mhpuchkeeydrwqpjd4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvq8k5mm" />
    <content type="html">
      [package]&lt;br/&gt;name = &amp;#34;get_file_hash_core&amp;#34;&lt;br/&gt;version = { workspace = true }&lt;br/&gt;edition = { workspace = true }&lt;br/&gt;description = { workspace = true }&lt;br/&gt;license = { workspace = true }&lt;br/&gt;documentation = { workspace = true }&lt;br/&gt;homepage = { workspace = true }&lt;br/&gt;repository = { workspace = true }&lt;br/&gt;authors = { workspace = true }&lt;br/&gt;&lt;br/&gt;[features]&lt;br/&gt;nostr = [&amp;#34;dep:nostr&amp;#34;, &amp;#34;dep:nostr-sdk&amp;#34;, &amp;#34;dep:serde_json&amp;#34;, &amp;#34;dep:sha2&amp;#34;, &amp;#34;dep:hex&amp;#34;, &amp;#34;dep:reqwest&amp;#34;, &amp;#34;dep:tokio&amp;#34;, &amp;#34;dep:csv&amp;#34;, &amp;#34;dep:url&amp;#34;, &amp;#34;dep:frost-secp256k1-tr&amp;#34;, &amp;#34;dep:rand&amp;#34;]&lt;br/&gt;frost = [&amp;#34;dep:nostr&amp;#34;, &amp;#34;dep:nostr-sdk&amp;#34;, &amp;#34;dep:serde_json&amp;#34;, &amp;#34;dep:sha2&amp;#34;, &amp;#34;dep:hex&amp;#34;, &amp;#34;dep:reqwest&amp;#34;, &amp;#34;dep:tokio&amp;#34;, &amp;#34;dep:csv&amp;#34;, &amp;#34;dep:url&amp;#34;, &amp;#34;dep:frost-secp256k1-tr&amp;#34;, &amp;#34;dep:rand&amp;#34;]&lt;br/&gt;&lt;br/&gt;[dependencies]&lt;br/&gt;sha2 = { workspace = true, optional = true }&lt;br/&gt;nostr = { workspace = true, optional = true }&lt;br/&gt;serde_json = { workspace = true, optional = true }&lt;br/&gt;nostr-sdk = { workspace = true, optional = true }&lt;br/&gt;hex = { workspace = true, optional = true }&lt;br/&gt;csv = { workspace = true, optional = true }&lt;br/&gt;url = { workspace = true, optional = true }&lt;br/&gt;frost-secp256k1-tr = { workspace = true, optional = true }&lt;br/&gt;rand = { workspace = true, optional = true }&lt;br/&gt;&lt;br/&gt;[dev-dependencies]&lt;br/&gt;sha2 = { workspace = true }&lt;br/&gt;tempfile = { workspace = true }&lt;br/&gt;nostr = { workspace = true }&lt;br/&gt;nostr-sdk = { workspace = true }&lt;br/&gt;serde_json = { workspace = true }&lt;br/&gt;hex = { workspace = true }&lt;br/&gt;tokio = { workspace = true, features = [&amp;#34;macros&amp;#34;, &amp;#34;rt-multi-thread&amp;#34;] }&lt;br/&gt;csv = { workspace = true }&lt;br/&gt;url = { workspace = true }&lt;br/&gt;frost-secp256k1-tr = { workspace = true }&lt;br/&gt;serial_test = { workspace = true, features = [&amp;#34;test_logging&amp;#34;] }&lt;br/&gt;log = { workspace = true }&lt;br/&gt;&lt;br/&gt;[build-dependencies]&lt;br/&gt;reqwest = { workspace = true, features = [&amp;#34;json&amp;#34;], optional = true }&lt;br/&gt;tokio = { workspace = true, features = [&amp;#34;macros&amp;#34;, &amp;#34;rt-multi-thread&amp;#34;], optional = true }&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:54:00Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsreu7trke765zfg0vk0dngkak9ghm9fx2m3g8fcfqnf7vuyj72rmszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvk2tuv</id>
    
      <title type="html">//! A simple command-line tool that calculates and displays the ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsreu7trke765zfg0vk0dngkak9ghm9fx2m3g8fcfqnf7vuyj72rmszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvk2tuv" />
    <content type="html">
      //! A simple command-line tool that calculates and displays the SHA-256 hash of&lt;br/&gt;//! its own source file.&lt;br/&gt;//!&lt;br/&gt;//! This utility demonstrates how to use the `get_file_hash!` macro to obtain&lt;br/&gt;//! the hash of a specified file at compile time and incorporate it into runtime&lt;br/&gt;//! logic.&lt;br/&gt;use get_file_hash::{BUILD_HASH, CARGO_TOML_HASH, LIB_HASH};&lt;br/&gt;use get_file_hash_core::get_file_hash;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART0: &amp;amp;str = r##&amp;#34;# `get_file_hash` macro&lt;br/&gt;&lt;br/&gt;This project provides a Rust procedural macro, `get_file_hash!`, designed to compute the SHA-256 hash of a specified file at compile time. This hash is then embedded directly into your compiled executable. This feature is invaluable for:&lt;br/&gt;&lt;br/&gt;*   **Integrity Verification:** Ensuring the deployed code hasn&amp;#39;t been tampered with.&lt;br/&gt;*   **Versioning:** Embedding a unique identifier linked to the exact source code version.&lt;br/&gt;*   **Cache Busting:** Generating unique names for assets based on their content.&lt;br/&gt;&lt;br/&gt;## Project Structure&lt;br/&gt;&lt;br/&gt;*   `get_file_hash_core`: A foundational crate containing the `get_file_hash!` macro definition.&lt;br/&gt;*   `get_file_hash`: The main library crate that re-exports the macro.&lt;br/&gt;*   `src/bin/get_file_hash.rs`: An example executable demonstrating the macro&amp;#39;s usage by hashing its own source file and updating this `README.md`.&lt;br/&gt;*   `build.rs`: A build script that also utilizes the `get_file_hash!` macro to hash `Cargo.toml` during the build process.&lt;br/&gt;&lt;br/&gt;## Usage of `get_file_hash!` Macro&lt;br/&gt;&lt;br/&gt;To use the `get_file_hash!` macro, ensure you have `get_file_hash` (or `get_file_hash_core` for direct usage) as a dependency in your `Cargo.toml`.&lt;br/&gt;&lt;br/&gt;### Example&lt;br/&gt;&lt;br/&gt;```rust&lt;br/&gt;use get_file_hash::get_file_hash;&lt;br/&gt;use get_file_hash::CARGO_TOML_HASH;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;fn main() {&lt;br/&gt;    // The macro resolves the path relative to CARGO_MANIFEST_DIR&lt;br/&gt;    let readme_hash = get_file_hash!(&amp;#34;src/bin/readme.rs&amp;#34;);&lt;br/&gt;    let lib_hash = get_file_hash!(&amp;#34;src/lib.rs&amp;#34;);&lt;br/&gt;    println!(&amp;#34;The SHA-256 hash of src/lib.rs is: {}&amp;#34;, lib_hash);&lt;br/&gt;    println!(&amp;#34;The SHA-256 hash of src/bin/readme.rs is: {}&amp;#34;, readme_hash);&lt;br/&gt;    println!(&amp;#34;The SHA-256 hash of Cargo.toml is: {}&amp;#34;, CARGO_TOML_HASH);&lt;br/&gt;}&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;&amp;#34;##;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART1: &amp;amp;str = r&amp;#34;## Release&lt;br/&gt;## [`README.md`](./README.md)&lt;br/&gt;&lt;br/&gt;```bash&lt;br/&gt;cargo run --bin readme &amp;gt; README.md&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;## [`src/bin/readme.rs`](src/bin/readme.rs)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `src/bin/readme.rs`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART2: &amp;amp;str = r&amp;#34;##&lt;br/&gt;&lt;br/&gt;## [`build.rs`](build.rs)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `build.rs`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART3: &amp;amp;str = r&amp;#34;##&lt;br/&gt;&lt;br/&gt;## [`Cargo.toml`](Cargo.toml)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `Cargo.toml`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART4: &amp;amp;str = r&amp;#34;##&lt;br/&gt;&lt;br/&gt;## [`src/lib.rs`](src/lib.rs)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `src/lib.rs`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART_NIP34: &amp;amp;str = r&amp;#34;## NIP-34 Integration: Git Repository Events on Nostr&lt;br/&gt;&lt;br/&gt;This library provides a set of powerful macros and functions for integrating Git repository events with the Nostr protocol, adhering to the [NIP-34: Git Repositories on Nostr](&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/34.md&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/34.md&lt;/a&gt;) specification.&lt;br/&gt;&lt;br/&gt;These tools allow you to publish various Git-related events to Nostr relays, enabling decentralized tracking and collaboration for your code repositories.&lt;br/&gt;&lt;br/&gt;### Available NIP-34 Macros&lt;br/&gt;&lt;br/&gt;Each macro provides a convenient way to publish specific NIP-34 event kinds:&lt;br/&gt;&lt;br/&gt;*   [`repository_announcement!`](#repository_announcement)&lt;br/&gt;    *   Publishes a `Repository Announcement` event (Kind 30617) to announce a new or updated Git repository.&lt;br/&gt;*   [`publish_patch!`](#publish_patch)&lt;br/&gt;    *   Publishes a `Patch` event (Kind 1617) containing a Git patch (diff) for a specific commit.&lt;br/&gt;*   [`publish_pull_request!`](#publish_pull_request)&lt;br/&gt;    *   Publishes a `Pull Request` event (Kind 1618) to propose changes and facilitate code review.&lt;br/&gt;*   [`publish_pr_update!`](#publish_pr_update)&lt;br/&gt;    *   Publishes a `Pull Request Update` event (Kind 1619) to update an existing pull request.&lt;br/&gt;*   [`publish_repository_state!`](#publish_repository_state)&lt;br/&gt;    *   Publishes a `Repository State` event (Kind 1620) to announce the current state of a branch (e.g., its latest commit).&lt;br/&gt;*   [`publish_issue!`](#publish_issue)&lt;br/&gt;    *   Publishes an `Issue` event (Kind 1621) to report bugs, request features, or track tasks.&lt;br/&gt;&lt;br/&gt;### Running NIP-34 Examples&lt;br/&gt;&lt;br/&gt;To see these macros in action, navigate to the `examples/` directory and run each example individually with the `nostr` feature enabled:&lt;br/&gt;&lt;br/&gt;```bash&lt;br/&gt;cargo run --example repository_announcement --features nostr&lt;br/&gt;cargo run --example publish_patch --features nostr&lt;br/&gt;cargo run --example publish_pull_request --features nostr&lt;br/&gt;cargo run --example publish_pr_update --features nostr&lt;br/&gt;cargo run --example publish_repository_state --features nostr&lt;br/&gt;cargo run --example publish_issue --features nostr&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// The main entry point of the application.&lt;br/&gt;///&lt;br/&gt;/// This function calculates the SHA-256 hash of the `get_file_hash.rs` source&lt;br/&gt;/// file using a custom procedural macro and then prints the hash to the&lt;br/&gt;/// console. It also includes a basic integrity verification check.&lt;br/&gt;fn main() {&lt;br/&gt;    // Calculate the SHA-256 hash of the current file (`readme.rs`) at&lt;br/&gt;    // compile time. The `get_file_hash!` macro reads the file content and&lt;br/&gt;    // computes its hash.&lt;br/&gt;    let self_hash = get_file_hash!(&amp;#34;readme.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let status_message = if self_hash.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let build_message = if BUILD_HASH.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;    let cargo_message = if CARGO_TOML_HASH.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;    let lib_message = if LIB_HASH.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    print!(&amp;#34;{}{}{}&amp;#34;, README_TEMPLATE_PART0, README_TEMPLATE_PART1, README_TEMPLATE_PART_NIP34);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** {}&amp;#34;, self_hash);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, status_message);&lt;br/&gt;    //&lt;br/&gt;    print!(&amp;#34;{}&amp;#34;, README_TEMPLATE_PART2);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** {}&amp;#34;, BUILD_HASH);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, build_message);&lt;br/&gt;    //&lt;br/&gt;    print!(&amp;#34;{}&amp;#34;, README_TEMPLATE_PART3);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** {}&amp;#34;, CARGO_TOML_HASH);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, cargo_message);&lt;br/&gt;    //&lt;br/&gt;    print!(&amp;#34;{}&amp;#34;, README_TEMPLATE_PART4);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** {}&amp;#34;, LIB_HASH);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, lib_message);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:53:47Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsvg357r62cphynhyud97jlk4e4m9n5h0jyv79fjp7c0hvwfwlnz7qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvmd0xwv</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsvg357r62cphynhyud97jlk4e4m9n5h0jyv79fjp7c0hvwfwlnz7qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvmd0xwv" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Command line interface module&lt;br/&gt;use n34::cli;&lt;br/&gt;// /// N34 errors&lt;br/&gt;// use n34::error;&lt;br/&gt;// /// Nostr utils module&lt;br/&gt;// use n34::nostr_utils;&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    process::ExitCode,&lt;br/&gt;    sync::atomic::{AtomicBool, Ordering},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Parser;&lt;br/&gt;use clap_verbosity_flag::Verbosity;&lt;br/&gt;use tracing::Level;&lt;br/&gt;use tracing_subscriber::{Layer, filter, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;use self::cli::Cli;&lt;br/&gt;&lt;br/&gt;/// Whether the editor is currently open. Prevents logging while the editor is&lt;br/&gt;/// open.&lt;br/&gt;static EDITOR_OPEN: AtomicBool = AtomicBool::new(false);&lt;br/&gt;&lt;br/&gt;/// Configures the logging level based on the provided verbosity.&lt;br/&gt;///&lt;br/&gt;/// When verbosity is set to TRACE, includes file and line numbers in logs.&lt;br/&gt;fn set_log_level(verbosity: Verbosity) {&lt;br/&gt;    let is_trace = verbosity&lt;br/&gt;        .tracing_level()&lt;br/&gt;        .is_some_and(|l| l == tracing::Level::TRACE);&lt;br/&gt;&lt;br/&gt;    let logs_filter = filter::dynamic_filter_fn(move |m, _| {&lt;br/&gt;        // Disable all logs while editor is open&lt;br/&gt;        verbosity.tracing_level().unwrap_or(Level::ERROR) &amp;gt;= *m.level()&lt;br/&gt;            &amp;amp;&amp;amp; !EDITOR_OPEN.load(Ordering::Relaxed)&lt;br/&gt;    });&lt;br/&gt;&lt;br/&gt;    let logs_layer = tracing_subscriber::fmt::layer()&lt;br/&gt;        .with_file(is_trace)&lt;br/&gt;        .with_line_number(is_trace)&lt;br/&gt;        .without_time();&lt;br/&gt;    let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter));&lt;br/&gt;    tracing::subscriber::set_global_default(subscriber).ok();&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() -&amp;gt; ExitCode {&lt;br/&gt;    let cli = match cli::post_cli(Cli::parse()) {&lt;br/&gt;        Ok(cli) =&amp;gt; cli,&lt;br/&gt;        Err(err) =&amp;gt; {&lt;br/&gt;            eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;            return ExitCode::FAILURE;&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    set_log_level(cli.verbosity);&lt;br/&gt;&lt;br/&gt;    if let Err(err) = cli.run().await {&lt;br/&gt;        tracing::error!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;        return err.exit_code();&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    ExitCode::SUCCESS&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:53:36Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsx0cd0ugv3gdncc63hn3qjc49f9xw6hdk8qkmu8jtakh3meyhtmeczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9s9axd</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsx0cd0ugv3gdncc63hn3qjc49f9xw6hdk8qkmu8jtakh3meyhtmeczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9s9axd" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{net::SocketAddr, process::ExitCode, sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::Extension;&lt;br/&gt;use hyper::{Method, header};&lt;br/&gt;use tokio::signal;&lt;br/&gt;use tower_http::{&lt;br/&gt;    cors,&lt;br/&gt;    decompression::RequestDecompressionLayer,&lt;br/&gt;    trace::{DefaultMakeSpan, TraceLayer},&lt;br/&gt;};&lt;br/&gt;use tracing::level_filters::LevelFilter;&lt;br/&gt;use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;/// Relay endpoints&lt;br/&gt;use n34_relay::endpoints;&lt;br/&gt;/// Relay errors.&lt;br/&gt;use n34_relay::errors;&lt;br/&gt;// /// Extension traits&lt;br/&gt;// use n34_relay::ext_traits;&lt;br/&gt;/// GRASP git server&lt;br/&gt;use n34_relay::git_server;&lt;br/&gt;// /// Relay pathes.&lt;br/&gt;// use n34_relay::pathes;&lt;br/&gt;// /// Raw axum websocket&lt;br/&gt;// use n34_relay::raw_websocket;&lt;br/&gt;/// Our relay.&lt;br/&gt;use n34_relay::relay;&lt;br/&gt;/// Relay configuration.&lt;br/&gt;use n34_relay::relay_config;&lt;br/&gt;/// Router state&lt;br/&gt;use n34_relay::router_state;&lt;br/&gt;/// Some useful utils.&lt;br/&gt;use n34_relay::utils;&lt;br/&gt;&lt;br/&gt;use self::{errors::RelayResult, relay_config::RelayConfig, router_state::RouterState};&lt;br/&gt;&lt;br/&gt;/// Sets up default logging with two outputs, stderr and a log file.&lt;br/&gt;///&lt;br/&gt;/// Log level for stderr is controlled by `RUST_LOG` environment variable,&lt;br/&gt;/// defaults to `ERROR`. The log file always uses `TRACE` level.&lt;br/&gt;fn setup_logs() -&amp;gt; errors::RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    tracing::subscriber::set_global_default(&lt;br/&gt;        tracing_subscriber::registry()&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(true)&lt;br/&gt;                    .with_writer(std::io::stderr)&lt;br/&gt;                    .without_time()&lt;br/&gt;                    .with_filter(EnvFilter::from_default_env()),&lt;br/&gt;            )&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(false)&lt;br/&gt;                    .with_writer(utils::logs_file()?)&lt;br/&gt;                    .with_file(false)&lt;br/&gt;                    .with_line_number(false)&lt;br/&gt;                    .with_filter(LevelFilter::TRACE),&lt;br/&gt;            ),&lt;br/&gt;    )&lt;br/&gt;    .ok();&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn shutdown_signal() {&lt;br/&gt;    let ctrl_c = async {&lt;br/&gt;        signal::ctrl_c()&lt;br/&gt;            .await&lt;br/&gt;            .expect(&amp;#34;Failed to install CTRL&#43;C handler&amp;#34;);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(unix)]&lt;br/&gt;    let terminate = async {&lt;br/&gt;        use tokio::signal::unix::SignalKind;&lt;br/&gt;&lt;br/&gt;        signal::unix::signal(SignalKind::terminate())&lt;br/&gt;            .expect(&amp;#34;Failed to create SIGTERM handler&amp;#34;)&lt;br/&gt;            .recv()&lt;br/&gt;            .await;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(not(unix))]&lt;br/&gt;    let terminate = std::future::pending::&amp;lt;()&amp;gt;();&lt;br/&gt;&lt;br/&gt;    tokio::select! {&lt;br/&gt;        _ = ctrl_c =&amp;gt; {},&lt;br/&gt;        _ = terminate =&amp;gt; {},&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn try_main() -&amp;gt; RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    setup_logs()?;&lt;br/&gt;    let config = Arc::new(RelayConfig::reload()?);&lt;br/&gt;    let relay_db = config.get_relay_db().await?;&lt;br/&gt;    let n34_relay = Arc::new(relay::build_relay(Arc::clone(&amp;amp;config), Arc::clone(&amp;amp;relay_db)).await);&lt;br/&gt;    let addr = SocketAddr::new(config.net.ip, config.net.port);&lt;br/&gt;&lt;br/&gt;    tracing::debug!(&amp;#34;Running relay with configuration: {config:#?}&amp;#34;);&lt;br/&gt;    tracing::info!(&amp;#34;Relay is running at `{}`&amp;#34;, n34_relay.url().await);&lt;br/&gt;&lt;br/&gt;    let mut app = axum::Router::new()&lt;br/&gt;        // main handler. GET and POST&lt;br/&gt;        .route(&amp;#34;/&amp;#34;, axum::routing::get(endpoints::main_handler).post(endpoints::main_handler));&lt;br/&gt;&lt;br/&gt;    if config.grasp.enable {&lt;br/&gt;        tracing::info!(&amp;#34;Git server is running&amp;#34;);&lt;br/&gt;        app = app.merge(git_server::router(&amp;amp;config));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    app = app&lt;br/&gt;        // enable cross-origin access&lt;br/&gt;        .route(&lt;br/&gt;            &amp;#34;/&amp;#34;,&lt;br/&gt;            axum::routing::options(|| async { hyper::StatusCode::NO_CONTENT }),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            cors::CorsLayer::new()&lt;br/&gt;                .allow_origin(cors::Any)&lt;br/&gt;                .allow_methods([Method::GET, Method::POST])&lt;br/&gt;                .allow_headers([header::CONTENT_TYPE]),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            TraceLayer::new_for_http()&lt;br/&gt;                .make_span_with(DefaultMakeSpan::default().include_headers(true)),&lt;br/&gt;        )&lt;br/&gt;        .layer(RequestDecompressionLayer::new())&lt;br/&gt;        .layer(Extension(Arc::new(RouterState::new(&lt;br/&gt;            config, n34_relay, relay_db,&lt;br/&gt;        ))));&lt;br/&gt;&lt;br/&gt;    axum::serve(&lt;br/&gt;        tokio::net::TcpListener::bind(addr).await?,&lt;br/&gt;        app.into_make_service_with_connect_info::&amp;lt;SocketAddr&amp;gt;(),&lt;br/&gt;    )&lt;br/&gt;    .with_graceful_shutdown(shutdown_signal())&lt;br/&gt;    .await?;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() -&amp;gt; ExitCode {&lt;br/&gt;    if let Err(err) = try_main().await {&lt;br/&gt;        eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;        return ExitCode::FAILURE;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    tracing::info!(&amp;#34;Exited gracefully without any errors&amp;#34;);&lt;br/&gt;    ExitCode::SUCCESS&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:53:25Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqstqyeld2zfdtun6cmjat037e847wxc4q0nejap27tlnjzcgnu0qvgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvwqrfqx</id>
    
      <title type="html">//! A simple command-line tool that calculates and displays the ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqstqyeld2zfdtun6cmjat037e847wxc4q0nejap27tlnjzcgnu0qvgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvwqrfqx" />
    <content type="html">
      //! A simple command-line tool that calculates and displays the SHA-256 hash of&lt;br/&gt;//! its own source file.&lt;br/&gt;//!&lt;br/&gt;//! This utility demonstrates how to use the `get_file_hash!` macro to obtain&lt;br/&gt;//! the hash of a specified file at compile time and incorporate it into runtime&lt;br/&gt;//! logic.&lt;br/&gt;&lt;br/&gt;use get_file_hash_core::get_file_hash;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART1: &amp;amp;str = r##&amp;#34;# `get_file_hash` macro&lt;br/&gt;&lt;br/&gt;This project provides a Rust procedural macro, `get_file_hash!`, designed to compute the SHA-256 hash of a specified file at compile time. This hash is then embedded directly into your compiled executable. This feature is invaluable for:&lt;br/&gt;&lt;br/&gt;*   **Integrity Verification:** Ensuring the deployed code hasn&amp;#39;t been tampered with.&lt;br/&gt;*   **Versioning:** Embedding a unique identifier linked to the exact source code version.&lt;br/&gt;*   **Cache Busting:** Generating unique names for assets based on their content.&lt;br/&gt;&lt;br/&gt;## Project Structure&lt;br/&gt;&lt;br/&gt;*   `get_file_hash_core`: A foundational crate containing the `get_file_hash!` macro definition.&lt;br/&gt;*   `get_file_hash`: The main library crate that re-exports the macro.&lt;br/&gt;*   `src/bin/get_file_hash.rs`: An example executable demonstrating the macro&amp;#39;s usage by hashing its own source file and updating this `README.md`.&lt;br/&gt;*   `build.rs`: A build script that also utilizes the `get_file_hash!` macro to hash `Cargo.toml` during the build process.&lt;br/&gt;&lt;br/&gt;## Usage of `get_file_hash!` Macro&lt;br/&gt;&lt;br/&gt;To use the `get_file_hash!` macro, ensure you have `get_file_hash` (or `get_file_hash_core` for direct usage) as a dependency in your `Cargo.toml`.&lt;br/&gt;&lt;br/&gt;### Example&lt;br/&gt;&lt;br/&gt;```rust&lt;br/&gt;use get_file_hash::get_file_hash;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;fn main() {&lt;br/&gt;    // The macro resolves the path relative to CARGO_MANIFEST_DIR&lt;br/&gt;    let file_hash = get_file_hash!(&amp;#34;src/lib.rs&amp;#34;);&lt;br/&gt;    println!(&amp;#34;The SHA-256 hash of src/lib.rs is: {}&amp;#34;, file_hash);&lt;br/&gt;}&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;&amp;#34;##;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART2: &amp;amp;str = r&amp;#34;## Setup and Building&lt;br/&gt;&lt;br/&gt;1.  **Clone the repository:**&lt;br/&gt;    ```bash&lt;br/&gt;    git clone &amp;lt;repository-url&amp;gt;&lt;br/&gt;    cd &amp;lt;repository-name&amp;gt;&lt;br/&gt;    ```&lt;br/&gt;2.  **Build the project:**&lt;br/&gt;    ```bash&lt;br/&gt;    cargo build&lt;br/&gt;    ```&lt;br/&gt;    During the build, `build.rs` will execute and print the hash of `Cargo.toml`.&lt;br/&gt;3.  **Run the example executable:**&lt;br/&gt;    ```bash&lt;br/&gt;    cargo run --bin get_file_hash&lt;br/&gt;    ```&lt;br/&gt;    This will print the hash of `src/bin/get_file_hash.rs` to your console.&lt;br/&gt;&lt;br/&gt;## Updating this `README.md`&lt;br/&gt;&lt;br/&gt;The hash information in this `README.md` is automatically generated by running the example executable.&lt;br/&gt;To update it, execute:&lt;br/&gt;&lt;br/&gt;```bash&lt;br/&gt;cargo run --bin get_file_hash &amp;gt; README.md&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;## Current File Hash Information (of `src/bin/get_file_hash.rs`)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `src/bin/get_file_hash.rs`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// The main entry point of the application.&lt;br/&gt;///&lt;br/&gt;/// This function calculates the SHA-256 hash of the `get_file_hash.rs` source&lt;br/&gt;/// file using a custom procedural macro and then prints the hash to the&lt;br/&gt;/// console. It also includes a basic integrity verification check.&lt;br/&gt;fn main() {&lt;br/&gt;    // Calculate the SHA-256 hash of the current file (`get_file_hash.rs`) at&lt;br/&gt;    // compile time. The `get_file_hash!` macro reads the file content and&lt;br/&gt;    // computes its hash.&lt;br/&gt;    let self_hash = get_file_hash!(&amp;#34;get_file_hash.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let status_message = if self_hash.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    print!(&amp;#34;{}{}&amp;#34;, README_TEMPLATE_PART1, README_TEMPLATE_PART2);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** `{}`&amp;#34;, self_hash);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, status_message);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:53:14Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqswsa3hmkaa38cf66vww7fn560pe5zjymd488wq0zh62tyqjremy4gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4y8uav</id>
    
      <title type="html">&amp;lt;?xml version=&amp;#39;1.0&amp;#39; ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqswsa3hmkaa38cf66vww7fn560pe5zjymd488wq0zh62tyqjremy4gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4y8uav" />
    <content type="html">
      &amp;lt;?xml version=&amp;#39;1.0&amp;#39; encoding=&amp;#39;windows-1252&amp;#39;?&amp;gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  Copyright (C) 2017 Christopher R. Field.&lt;br/&gt;&lt;br/&gt;  Licensed under the Apache License, Version 2.0 (the &amp;#34;License&amp;#34;);&lt;br/&gt;  you may not use this file except in compliance with the License.&lt;br/&gt;  You may obtain a copy of the License at&lt;br/&gt;&lt;br/&gt;  &lt;a href=&#34;http://www.apache.org/licenses/LICENSE-2.0&#34;&gt;http://www.apache.org/licenses/LICENSE-2.0&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;  Unless required by applicable law or agreed to in writing, software&lt;br/&gt;  distributed under the License is distributed on an &amp;#34;AS IS&amp;#34; BASIS,&lt;br/&gt;  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&lt;br/&gt;  See the License for the specific language governing permissions and&lt;br/&gt;  limitations under the License.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  The &amp;#34;cargo wix&amp;#34; subcommand provides a variety of predefined variables available&lt;br/&gt;  for customization of this template. The values for each variable are set at&lt;br/&gt;  installer creation time. The following variables are available:&lt;br/&gt;&lt;br/&gt;  TargetTriple      = The rustc target triple name.&lt;br/&gt;  TargetEnv         = The rustc target environment. This is typically either&lt;br/&gt;                      &amp;#34;msvc&amp;#34; or &amp;#34;gnu&amp;#34; depending on the toolchain downloaded and&lt;br/&gt;                      installed.&lt;br/&gt;  TargetVendor      = The rustc target vendor. This is typically &amp;#34;pc&amp;#34;, but Rust&lt;br/&gt;                      does support other vendors, like &amp;#34;uwp&amp;#34;.&lt;br/&gt;  CargoTargetBinDir = The complete path to the directory containing the&lt;br/&gt;                      binaries (exes) to include. The default would be&lt;br/&gt;                      &amp;#34;target\release\&amp;#34;. If an explicit rustc target triple is&lt;br/&gt;                      used, i.e. cross-compiling, then the default path would&lt;br/&gt;                      be &amp;#34;target\&amp;lt;CARGO_TARGET&amp;gt;\&amp;lt;CARGO_PROFILE&amp;gt;&amp;#34;,&lt;br/&gt;                      where &amp;#34;&amp;lt;CARGO_TARGET&amp;gt;&amp;#34; is replaced with the &amp;#34;CargoTarget&amp;#34;&lt;br/&gt;                      variable value and &amp;#34;&amp;lt;CARGO_PROFILE&amp;gt;&amp;#34; is replaced with the&lt;br/&gt;                      value from the &amp;#34;CargoProfile&amp;#34; variable. This can also&lt;br/&gt;                      be overridden manually with the &amp;#34;target-bin-dir&amp;#34; flag.&lt;br/&gt;  CargoTargetDir    = The path to the directory for the build artifacts, i.e.&lt;br/&gt;                      &amp;#34;target&amp;#34;.&lt;br/&gt;  CargoProfile      = The cargo profile used to build the binaries&lt;br/&gt;                      (usually &amp;#34;debug&amp;#34; or &amp;#34;release&amp;#34;).&lt;br/&gt;  Version           = The version for the installer. The default is the&lt;br/&gt;                      &amp;#34;Major.Minor.Fix&amp;#34; semantic versioning number of the Rust&lt;br/&gt;                      package.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  Please do not remove these pre-processor If-Else blocks. These are used with&lt;br/&gt;  the `cargo wix` subcommand to automatically determine the installation&lt;br/&gt;  destination for 32-bit versus 64-bit installers. Removal of these lines will&lt;br/&gt;  cause installation errors.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&amp;lt;?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?&amp;gt;&lt;br/&gt;    &amp;lt;?define PlatformProgramFilesFolder = &amp;#34;ProgramFiles64Folder&amp;#34; ?&amp;gt;&lt;br/&gt;&amp;lt;?else ?&amp;gt;&lt;br/&gt;    &amp;lt;?define PlatformProgramFilesFolder = &amp;#34;ProgramFilesFolder&amp;#34; ?&amp;gt;&lt;br/&gt;&amp;lt;?endif ?&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;Wix xmlns=&amp;#39;&lt;a href=&#34;http://schemas.microsoft.com/wix/2006/wi&amp;#39;&amp;gt&#34;&gt;http://schemas.microsoft.com/wix/2006/wi&amp;#39;&amp;gt&lt;/a&gt;;&lt;br/&gt;&lt;br/&gt;    &amp;lt;Product&lt;br/&gt;        Id=&amp;#39;*&amp;#39;&lt;br/&gt;        Name=&amp;#39;n34&amp;#39;&lt;br/&gt;        UpgradeCode=&amp;#39;5E8926D0-4A18-48E9-A070-AFB0F698F04C&amp;#39;&lt;br/&gt;        Manufacturer=&amp;#39;Awiteb&amp;#39;&lt;br/&gt;        Language=&amp;#39;1033&amp;#39;&lt;br/&gt;        Codepage=&amp;#39;1252&amp;#39;&lt;br/&gt;        Version=&amp;#39;$(var.Version)&amp;#39;&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Package Id=&amp;#39;*&amp;#39;&lt;br/&gt;            Keywords=&amp;#39;Installer&amp;#39;&lt;br/&gt;            Description=&amp;#39;A CLI to interact with NIP-34 and other stuff related to code in Nostr&amp;#39;&lt;br/&gt;            Manufacturer=&amp;#39;Awiteb&amp;#39;&lt;br/&gt;            InstallerVersion=&amp;#39;450&amp;#39;&lt;br/&gt;            Languages=&amp;#39;1033&amp;#39;&lt;br/&gt;            Compressed=&amp;#39;yes&amp;#39;&lt;br/&gt;            InstallScope=&amp;#39;perMachine&amp;#39;&lt;br/&gt;            SummaryCodepage=&amp;#39;1252&amp;#39;&lt;br/&gt;            /&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;MajorUpgrade&lt;br/&gt;            Schedule=&amp;#39;afterInstallInitialize&amp;#39;&lt;br/&gt;            DowngradeErrorMessage=&amp;#39;A newer version of [ProductName] is already installed. Setup will now exit.&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Media Id=&amp;#39;1&amp;#39; Cabinet=&amp;#39;media1.cab&amp;#39; EmbedCab=&amp;#39;yes&amp;#39; DiskPrompt=&amp;#39;CD-ROM #1&amp;#39;/&amp;gt;&lt;br/&gt;        &amp;lt;Property Id=&amp;#39;DiskPrompt&amp;#39; Value=&amp;#39;n34 Installation&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Directory Id=&amp;#39;TARGETDIR&amp;#39; Name=&amp;#39;SourceDir&amp;#39;&amp;gt;&lt;br/&gt;            &amp;lt;Directory Id=&amp;#39;$(var.PlatformProgramFilesFolder)&amp;#39; Name=&amp;#39;PFiles&amp;#39;&amp;gt;&lt;br/&gt;                &amp;lt;Directory Id=&amp;#39;APPLICATIONFOLDER&amp;#39; Name=&amp;#39;n34&amp;#39;&amp;gt;&lt;br/&gt;                    &lt;br/&gt;                    &amp;lt;!--&lt;br/&gt;                      Enabling the license sidecar file in the installer is a four step process:&lt;br/&gt;&lt;br/&gt;                      1. Uncomment the `Component` tag and its contents.&lt;br/&gt;                      2. Change the value for the `Source` attribute in the `File` tag to a path&lt;br/&gt;                         to the file that should be included as the license sidecar file. The path&lt;br/&gt;                         can, and probably should be, relative to this file.&lt;br/&gt;                      3. Change the value for the `Name` attribute in the `File` tag to the&lt;br/&gt;                         desired name for the file when it is installed alongside the `bin` folder&lt;br/&gt;                         in the installation directory. This can be omitted if the desired name is&lt;br/&gt;                         the same as the file name.&lt;br/&gt;                      4. Uncomment the `ComponentRef` tag with the Id attribute value of &amp;#34;License&amp;#34;&lt;br/&gt;                         further down in this file.&lt;br/&gt;                    --&amp;gt;&lt;br/&gt;                    &amp;lt;!--&lt;br/&gt;                    &amp;lt;Component Id=&amp;#39;License&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                        &amp;lt;File Id=&amp;#39;LicenseFile&amp;#39; Name=&amp;#39;ChangeMe&amp;#39; DiskId=&amp;#39;1&amp;#39; Source=&amp;#39;C:\Path\To\File&amp;#39; KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                    &amp;lt;/Component&amp;gt;&lt;br/&gt;                    --&amp;gt;&lt;br/&gt;&lt;br/&gt;                    &amp;lt;Directory Id=&amp;#39;Bin&amp;#39; Name=&amp;#39;bin&amp;#39;&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;Path&amp;#39; Guid=&amp;#39;7A2E5F6A-FC23-498B-AEC1-5FA63678FBFD&amp;#39; KeyPath=&amp;#39;yes&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;Environment&lt;br/&gt;                                Id=&amp;#39;PATH&amp;#39;&lt;br/&gt;                                Name=&amp;#39;PATH&amp;#39;&lt;br/&gt;                                Value=&amp;#39;[Bin]&amp;#39;&lt;br/&gt;                                Permanent=&amp;#39;no&amp;#39;&lt;br/&gt;                                Part=&amp;#39;last&amp;#39;&lt;br/&gt;                                Action=&amp;#39;set&amp;#39;&lt;br/&gt;                                System=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary0&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe0&amp;#39;&lt;br/&gt;                                Name=&amp;#39;n34-cli.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\n34-cli.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                    &amp;lt;/Directory&amp;gt;&lt;br/&gt;                &amp;lt;/Directory&amp;gt;&lt;br/&gt;            &amp;lt;/Directory&amp;gt;&lt;br/&gt;        &amp;lt;/Directory&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Feature&lt;br/&gt;            Id=&amp;#39;Binaries&amp;#39;&lt;br/&gt;            Title=&amp;#39;Application&amp;#39;&lt;br/&gt;            Description=&amp;#39;Installs all binaries and the license.&amp;#39;&lt;br/&gt;            Level=&amp;#39;1&amp;#39;&lt;br/&gt;            ConfigurableDirectory=&amp;#39;APPLICATIONFOLDER&amp;#39;&lt;br/&gt;            AllowAdvertise=&amp;#39;no&amp;#39;&lt;br/&gt;            Display=&amp;#39;expand&amp;#39;&lt;br/&gt;            Absent=&amp;#39;disallow&amp;#39;&amp;gt;&lt;br/&gt;            &lt;br/&gt;            &amp;lt;!--&lt;br/&gt;              Uncomment the following `ComponentRef` tag to add the license&lt;br/&gt;              sidecar file to the installer.&lt;br/&gt;            --&amp;gt;&lt;br/&gt;            &amp;lt;!--&amp;lt;ComponentRef Id=&amp;#39;License&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary0&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;            &amp;lt;Feature&lt;br/&gt;                Id=&amp;#39;Environment&amp;#39;&lt;br/&gt;                Title=&amp;#39;PATH Environment Variable&amp;#39;&lt;br/&gt;                Description=&amp;#39;Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.&amp;#39;&lt;br/&gt;                Level=&amp;#39;1&amp;#39;&lt;br/&gt;                Absent=&amp;#39;allow&amp;#39;&amp;gt;&lt;br/&gt;                &amp;lt;ComponentRef Id=&amp;#39;Path&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;/Feature&amp;gt;&lt;br/&gt;        &amp;lt;/Feature&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;SetProperty Id=&amp;#39;ARPINSTALLLOCATION&amp;#39; Value=&amp;#39;[APPLICATIONFOLDER]&amp;#39; After=&amp;#39;CostFinalize&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the following `Icon` and `Property` tags to change the product icon.&lt;br/&gt;&lt;br/&gt;          The product icon is the graphic that appears in the Add/Remove&lt;br/&gt;          Programs control panel for the application.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;Icon Id=&amp;#39;ProductICO&amp;#39; SourceFile=&amp;#39;wix\Product.ico&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;Property Id=&amp;#39;ARPPRODUCTICON&amp;#39; Value=&amp;#39;ProductICO&amp;#39; /&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Property Id=&amp;#39;ARPHELPLINK&amp;#39; Value=&amp;#39;&lt;a href=&#34;https://n34.dev/commands.html&amp;#39;/&amp;gt&#34;&gt;https://n34.dev/commands.html&amp;#39;/&amp;gt&lt;/a&gt;;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;UI&amp;gt;&lt;br/&gt;            &amp;lt;UIRef Id=&amp;#39;WixUI_FeatureTree&amp;#39;/&amp;gt;&lt;br/&gt;            &lt;br/&gt;            &amp;lt;!--&lt;br/&gt;              Enabling the EULA dialog in the installer is a three step process:&lt;br/&gt;&lt;br/&gt;                1. Comment out or remove the two `Publish` tags that follow the&lt;br/&gt;                   `WixVariable` tag.&lt;br/&gt;                2. Uncomment the `&amp;lt;WixVariable Id=&amp;#39;WixUILicenseRtf&amp;#39; Value=&amp;#39;Path\to\Eula.rft&amp;#39;&amp;gt;` tag further down&lt;br/&gt;                3. Replace the `Value` attribute of the `WixVariable` tag with&lt;br/&gt;                   the path to a RTF file that will be used as the EULA and&lt;br/&gt;                   displayed in the license agreement dialog.&lt;br/&gt;            --&amp;gt;&lt;br/&gt;            &amp;lt;Publish Dialog=&amp;#39;WelcomeDlg&amp;#39; Control=&amp;#39;Next&amp;#39; Event=&amp;#39;NewDialog&amp;#39; Value=&amp;#39;CustomizeDlg&amp;#39; Order=&amp;#39;99&amp;#39;&amp;gt;1&amp;lt;/Publish&amp;gt;&lt;br/&gt;            &amp;lt;Publish Dialog=&amp;#39;CustomizeDlg&amp;#39; Control=&amp;#39;Back&amp;#39; Event=&amp;#39;NewDialog&amp;#39; Value=&amp;#39;WelcomeDlg&amp;#39; Order=&amp;#39;99&amp;#39;&amp;gt;1&amp;lt;/Publish&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;/UI&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Enabling the EULA dialog in the installer requires uncommenting&lt;br/&gt;          the following `WixUILicenseRTF` tag and changing the `Value`&lt;br/&gt;          attribute.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!-- &amp;lt;WixVariable Id=&amp;#39;WixUILicenseRtf&amp;#39; Value=&amp;#39;Relative\Path\to\Eula.rtf&amp;#39;/&amp;gt; --&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the next `WixVariable` tag to customize the installer&amp;#39;s&lt;br/&gt;          Graphical User Interface (GUI) and add a custom banner image across&lt;br/&gt;          the top of each screen. See the WiX Toolset documentation for details&lt;br/&gt;          about customization.&lt;br/&gt;&lt;br/&gt;          The banner BMP dimensions are 493 x 58 pixels.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;WixVariable Id=&amp;#39;WixUIBannerBmp&amp;#39; Value=&amp;#39;wix\Banner.bmp&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the next `WixVariable` tag to customize the installer&amp;#39;s&lt;br/&gt;          Graphical User Interface (GUI) and add a custom image to the first&lt;br/&gt;          dialog, or screen. See the WiX Toolset documentation for details about&lt;br/&gt;          customization.&lt;br/&gt;&lt;br/&gt;          The dialog BMP dimensions are 493 x 312 pixels.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;WixVariable Id=&amp;#39;WixUIDialogBmp&amp;#39; Value=&amp;#39;wix\Dialog.bmp&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;/Product&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;/Wix&amp;gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:52:54Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqszjenfyhrzzjm9fvuw23r305peqnrzhdxy5laj3y0v92mrqejm3wgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvzn7cen</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqszjenfyhrzzjm9fvuw23r305peqnrzhdxy5laj3y0v92mrqejm3wgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvzn7cen" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    fmt,&lt;br/&gt;    fs,&lt;br/&gt;    path::{Path, PathBuf},&lt;br/&gt;    str::FromStr,&lt;br/&gt;    sync::atomic::Ordering,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventId, Kind, Tag, TagKind, TagStandard},&lt;br/&gt;    filter::Alphabet,&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    nips::{&lt;br/&gt;        nip01::Coordinate,&lt;br/&gt;        nip10::Marker,&lt;br/&gt;        nip19::{Nip19Coordinate, Nip19Event, ToBech32},&lt;br/&gt;        nip34::GitRepositoryAnnouncement,&lt;br/&gt;        nip65::{self, RelayMetadata},&lt;br/&gt;    },&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use super::traits::TagsExt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{NOSTR_ADDRESS_FILE, parsers},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Returns the value of the given tag&lt;br/&gt;#[inline]&lt;br/&gt;fn tag_value(tag: &amp;amp;TagStandard) -&amp;gt; String {&lt;br/&gt;    tag.clone().to_vec().remove(1)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses the tag value into type `T` if possible.&lt;br/&gt;#[inline]&lt;br/&gt;fn parse_value&amp;lt;T: FromStr&amp;gt;(tag: &amp;amp;TagStandard) -&amp;gt; Option&amp;lt;T&amp;gt; {&lt;br/&gt;    tag_value(tag).parse().ok()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Gets all values from the tag. If any value fails to parse, returns an empty&lt;br/&gt;/// vector.&lt;br/&gt;#[inline]&lt;br/&gt;fn tag_values&amp;lt;T&amp;gt;(tag: &amp;amp;TagStandard) -&amp;gt; Vec&amp;lt;T&amp;gt;&lt;br/&gt;where&lt;br/&gt;    T: FromStr &#43; fmt::Debug,&lt;br/&gt;    &amp;lt;T as FromStr&amp;gt;::Err: fmt::Debug,&lt;br/&gt;{&lt;br/&gt;    tag.clone()&lt;br/&gt;        .to_vec()&lt;br/&gt;        .into_iter()&lt;br/&gt;        .skip(1)&lt;br/&gt;        .map(|t| {&lt;br/&gt;            let result = T::from_str(t.as_str());&lt;br/&gt;            tracing::trace!(&amp;#34;Parsing `{t}` result: `{result:?}`&amp;#34;);&lt;br/&gt;            result&lt;br/&gt;        })&lt;br/&gt;        .collect::&amp;lt;Result&amp;lt;_, _&amp;gt;&amp;gt;()&lt;br/&gt;        .unwrap_or_default()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Convert [`Event`] to [`GitRepositoryAnnouncement`]&lt;br/&gt;pub fn event_into_repo(event: Event, repo_id: impl Into&amp;lt;String&amp;gt;) -&amp;gt; GitRepositoryAnnouncement {&lt;br/&gt;    let tags = &amp;amp;event.tags;&lt;br/&gt;&lt;br/&gt;    GitRepositoryAnnouncement {&lt;br/&gt;        id:          repo_id.into(),&lt;br/&gt;        name:        tags.map_tag(TagKind::Name, tag_value),&lt;br/&gt;        description: tags.map_tag(TagKind::Description, tag_value),&lt;br/&gt;        euc:         tags&lt;br/&gt;            .map_marker(&lt;br/&gt;                TagKind::single_letter(Alphabet::R, false),&lt;br/&gt;                &amp;#34;euc&amp;#34;,&lt;br/&gt;                parse_value,&lt;br/&gt;            )&lt;br/&gt;            .flatten(),&lt;br/&gt;        web:         tags.dmap_tag(TagKind::Web, tag_values),&lt;br/&gt;        clone:       tags.dmap_tag(TagKind::Clone, tag_values),&lt;br/&gt;        relays:      tags.dmap_tag(TagKind::Relays, tag_values),&lt;br/&gt;        maintainers: tags.dmap_tag(TagKind::Maintainers, tag_values),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns a new string with leading and trailing whitespace removed.&lt;br/&gt;pub fn str_trim(s: String) -&amp;gt; String {&lt;br/&gt;    s.trim().to_owned()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns a vector with duplicate elements removed.&lt;br/&gt;pub fn dedup&amp;lt;I, T&amp;gt;(iter: I) -&amp;gt; Vec&amp;lt;T&amp;gt;&lt;br/&gt;where&lt;br/&gt;    T: std::cmp::Ord,&lt;br/&gt;    I: Iterator&amp;lt;Item = T&amp;gt;,&lt;br/&gt;{&lt;br/&gt;    let mut vector: Vec&amp;lt;T&amp;gt; = iter.collect();&lt;br/&gt;    vector.sort_unstable();&lt;br/&gt;    vector.dedup();&lt;br/&gt;    vector&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Sorts items from the iterator using the given key function.&lt;br/&gt;/// The sorting is unstable, but faster than stable sorting.&lt;br/&gt;pub fn sort_by_key&amp;lt;I, T, K&amp;gt;(iterator: I, key: impl FnMut(&amp;amp;T) -&amp;gt; K) -&amp;gt; impl Iterator&amp;lt;Item = T&amp;gt;&lt;br/&gt;where&lt;br/&gt;    I: IntoIterator&amp;lt;Item = T&amp;gt;,&lt;br/&gt;    K: Ord,&lt;br/&gt;{&lt;br/&gt;    let mut vector = Vec::&amp;lt;T&amp;gt;::from_iter(iterator);&lt;br/&gt;    vector.sort_unstable_by_key(key);&lt;br/&gt;    vector.into_iter()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates a new NIP-19 nevent string from an event ID and up to 3 unique relay&lt;br/&gt;/// URLs.&lt;br/&gt;#[inline]&lt;br/&gt;pub fn new_nevent(event_id: EventId, relays: &amp;amp;[RelayUrl]) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    Nip19Event::new(event_id)&lt;br/&gt;        .relays(&lt;br/&gt;            dedup(relays.iter().cloned())&lt;br/&gt;                .into_iter()&lt;br/&gt;                .take(3)&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;(),&lt;br/&gt;        )&lt;br/&gt;        .to_bech32()&lt;br/&gt;        .map_err(N34Error::from)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates a NIP-19 naddr string for a git repository announcement and up to 3&lt;br/&gt;/// unique relay URLs.&lt;br/&gt;#[inline]&lt;br/&gt;pub fn repo_naddr(&lt;br/&gt;    repo_id: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;    pubk: PublicKey,&lt;br/&gt;    relays: &amp;amp;[RelayUrl],&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    Nip19Coordinate::new(&lt;br/&gt;        Coordinate::new(Kind::GitRepoAnnouncement, pubk).identifier(repo_id),&lt;br/&gt;        dedup(relays.iter().cloned()).into_iter().take(3),&lt;br/&gt;    )&lt;br/&gt;    .to_bech32()&lt;br/&gt;    .map_err(N34Error::from)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts write relay URLs from an event if present, otherwise returns an&lt;br/&gt;/// empty vector.&lt;br/&gt;pub fn add_write_relays(event: Option&amp;lt;&amp;amp;Event&amp;gt;) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;    let mut vector = Vec::new();&lt;br/&gt;    if let Some(event) = event {&lt;br/&gt;        vector.extend(&lt;br/&gt;            nip65::extract_owned_relay_list(event.clone())&lt;br/&gt;                .filter_map(|(r, m)| m.is_none_or(|m| m == RelayMetadata::Write).then_some(r)),&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;    vector&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts read relay URLs from an event if present, otherwise returns an&lt;br/&gt;/// empty vector.&lt;br/&gt;pub fn add_read_relays(event: Option&amp;lt;&amp;amp;Event&amp;gt;) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;    let mut vector = Vec::new();&lt;br/&gt;    if let Some(event) = event {&lt;br/&gt;        vector.extend(&lt;br/&gt;            nip65::extract_owned_relay_list(event.clone())&lt;br/&gt;                .filter_map(|(r, m)| m.is_none_or(|m| m == RelayMetadata::Read).then_some(r)),&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;    vector&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Opens the user&amp;#39;s default editor ($EDITOR) to edit a temporary file with&lt;br/&gt;/// given suffix, then reads and returns the file contents. The temporary file&lt;br/&gt;/// is automatically deleted.&lt;br/&gt;pub fn read_editor(file_pre_content: Option&amp;lt;&amp;amp;str&amp;gt;, file_suffix: &amp;amp;str) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    let Ok(editor) = std::env::var(&amp;#34;EDITOR&amp;#34;) else {&lt;br/&gt;        return Err(N34Error::EditorNotFound);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let temp_path = tempfile::NamedTempFile::with_suffix(file_suffix)?.into_temp_path();&lt;br/&gt;&lt;br/&gt;    if let Some(pre_content) = file_pre_content {&lt;br/&gt;        fs::write(&amp;amp;temp_path, pre_content)?;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Disable the logs to not show up in a terminal text editor&lt;br/&gt;    crate::EDITOR_OPEN.store(true, Ordering::Relaxed);&lt;br/&gt;    let exit_status = std::process::Command::new(&amp;amp;editor)&lt;br/&gt;        .arg(temp_path.to_str().expect(&amp;#34;The path is valid utf8&amp;#34;))&lt;br/&gt;        .spawn()?&lt;br/&gt;        .wait()?;&lt;br/&gt;    crate::EDITOR_OPEN.store(false, Ordering::Relaxed);&lt;br/&gt;&lt;br/&gt;    if !exit_status.success()&lt;br/&gt;        &amp;amp;&amp;amp; let Some(code) = exit_status.code()&lt;br/&gt;    {&lt;br/&gt;        return Err(N34Error::EditorErr(editor, code));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let content = fs::read_to_string(&amp;amp;temp_path)&lt;br/&gt;        .map_err(N34Error::from)?&lt;br/&gt;        .trim()&lt;br/&gt;        .to_owned();&lt;br/&gt;&lt;br/&gt;    if content.is_empty() {&lt;br/&gt;        return Err(N34Error::EmptyEditorFile);&lt;br/&gt;    }&lt;br/&gt;    Ok(content)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns the given content if it&amp;#39;s `Option::Some` or call [`read_editor`]&lt;br/&gt;pub fn get_content(&lt;br/&gt;    content: Option&amp;lt;impl AsRef&amp;lt;str&amp;gt;&amp;gt;,&lt;br/&gt;    quoted_content: Option&amp;lt;impl AsRef&amp;lt;str&amp;gt;&amp;gt;,&lt;br/&gt;    file_suffix: &amp;amp;str,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    if let Some(content) = content {&lt;br/&gt;        return Ok(content.as_ref().trim().to_owned());&lt;br/&gt;    }&lt;br/&gt;    read_editor(&lt;br/&gt;        quoted_content.map(|s| s.as_ref().to_owned()).as_deref(),&lt;br/&gt;        file_suffix,&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Path to the `nostr-address` file in current directory.&lt;br/&gt;#[inline]&lt;br/&gt;pub fn nostr_address_path() -&amp;gt; std::io::Result&amp;lt;PathBuf&amp;gt; {&lt;br/&gt;    std::env::current_dir().map(|p| p.join(NOSTR_ADDRESS_FILE))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns the given coordinates if Some, otherwise attempts to read and parse&lt;br/&gt;/// coordinates from the specified file. Returns an empty vector if the file&lt;br/&gt;/// doesn&amp;#39;t exist.&lt;br/&gt;pub fn naddrs_or_file(&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt;,&lt;br/&gt;    address_file_path: &amp;amp;Path,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;    if let Some(naddrs) = naddrs {&lt;br/&gt;        return Ok(naddrs);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if address_file_path.exists() {&lt;br/&gt;        parsers::parse_nostr_address_file(address_file_path)&lt;br/&gt;    } else {&lt;br/&gt;        Ok(Vec::new())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Generate a reply tag for an event with the given ID, relay URL (if any), and&lt;br/&gt;/// marker.&lt;br/&gt;#[inline]&lt;br/&gt;pub fn event_reply_tag(reply_to: &amp;amp;EventId, relay: Option&amp;lt;&amp;amp;RelayUrl&amp;gt;, marker: Marker) -&amp;gt; Tag {&lt;br/&gt;    Tag::custom(&lt;br/&gt;        TagKind::e(),&lt;br/&gt;        [&lt;br/&gt;            reply_to.to_hex(),&lt;br/&gt;            relay.map(|r| r.to_string()).unwrap_or_default(),&lt;br/&gt;            marker.to_string(),&lt;br/&gt;        ],&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Wraps text into lines no longer than max_width, breaking only at whitespace.&lt;br/&gt;pub fn smart_wrap(text: &amp;amp;str, max_width: usize) -&amp;gt; String {&lt;br/&gt;    text.lines()&lt;br/&gt;        .map(|line| {&lt;br/&gt;            if !line.trim().is_empty() {&lt;br/&gt;                line.split(&amp;#34; &amp;#34;)&lt;br/&gt;                    .fold((String::new(), 0), |(result, last_newline), word| {&lt;br/&gt;                        let result_len = result.chars().count();&lt;br/&gt;                        if result_len == 0 {&lt;br/&gt;                            (word.to_owned(), 0)&lt;br/&gt;                        } else if (result_len - last_newline) &#43; word.chars().count() &amp;gt; max_width {&lt;br/&gt;                            (format!(&amp;#34;{result}\n{word}&amp;#34;), result_len &#43; 1)&lt;br/&gt;                        } else {&lt;br/&gt;                            (format!(&amp;#34;{result} {word}&amp;#34;), last_newline)&lt;br/&gt;                        }&lt;br/&gt;                    })&lt;br/&gt;                    .0&lt;br/&gt;            } else {&lt;br/&gt;                String::new()&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;        .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;        .join(&amp;#34;\n&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns an error if the given naddrs is empty otherwise returned it&lt;br/&gt;pub fn check_empty_naddrs(naddrs: Vec&amp;lt;Nip19Coordinate&amp;gt;) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;    if naddrs.is_empty() {&lt;br/&gt;        return Err(N34Error::EmptyNaddrs);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(naddrs)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:52:43Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsyzhf5r4xq65n428rvgn7mnytkv4kaurmy4kr2c5p8mhfdv2jnqdqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvr4fheh</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsyzhf5r4xq65n428rvgn7mnytkv4kaurmy4kr2c5p8mhfdv2jnqdqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvr4fheh" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use convert_case::{Case, Casing};&lt;br/&gt;use nostr::hashes::sha1::Hash as Sha1Hash;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard, Tags},&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    nips::{&lt;br/&gt;        nip01::Coordinate,&lt;br/&gt;        nip19::Nip19Coordinate,&lt;br/&gt;        nip21::Nip21,&lt;br/&gt;        nip34::{GitIssue, GitRepositoryAnnouncement},&lt;br/&gt;    },&lt;br/&gt;    parser::Token,&lt;br/&gt;    types::{RelayUrl, Url},&lt;br/&gt;};&lt;br/&gt;use nostr_keyring::KeyringError;&lt;br/&gt;&lt;br/&gt;use crate::cli::issue::ISSUE_ALT_PREFIX;&lt;br/&gt;use crate::cli::patch::{&lt;br/&gt;    LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT,&lt;br/&gt;    REVISION_ROOT_HASHTAG_CONTENT,&lt;br/&gt;    ROOT_HASHTAG_CONTENT,&lt;br/&gt;};&lt;br/&gt;use crate::error::{N34Error, N34Result};&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// A trait to add helper instance function to [`Tags`] type&lt;br/&gt;#[easy_ext::ext(TagsExt)]&lt;br/&gt;impl Tags {&lt;br/&gt;    /// Search for the given tag and map it value to a function&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn map_tag&amp;lt;T&amp;gt;(&amp;amp;self, kind: TagKind, f: impl FnOnce(&amp;amp;TagStandard) -&amp;gt; T) -&amp;gt; Option&amp;lt;T&amp;gt; {&lt;br/&gt;        self.find_standardized(kind).map(f)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Search for the given tag and map it value to a function. If the tag not&lt;br/&gt;    /// found return the default `T`&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn dmap_tag&amp;lt;T&amp;gt;(&amp;amp;self, kind: TagKind, f: impl FnOnce(&amp;amp;TagStandard) -&amp;gt; T) -&amp;gt; T&lt;br/&gt;    where&lt;br/&gt;        T: Default,&lt;br/&gt;    {&lt;br/&gt;        self.map_tag(kind, f).unwrap_or_default()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finds the first standard tag of the given kind with the specified&lt;br/&gt;    /// marker, then applies the function to the tag and returns the result.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn map_marker&amp;lt;T&amp;gt;(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        kind: TagKind,&lt;br/&gt;        marker: &amp;amp;str,&lt;br/&gt;        f: impl FnOnce(&amp;amp;TagStandard) -&amp;gt; T,&lt;br/&gt;    ) -&amp;gt; Option&amp;lt;T&amp;gt; {&lt;br/&gt;        self.filter_standardized(kind)&lt;br/&gt;            .find(|t| (*t).clone().to_vec().last().is_some_and(|m| m == marker))&lt;br/&gt;            .map(f)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Trait for building [`GitRepositoryAnnouncement`] events&lt;br/&gt;#[easy_ext::ext(NewGitRepositoryAnnouncement)]&lt;br/&gt;impl EventBuilder {&lt;br/&gt;    /// Creates a new [`GitRepositoryAnnouncement`] event builder with the given&lt;br/&gt;    /// repository details.&lt;br/&gt;    #[allow(clippy::too_many_arguments)]&lt;br/&gt;    pub fn new_git_repo(&lt;br/&gt;        repo_id: String,&lt;br/&gt;        name: Option&amp;lt;String&amp;gt;,&lt;br/&gt;        description: Option&amp;lt;String&amp;gt;,&lt;br/&gt;        web: Vec&amp;lt;Url&amp;gt;,&lt;br/&gt;        clone: Vec&amp;lt;Url&amp;gt;,&lt;br/&gt;        relays: Vec&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;        maintainers: Vec&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;        labels: Vec&amp;lt;String&amp;gt;,&lt;br/&gt;        force_id: bool,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;EventBuilder&amp;gt; {&lt;br/&gt;        let repo_id = repo_id.trim();&lt;br/&gt;        let kebab_repo_id = repo_id.to_case(Case::Kebab);&lt;br/&gt;        if repo_id.is_empty() || (!force_id &amp;amp;&amp;amp; repo_id != kebab_repo_id) {&lt;br/&gt;            if repo_id != kebab_repo_id {&lt;br/&gt;                tracing::error!(&lt;br/&gt;                    &amp;#34;The repo id should be `{kebab_repo_id}` (kebab-case). Use `--force-id` to \&lt;br/&gt;                     override this check&amp;#34;&lt;br/&gt;                );&lt;br/&gt;            }&lt;br/&gt;            return Err(N34Error::InvalidRepoId);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(&lt;br/&gt;            EventBuilder::git_repository_announcement(GitRepositoryAnnouncement {&lt;br/&gt;                id: repo_id.to_owned(),&lt;br/&gt;                name,&lt;br/&gt;                description,&lt;br/&gt;                web,&lt;br/&gt;                clone,&lt;br/&gt;                relays,&lt;br/&gt;                euc: None,&lt;br/&gt;                maintainers,&lt;br/&gt;            })?&lt;br/&gt;            .dedup_tags()&lt;br/&gt;            .tags(labels.into_iter().map(Tag::hashtag)),&lt;br/&gt;        )&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Creates a new [`GitIssue`] event builder with the given&lt;br/&gt;    /// issue details.&lt;br/&gt;    pub fn new_git_issue(&lt;br/&gt;        coordinates: &amp;amp;[Coordinate],&lt;br/&gt;        content: String,&lt;br/&gt;        subject: Option&amp;lt;String&amp;gt;,&lt;br/&gt;        labels: Vec&amp;lt;String&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;EventBuilder&amp;gt; {&lt;br/&gt;        let mut coordinates = coordinates.iter();&lt;br/&gt;        let first_coordinate = coordinates.next().ok_or(N34Error::EmptyNaddrs)?;&lt;br/&gt;&lt;br/&gt;        let mut event_builder = EventBuilder::git_issue(GitIssue {&lt;br/&gt;            repository: first_coordinate.clone(),&lt;br/&gt;            content,&lt;br/&gt;            subject: subject.clone(),&lt;br/&gt;            labels: labels.into_iter().map(|l| l.trim().to_owned()).collect(),&lt;br/&gt;        })&lt;br/&gt;        .map_err(N34Error::from)?&lt;br/&gt;        .tags(&lt;br/&gt;            coordinates&lt;br/&gt;                .clone()&lt;br/&gt;                .map(|c| Tag::coordinate(c.clone(), None)),&lt;br/&gt;        )&lt;br/&gt;        .tags(coordinates.map(|c| Tag::public_key(c.public_key)));&lt;br/&gt;&lt;br/&gt;        if let Some(issue_subject) = subject {&lt;br/&gt;            event_builder =&lt;br/&gt;                event_builder.tag(Tag::alt(format!(&amp;#34;{ISSUE_ALT_PREFIX}{issue_subject}&amp;#34;)))&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(event_builder)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Helper functions for [`Token`] type&lt;br/&gt;#[easy_ext::ext(TokenUtils)]&lt;br/&gt;impl Token&amp;lt;&amp;#39;_&amp;gt; {&lt;br/&gt;    /// Returns `Some((public_key, relays))` from the givin token if it&amp;#39;s npub1&lt;br/&gt;    /// or nprofile1&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_public_key(&amp;amp;self) -&amp;gt; Option&amp;lt;(PublicKey, Vec&amp;lt;RelayUrl&amp;gt;)&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Token::Nostr(nip21) =&amp;gt; {&lt;br/&gt;                match nip21 {&lt;br/&gt;                    Nip21::Pubkey(pkey) =&amp;gt; Some((*pkey, Vec::new())),&lt;br/&gt;                    Nip21::Profile(profile) =&amp;gt; Some((profile.public_key, profile.relays.clone())),&lt;br/&gt;                    _ =&amp;gt; None,&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            _ =&amp;gt; None,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `Some((note_id, relays))` from the givin token if it&amp;#39;s note1 or&lt;br/&gt;    /// nevent1&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_event_id(&amp;amp;self) -&amp;gt; Option&amp;lt;(EventId, Vec&amp;lt;RelayUrl&amp;gt;)&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Token::Nostr(nip21) =&amp;gt; {&lt;br/&gt;                match nip21 {&lt;br/&gt;                    Nip21::EventId(event_id) =&amp;gt; Some((*event_id, Vec::new())),&lt;br/&gt;                    Nip21::Event(event) =&amp;gt; Some((event.event_id, event.relays.clone())),&lt;br/&gt;                    _ =&amp;gt; None,&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            _ =&amp;gt; None,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `Some(hashtag)` from the givin token if it&amp;#39;s a hashtag&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_hashtag(&amp;amp;self) -&amp;gt; Option&amp;lt;String&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Token::Hashtag(tag) =&amp;gt; Some(tag.trim().to_owned()),&lt;br/&gt;            _ =&amp;gt; None,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Utility functions for working with lists of NIP-19 coordinates&lt;br/&gt;#[easy_ext::ext(NaddrsUtils)]&lt;br/&gt;impl Vec&amp;lt;Nip19Coordinate&amp;gt; {&lt;br/&gt;    /// Converts these coordinate addresses to basic coordinates&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn into_coordinates(self) -&amp;gt; Vec&amp;lt;Coordinate&amp;gt; {&lt;br/&gt;        self.into_iter().map(|n| n.coordinate).collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns all repository owners&amp;#39; public keys from these coordinates.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_owners(&amp;amp;self) -&amp;gt; Vec&amp;lt;PublicKey&amp;gt; {&lt;br/&gt;        self.iter().map(|n| n.public_key).collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Extracts all relay URLs from these coordinates&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_relays(&amp;amp;self) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;        self.iter().flat_map(|n| n.relays.clone()).collect()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Utility functions for working with lists of repository announcement&lt;br/&gt;#[easy_ext::ext(ReposUtils)]&lt;br/&gt;impl Vec&amp;lt;GitRepositoryAnnouncement&amp;gt; {&lt;br/&gt;    /// Extracts all relay URLs from these repositories&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_relays(&amp;amp;self) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;        self.iter().flat_map(|n| n.relays.clone()).collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Extract all the maintainers from these repositories&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_maintainers(&amp;amp;self) -&amp;gt; Vec&amp;lt;PublicKey&amp;gt; {&lt;br/&gt;        self.iter().flat_map(|r| r.maintainers.clone()).collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the first EUC hash from the reposotoies if it exists.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_euc(&amp;amp;self) -&amp;gt; Option&amp;lt;&amp;amp;Sha1Hash&amp;gt; {&lt;br/&gt;        self.iter().find_map(|r| r.euc.as_ref())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Utility functions for working with patch events&lt;br/&gt;#[easy_ext::ext(GitPatchUtils)]&lt;br/&gt;impl Event {&lt;br/&gt;    /// Returns whether the patch is a root or not&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_root_patch(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.kind == Kind::GitPatch&lt;br/&gt;            &amp;amp;&amp;amp; self&lt;br/&gt;                .tags&lt;br/&gt;                .filter(TagKind::t())&lt;br/&gt;                .any(|t| t.content() == Some(ROOT_HASHTAG_CONTENT))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns whether the patch is patch-revision or not&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_revision_patch(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.kind == Kind::GitPatch&lt;br/&gt;            &amp;amp;&amp;amp; self.tags.filter(TagKind::t()).any(|t| {&lt;br/&gt;                [&lt;br/&gt;                    Some(REVISION_ROOT_HASHTAG_CONTENT),&lt;br/&gt;                    Some(LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT),&lt;br/&gt;                ]&lt;br/&gt;                .contains(&amp;amp;t.content())&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the root patch ID from a patch-revision event by finding the `e`&lt;br/&gt;    /// tag that replies to it. Fails if no such tag is found or if the tag&lt;br/&gt;    /// contains an invalid event ID.&lt;br/&gt;    pub fn root_patch_from_revision(&amp;amp;self) -&amp;gt; N34Result&amp;lt;EventId&amp;gt; {&lt;br/&gt;        self.tags&lt;br/&gt;            .iter()&lt;br/&gt;            .find(|tag| tag.is_reply())&lt;br/&gt;            .ok_or_else(|| {&lt;br/&gt;                N34Error::InvalidEvent(&lt;br/&gt;                    &amp;#34;A patch revision without `e`-reply to the root patch&amp;#34;.to_owned(),&lt;br/&gt;                )&lt;br/&gt;            })?&lt;br/&gt;            .content()&lt;br/&gt;            .ok_or_else(|| N34Error::InvalidEvent(&amp;#34;`e` tag without an event&amp;#34;.to_owned()))?&lt;br/&gt;            .parse()&lt;br/&gt;            .map_err(|err| N34Error::InvalidEvent(format!(&amp;#34;Invalid event ID in `e` tag: {err}&amp;#34;)))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Utility functions for working with issue events&lt;br/&gt;#[easy_ext::ext(GitIssueUtils)]&lt;br/&gt;impl Event {&lt;br/&gt;    /// Gets the subject line of the issue or &amp;#34;N/A&amp;#34; if none exists&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_issue_subject(&amp;amp;self) -&amp;gt; &amp;amp;str {&lt;br/&gt;        self.tags&lt;br/&gt;            .find(TagKind::Subject)&lt;br/&gt;            .and_then(|t| t.content())&lt;br/&gt;            .unwrap_or(&amp;#34;N/A&amp;#34;)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets all issue labels formatted as comma-separated hashtags (e.g. &amp;#34;#bug,&lt;br/&gt;    /// #feature&amp;#34;)&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_issue_labels(&amp;amp;self) -&amp;gt; String {&lt;br/&gt;        self.tags&lt;br/&gt;            .filter(TagKind::t())&lt;br/&gt;            .filter_map(|t| t.content().map(|l| format!(&amp;#34;#{l}&amp;#34;)))&lt;br/&gt;            .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;            .join(&amp;#34;, &amp;#34;)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(NostrKeyringErrorUtils)]&lt;br/&gt;impl nostr_keyring::Error {&lt;br/&gt;    /// Checks if the error indicates a missing keyring entry.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_keyring_no_entry(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, nostr_keyring::Error::Keyring(KeyringError::NoEntry))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:52:32Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqst27jvq9lj4le6e5msh4q2xzfkeymyy03mxn3q6t26anh3eu67nxgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7jmh25</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqst27jvq9lj4le6e5msh4q2xzfkeymyy03mxn3q6t26anh3eu67nxgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7jmh25" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Extension traits for nostr types.&lt;br/&gt;pub mod traits;&lt;br/&gt;/// Utility functions for nostr.&lt;br/&gt;pub mod utils;&lt;br/&gt;&lt;br/&gt;use std::{collections::HashSet, time::Duration};&lt;br/&gt;&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventId, Kind, Tag, TagKind, TagStandard, Tags, UnsignedEvent},&lt;br/&gt;    filter::Filter,&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    nips::{&lt;br/&gt;        nip01::{Coordinate, Metadata},&lt;br/&gt;        nip19::ToBech32,&lt;br/&gt;        nip22,&lt;br/&gt;        nip34::GitRepositoryAnnouncement,&lt;br/&gt;    },&lt;br/&gt;    parser::NostrParser,&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;use nostr_sdk::{Client, ClientOptions};&lt;br/&gt;use traits::TokenUtils;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, issue::IssueStatus, patch::PatchStatus},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Timeout duration for the client.&lt;br/&gt;const CLIENT_TIMEOUT: Duration = Duration::from_millis(1500);&lt;br/&gt;/// Length of a Nostr npub (public key) in characters.&lt;br/&gt;const NPUB_LEN: usize = 63;&lt;br/&gt;&lt;br/&gt;/// Parsed content details&lt;br/&gt;#[derive(Clone)]&lt;br/&gt;pub struct ContentDetails {&lt;br/&gt;    /// Public keys of users mentioned in the content.&lt;br/&gt;    pub p_tagged:     HashSet&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;    /// Event IDs and optional relay URLs for quoted events.&lt;br/&gt;    pub quotes:       HashSet&amp;lt;(EventId, Option&amp;lt;RelayUrl&amp;gt;)&amp;gt;,&lt;br/&gt;    /// Hashtags found in the content.&lt;br/&gt;    pub hashtags:     HashSet&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Relays where mentioned users and quoted authors are read.&lt;br/&gt;    pub write_relays: HashSet&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// A client for interacting with the Nostr relays&lt;br/&gt;#[derive(Clone)]&lt;br/&gt;pub struct NostrClient {&lt;br/&gt;    /// The underlying Nostr client implementation&lt;br/&gt;    pub client: Client,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl ContentDetails {&lt;br/&gt;    /// Create a new [`ContentDetails`] instance&lt;br/&gt;    pub fn new(&lt;br/&gt;        users: impl IntoIterator&amp;lt;Item = PublicKey&amp;gt;,&lt;br/&gt;        quotes: impl IntoIterator&amp;lt;Item = (EventId, Option&amp;lt;RelayUrl&amp;gt;)&amp;gt;,&lt;br/&gt;        hashtags: impl IntoIterator&amp;lt;Item = String&amp;gt;,&lt;br/&gt;        write_relays: impl IntoIterator&amp;lt;Item = RelayUrl&amp;gt;,&lt;br/&gt;    ) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            p_tagged:     HashSet::from_iter(users),&lt;br/&gt;            quotes:       HashSet::from_iter(quotes),&lt;br/&gt;            hashtags:     HashSet::from_iter(hashtags),&lt;br/&gt;            write_relays: HashSet::from_iter(write_relays),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Converts the instance into a list of tags including hashtags, p-tagged&lt;br/&gt;    /// users, and quoted events.&lt;br/&gt;    pub fn into_tags(self) -&amp;gt; Tags {&lt;br/&gt;        let mut tags = Tags::new();&lt;br/&gt;        tags.extend(self.hashtags.into_iter().map(Tag::hashtag));&lt;br/&gt;        tags.extend(self.p_tagged.into_iter().map(Tag::public_key));&lt;br/&gt;        tags.extend(self.quotes.into_iter().map(|(event_id, relay_url)| {&lt;br/&gt;            // TODO: Add the author public key if we know it&lt;br/&gt;            Tag::from_standardized(TagStandard::Quote {&lt;br/&gt;                event_id,&lt;br/&gt;                relay_url,&lt;br/&gt;                public_key: None,&lt;br/&gt;            })&lt;br/&gt;        }));&lt;br/&gt;        tags&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl NostrClient {&lt;br/&gt;    /// Creates a new [`NostrClient`] with the given client and options.&lt;br/&gt;    const fn new(client: Client) -&amp;gt; Self {&lt;br/&gt;        Self { client }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Initializes a new [`NostrClient`] instance and connects to the specified&lt;br/&gt;    /// relays.&lt;br/&gt;    pub async fn init(options: &amp;amp;CliOptions, relays: &amp;amp;[RelayUrl]) -&amp;gt; Self {&lt;br/&gt;        let mut client_builder =&lt;br/&gt;            Client::builder().opts(ClientOptions::new().verify_subscriptions(true));&lt;br/&gt;&lt;br/&gt;        if let Ok(Some(signer)) = options.signer().await {&lt;br/&gt;            client_builder = client_builder.signer(signer);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let client = Self::new(client_builder.build());&lt;br/&gt;&lt;br/&gt;        client.add_relays(relays).await;&lt;br/&gt;        client&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    //// Returns the users public key&lt;br/&gt;    pub async fn pubkey(&amp;amp;self) -&amp;gt; N34Result&amp;lt;PublicKey&amp;gt; {&lt;br/&gt;        self.client&lt;br/&gt;            .signer()&lt;br/&gt;            .await?&lt;br/&gt;            .get_public_key()&lt;br/&gt;            .await&lt;br/&gt;            .map_err(N34Error::SignerError)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Add relays and connect to them&lt;br/&gt;    pub async fn add_relays(&amp;amp;self, relays: &amp;amp;[RelayUrl]) {&lt;br/&gt;        if relays.is_empty() {&lt;br/&gt;            return;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let mut tasks = Vec::new();&lt;br/&gt;        for relay in relays {&lt;br/&gt;            let relay = relay.clone();&lt;br/&gt;            let client = self.client.clone();&lt;br/&gt;            tasks.push(tokio::spawn(async move {&lt;br/&gt;                client&lt;br/&gt;                    .add_relay(&amp;amp;relay)&lt;br/&gt;                    .await&lt;br/&gt;                    .expect(&amp;#34;It&amp;#39;s a valid relay url&amp;#34;);&lt;br/&gt;                if let Err(err) = client.try_connect_relay(&amp;amp;relay, CLIENT_TIMEOUT).await {&lt;br/&gt;                    tracing::error!(&amp;#34;Failed to connect to relay &amp;#39;{relay}&amp;#39;: {err}&amp;#34;);&lt;br/&gt;                }&lt;br/&gt;            }));&lt;br/&gt;        }&lt;br/&gt;        future::join_all(tasks).await;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Add a relay hint and connect to it&lt;br/&gt;    pub async fn add_relay_hint(&amp;amp;self, hint: Option&amp;lt;RelayUrl&amp;gt;) {&lt;br/&gt;        if let Some(relay) = hint {&lt;br/&gt;            self.add_relays(&amp;amp;[relay]).await&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// broadcast an event to the given relays&lt;br/&gt;    pub async fn broadcast(&amp;amp;self, event: &amp;amp;Event, relays: &amp;amp;[RelayUrl]) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        self.client.send_event_to(relays, event).await?;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Broadcasts an unsigned event to given relays, optionally broadcast the&lt;br/&gt;    /// relays list event. Returns URLs of relays that successfully received&lt;br/&gt;    /// the event.&lt;br/&gt;    pub async fn send_event_to(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        mut event: UnsignedEvent,&lt;br/&gt;        relays_list: Option&amp;lt;&amp;amp;Event&amp;gt;,&lt;br/&gt;        relays: &amp;amp;[RelayUrl],&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;RelayUrl&amp;gt;&amp;gt; {&lt;br/&gt;        self.add_relays(relays).await;&lt;br/&gt;        let event_id = event.id();&lt;br/&gt;&lt;br/&gt;        let (result, ..) = futures::join!(&lt;br/&gt;            async {&lt;br/&gt;                N34Result::Ok(&lt;br/&gt;                    self.client&lt;br/&gt;                        .send_event_to(relays, &amp;amp;event.sign(&amp;amp;self.client.signer().await?).await?)&lt;br/&gt;                        .await?,&lt;br/&gt;                )&lt;br/&gt;            },&lt;br/&gt;            async {&lt;br/&gt;                if let Some(event) = relays_list {&lt;br/&gt;                    let _ = self.client.send_event_to(relays, event).await;&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;        );&lt;br/&gt;        let result = result?;&lt;br/&gt;&lt;br/&gt;        for relay in &amp;amp;result.success {&lt;br/&gt;            tracing::info!(event_id = %event_id, relay = %relay, &amp;#34;Event sent successfully&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;        for (relay, reason) in &amp;amp;result.failed {&lt;br/&gt;            tracing::warn!(event_id = %event_id, relay = %relay, reason = %reason, &amp;#34;Failed to send event&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(result.success.into_iter().collect())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Fetches the first event matching the given filter, or None if no event&lt;br/&gt;    /// is found.&lt;br/&gt;    pub async fn fetch_event(&amp;amp;self, filter: Filter) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        Ok(self&lt;br/&gt;            .client&lt;br/&gt;            .fetch_events(filter.limit(1), CLIENT_TIMEOUT)&lt;br/&gt;            .await?&lt;br/&gt;            .first_owned())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Fetches the events matching the given filter&lt;br/&gt;    pub async fn fetch_events(&amp;amp;self, filter: Filter) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        // Multiply timeout by 5 to account for multiple events being fetched&lt;br/&gt;        Ok(self&lt;br/&gt;            .client&lt;br/&gt;            .fetch_events(filter, CLIENT_TIMEOUT * 5)&lt;br/&gt;            .await?&lt;br/&gt;            .to_vec())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Try to fetch the repositories and returns them&lt;br/&gt;    pub async fn fetch_repos(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        repo_naddrs: &amp;amp;[Coordinate],&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;GitRepositoryAnnouncement&amp;gt;&amp;gt; {&lt;br/&gt;        future::join_all(repo_naddrs.iter().map(|c| {&lt;br/&gt;            async {&lt;br/&gt;                self.fetch_event(&lt;br/&gt;                    Filter::new()&lt;br/&gt;                        .author(c.public_key)&lt;br/&gt;                        .identifier(&amp;amp;c.identifier)&lt;br/&gt;                        .kind(Kind::GitRepoAnnouncement),&lt;br/&gt;                )&lt;br/&gt;                .await?&lt;br/&gt;                .map(|e| utils::event_into_repo(e, &amp;amp;c.identifier))&lt;br/&gt;                .ok_or(N34Error::NotFoundRepo)&lt;br/&gt;            }&lt;br/&gt;        }))&lt;br/&gt;        .await&lt;br/&gt;        .into_iter()&lt;br/&gt;        .collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Fetch the patch by the given id. None if not found&lt;br/&gt;    pub async fn fetch_patch(&amp;amp;self, patch_id: EventId) -&amp;gt; N34Result&amp;lt;Event&amp;gt; {&lt;br/&gt;        self.fetch_event(Filter::new().id(patch_id).kind(Kind::GitPatch))&lt;br/&gt;            .await?&lt;br/&gt;            .ok_or(N34Error::CanNotFoundPatch)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the username for a given public key. If no username is found,&lt;br/&gt;    /// falls back to a shortened version of the public key.&lt;br/&gt;    pub async fn get_username(&amp;amp;self, user: PublicKey) -&amp;gt; String {&lt;br/&gt;        self.fetch_event(Filter::new().kind(Kind::Metadata).author(user))&lt;br/&gt;            .await&lt;br/&gt;            .ok()&lt;br/&gt;            .flatten()&lt;br/&gt;            .and_then(|e| Metadata::try_from(&amp;amp;e).ok())&lt;br/&gt;            .and_then(|m| m.display_name.or(m.name))&lt;br/&gt;            .unwrap_or_else(|| {&lt;br/&gt;                let pubkey = user.to_bech32().expect(&amp;#34;The error is `Infallible`&amp;#34;);&lt;br/&gt;                format!(&amp;#34;{}...{}&amp;#34;, &amp;amp;pubkey[..8], &amp;amp;pubkey[NPUB_LEN - 8..])&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Get the latest status of an issue by its ID, only considering status&lt;br/&gt;    /// events from authorized_pubkeys. If no valid status event is found,&lt;br/&gt;    /// defaults to Open.&lt;br/&gt;    pub async fn fetch_issue_status(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        issue_id: EventId,&lt;br/&gt;        authorized_pubkeys: Vec&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;IssueStatus&amp;gt; {&lt;br/&gt;        self.fetch_events(&lt;br/&gt;            Filter::new()&lt;br/&gt;                .event(issue_id)&lt;br/&gt;                .kinds([&lt;br/&gt;                    Kind::GitStatusOpen,&lt;br/&gt;                    Kind::GitStatusApplied,&lt;br/&gt;                    Kind::GitStatusClosed,&lt;br/&gt;                ])&lt;br/&gt;                .authors(utils::dedup(authorized_pubkeys.into_iter())),&lt;br/&gt;        )&lt;br/&gt;        .await?&lt;br/&gt;        .into_iter()&lt;br/&gt;        .max_by_key(|e| e.created_at)&lt;br/&gt;        .map(|status| IssueStatus::try_from(status.kind))&lt;br/&gt;        .unwrap_or_else(|| Ok(IssueStatus::Open))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the status of a patch. If it&amp;#39;s a revision patch, checks if it&amp;#39;s&lt;br/&gt;    /// closed when the root patch is already merged/applied but doesn&amp;#39;t&lt;br/&gt;    /// reference this revision. Defaults to Open status if no status event&lt;br/&gt;    /// is found.&lt;br/&gt;    pub async fn fetch_patch_status(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        root_patch: EventId,&lt;br/&gt;        root_revision: Option&amp;lt;EventId&amp;gt;,&lt;br/&gt;        authorized_pubkeys: Vec&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;PatchStatus&amp;gt; {&lt;br/&gt;        let (root_status, event_tags) = self&lt;br/&gt;            .fetch_events(&lt;br/&gt;                Filter::new()&lt;br/&gt;                    .event(root_patch)&lt;br/&gt;                    .kinds([&lt;br/&gt;                        Kind::GitStatusOpen,&lt;br/&gt;                        Kind::GitStatusApplied,&lt;br/&gt;                        Kind::GitStatusClosed,&lt;br/&gt;                        Kind::GitStatusDraft,&lt;br/&gt;                    ])&lt;br/&gt;                    .authors(utils::dedup(authorized_pubkeys.into_iter())),&lt;br/&gt;            )&lt;br/&gt;            .await?&lt;br/&gt;            .into_iter()&lt;br/&gt;            .max_by_key(|e| e.created_at)&lt;br/&gt;            .map(|status| N34Result::Ok((PatchStatus::try_from(status.kind)?, status.tags)))&lt;br/&gt;            .unwrap_or_else(|| Ok((PatchStatus::Open, Tags::new())))?;&lt;br/&gt;&lt;br/&gt;        if let Some(revision_id) = root_revision&lt;br/&gt;            &amp;amp;&amp;amp; root_status.is_merged_or_applied()&lt;br/&gt;            &amp;amp;&amp;amp; !event_tags&lt;br/&gt;                .filter(TagKind::e())&lt;br/&gt;                .any(|t| t.is_reply() &amp;amp;&amp;amp; t.content().is_some_and(|c| c == revision_id.to_hex()))&lt;br/&gt;        {&lt;br/&gt;            return Ok(PatchStatus::Closed);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(root_status)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    pub async fn fetch_patch_series(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        root_patch_id: EventId,&lt;br/&gt;        root_patch_author: PublicKey,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        Ok(self&lt;br/&gt;            .fetch_events(&lt;br/&gt;                Filter::new()&lt;br/&gt;                    .kind(Kind::GitPatch)&lt;br/&gt;                    .author(root_patch_author)&lt;br/&gt;                    .event(root_patch_id),&lt;br/&gt;            )&lt;br/&gt;            .await?&lt;br/&gt;            .into_iter()&lt;br/&gt;            .filter(|e| {&lt;br/&gt;                e.tags.iter().any(|t| {&lt;br/&gt;                    t.is_root() &amp;amp;&amp;amp; t.content().is_some_and(|c| c == root_patch_id.to_hex())&lt;br/&gt;                })&lt;br/&gt;            })&lt;br/&gt;            .collect())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finds the root issue or patch for a given event. If the event is already&lt;br/&gt;    /// a root (issue/patch), returns it directly. For comments, follows&lt;br/&gt;    /// parent/root references until finding the root or failing. Returns&lt;br/&gt;    /// None if no root can be found.&lt;br/&gt;    pub async fn find_root(&amp;amp;self, mut event: Event) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        if !matches!(event.kind, Kind::GitIssue | Kind::GitPatch | Kind::Comment) {&lt;br/&gt;            return Err(N34Error::CanNotReplyToEvent);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        loop {&lt;br/&gt;            if matches!(event.kind, Kind::GitIssue | Kind::GitPatch) {&lt;br/&gt;                return Ok(Some(event));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            if let Some(nip22::CommentTarget::Event { id, relay_hint, .. }) =&lt;br/&gt;                nip22::extract_root(&amp;amp;event)&lt;br/&gt;            {&lt;br/&gt;                self.add_relay_hint(relay_hint.cloned()).await;&lt;br/&gt;                let root_event = self.fetch_event(Filter::new().id(*id)).await?;&lt;br/&gt;                if let Some(ref root_event) = root_event&lt;br/&gt;                    &amp;amp;&amp;amp; !matches!(root_event.kind, Kind::GitIssue | Kind::GitPatch)&lt;br/&gt;                {&lt;br/&gt;                    return Err(N34Error::CanNotReplyToEvent);&lt;br/&gt;                }&lt;br/&gt;                return Ok(root_event);&lt;br/&gt;            } else if let Some(nip22::CommentTarget::Event { id, relay_hint, .. }) =&lt;br/&gt;                nip22::extract_parent(&amp;amp;event)&lt;br/&gt;            {&lt;br/&gt;                self.add_relay_hint(relay_hint.cloned()).await;&lt;br/&gt;                if let Ok(Some(parent_event)) = self.fetch_event(Filter::new().id(*id)).await {&lt;br/&gt;                    event = parent_event;&lt;br/&gt;                    continue;&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            // Break if: no root/parent tags found, parent/root event fetch failed&lt;br/&gt;            break;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(None)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Fetches the relay list (kind 10002) for the given user. Returns None if&lt;br/&gt;    /// no relays are found.&lt;br/&gt;    pub async fn user_relays_list(&amp;amp;self, user: PublicKey) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        self.fetch_event(Filter::new().author(user).kind(Kind::RelayList))&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the author of the specified event, if found.&lt;br/&gt;    pub async fn event_author(&amp;amp;self, event_id: EventId) -&amp;gt; N34Result&amp;lt;Option&amp;lt;PublicKey&amp;gt;&amp;gt; {&lt;br/&gt;        Ok(self&lt;br/&gt;            .fetch_event(Filter::new().id(event_id))&lt;br/&gt;            .await?&lt;br/&gt;            .map(|e| e.pubkey))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the read relays of the given user if found, otherwise empty&lt;br/&gt;    /// vector&lt;br/&gt;    pub async fn read_relays_from_user(&amp;amp;self, user: PublicKey) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;        utils::add_read_relays(self.user_relays_list(user).await.ok().flatten().as_ref())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the read relays of the given users if found, otherwise empty&lt;br/&gt;    /// vector&lt;br/&gt;    pub async fn read_relays_from_users(&amp;amp;self, users: &amp;amp;[PublicKey]) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;        self.fetch_events(&lt;br/&gt;            Filter::new()&lt;br/&gt;                .kind(nostr::event::Kind::RelayList)&lt;br/&gt;                .authors(utils::dedup(users.iter().copied())),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .unwrap_or_default()&lt;br/&gt;        .into_iter()&lt;br/&gt;        .flat_map(|e| utils::add_read_relays(Some(&amp;amp;e)))&lt;br/&gt;        .collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Parse the given content and returns the details that inside it&lt;br/&gt;    pub async fn parse_content(&amp;amp;self, content: &amp;amp;str) -&amp;gt; ContentDetails {&lt;br/&gt;        let mut write_relays = Vec::new();&lt;br/&gt;        let tokens = NostrParser::new().parse(content).collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;&lt;br/&gt;        let mut p_tagged_users = tokens&lt;br/&gt;            .iter()&lt;br/&gt;            .filter_map(TokenUtils::extract_public_key)&lt;br/&gt;            .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;        let quotes = tokens&lt;br/&gt;            .iter()&lt;br/&gt;            .filter_map(TokenUtils::extract_event_id)&lt;br/&gt;            .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;        let hashtags = tokens&lt;br/&gt;            .iter()&lt;br/&gt;            .filter_map(TokenUtils::extract_hashtag)&lt;br/&gt;            .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;&lt;br/&gt;        for (user, relays) in &amp;amp;p_tagged_users {&lt;br/&gt;            self.add_relays(relays).await;&lt;br/&gt;            write_relays.extend(self.read_relays_from_user(*user).await);&lt;br/&gt;        }&lt;br/&gt;        for (event_id, relays) in &amp;amp;quotes {&lt;br/&gt;            self.add_relays(relays).await;&lt;br/&gt;            // Add the event author to the p-tagged users&lt;br/&gt;            if let Ok(Some(author)) = self.event_author(*event_id).await {&lt;br/&gt;                p_tagged_users.push((author, Vec::new()));&lt;br/&gt;                write_relays.extend(self.read_relays_from_user(author).await);&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        ContentDetails::new(&lt;br/&gt;            p_tagged_users.into_iter().map(|(p, _)| p),&lt;br/&gt;            quotes.into_iter().map(|(e, r)| (e, r.first().cloned())),&lt;br/&gt;            hashtags,&lt;br/&gt;            write_relays,&lt;br/&gt;        )&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:52:21Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsvcpus0yls5645pge4jw7a4f626ky88u7kar089jr2l6k8lp4u6gczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvmp5fua</id>
    
      <title type="html">Build manifest for get_file_hash v0.4.7</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsvcpus0yls5645pge4jw7a4f626ky88u7kar089jr2l6k8lp4u6gczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvmp5fua" />
    <content type="html">
      In reply to &lt;a href=&#39;/nevent1qqsxu84pzt94r22pgqeplxdprhpegenc47gztq5ke724ey4vxapzfsge6rewe&#39;&gt;nevent1q…rewe&lt;/a&gt;&lt;br/&gt;_________________________&lt;br/&gt;&lt;br/&gt;Build manifest for get_file_hash v0.4.7
    </content>
    <updated>2026-04-05T05:52:07Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs2clcekrq6ga6adxqymqn2guprldrlh4nf384kc95fjt5kyrm9qfszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvpms45n</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs2clcekrq6ga6adxqymqn2guprldrlh4nf384kc95fjt5kyrm9qfszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvpms45n" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Command line interface module&lt;br/&gt;pub mod cli;&lt;br/&gt;/// N34 errors&lt;br/&gt;pub mod error;&lt;br/&gt;/// Nostr utils module&lt;br/&gt;pub mod nostr_utils;&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    process::ExitCode,&lt;br/&gt;    sync::atomic::{AtomicBool, Ordering},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Parser;&lt;br/&gt;use clap_verbosity_flag::Verbosity;&lt;br/&gt;use tracing::Level;&lt;br/&gt;use tracing_subscriber::{Layer, filter, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;use self::cli::Cli;&lt;br/&gt;&lt;br/&gt;/// Whether the editor is currently open. Prevents logging while the editor is&lt;br/&gt;/// open.&lt;br/&gt;static EDITOR_OPEN: AtomicBool = AtomicBool::new(false);&lt;br/&gt;&lt;br/&gt;/// Configures the logging level based on the provided verbosity.&lt;br/&gt;///&lt;br/&gt;/// When verbosity is set to TRACE, includes file and line numbers in logs.&lt;br/&gt;fn set_log_level(verbosity: Verbosity) {&lt;br/&gt;    let is_trace = verbosity&lt;br/&gt;        .tracing_level()&lt;br/&gt;        .is_some_and(|l| l == tracing::Level::TRACE);&lt;br/&gt;&lt;br/&gt;    let logs_filter = filter::dynamic_filter_fn(move |m, _| {&lt;br/&gt;        // Disable all logs while editor is open&lt;br/&gt;        verbosity.tracing_level().unwrap_or(Level::ERROR) &amp;gt;= *m.level()&lt;br/&gt;            &amp;amp;&amp;amp; !EDITOR_OPEN.load(Ordering::Relaxed)&lt;br/&gt;    });&lt;br/&gt;&lt;br/&gt;    let logs_layer = tracing_subscriber::fmt::layer()&lt;br/&gt;        .with_file(is_trace)&lt;br/&gt;        .with_line_number(is_trace)&lt;br/&gt;        .without_time();&lt;br/&gt;    let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter));&lt;br/&gt;    tracing::subscriber::set_global_default(subscriber).ok();&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() -&amp;gt; ExitCode {&lt;br/&gt;    let cli = match cli::post_cli(Cli::parse()) {&lt;br/&gt;        Ok(cli) =&amp;gt; cli,&lt;br/&gt;        Err(err) =&amp;gt; {&lt;br/&gt;            eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;            return ExitCode::FAILURE;&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    set_log_level(cli.verbosity);&lt;br/&gt;&lt;br/&gt;    if let Err(err) = cli.run().await {&lt;br/&gt;        tracing::error!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;        return err.exit_code();&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    ExitCode::SUCCESS&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:52:05Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsxu84pzt94r22pgqeplxdprhpegenc47gztq5ke724ey4vxapzfsgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvr8m8tg</id>
    
      <title type="html">&amp;lt;?xml version=&amp;#39;1.0&amp;#39; ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsxu84pzt94r22pgqeplxdprhpegenc47gztq5ke724ey4vxapzfsgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvr8m8tg" />
    <content type="html">
      &amp;lt;?xml version=&amp;#39;1.0&amp;#39; encoding=&amp;#39;windows-1252&amp;#39;?&amp;gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  Copyright (C) 2017 Christopher R. Field.&lt;br/&gt;&lt;br/&gt;  Licensed under the Apache License, Version 2.0 (the &amp;#34;License&amp;#34;);&lt;br/&gt;  you may not use this file except in compliance with the License.&lt;br/&gt;  You may obtain a copy of the License at&lt;br/&gt;&lt;br/&gt;  &lt;a href=&#34;http://www.apache.org/licenses/LICENSE-2.0&#34;&gt;http://www.apache.org/licenses/LICENSE-2.0&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;  Unless required by applicable law or agreed to in writing, software&lt;br/&gt;  distributed under the License is distributed on an &amp;#34;AS IS&amp;#34; BASIS,&lt;br/&gt;  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&lt;br/&gt;  See the License for the specific language governing permissions and&lt;br/&gt;  limitations under the License.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  The &amp;#34;cargo wix&amp;#34; subcommand provides a variety of predefined variables available&lt;br/&gt;  for customization of this template. The values for each variable are set at&lt;br/&gt;  installer creation time. The following variables are available:&lt;br/&gt;&lt;br/&gt;  TargetTriple      = The rustc target triple name.&lt;br/&gt;  TargetEnv         = The rustc target environment. This is typically either&lt;br/&gt;                      &amp;#34;msvc&amp;#34; or &amp;#34;gnu&amp;#34; depending on the toolchain downloaded and&lt;br/&gt;                      installed.&lt;br/&gt;  TargetVendor      = The rustc target vendor. This is typically &amp;#34;pc&amp;#34;, but Rust&lt;br/&gt;                      does support other vendors, like &amp;#34;uwp&amp;#34;.&lt;br/&gt;  CargoTargetBinDir = The complete path to the directory containing the&lt;br/&gt;                      binaries (exes) to include. The default would be&lt;br/&gt;                      &amp;#34;target\release\&amp;#34;. If an explicit rustc target triple is&lt;br/&gt;                      used, i.e. cross-compiling, then the default path would&lt;br/&gt;                      be &amp;#34;target\&amp;lt;CARGO_TARGET&amp;gt;\&amp;lt;CARGO_PROFILE&amp;gt;&amp;#34;,&lt;br/&gt;                      where &amp;#34;&amp;lt;CARGO_TARGET&amp;gt;&amp;#34; is replaced with the &amp;#34;CargoTarget&amp;#34;&lt;br/&gt;                      variable value and &amp;#34;&amp;lt;CARGO_PROFILE&amp;gt;&amp;#34; is replaced with the&lt;br/&gt;                      value from the &amp;#34;CargoProfile&amp;#34; variable. This can also&lt;br/&gt;                      be overridden manually with the &amp;#34;target-bin-dir&amp;#34; flag.&lt;br/&gt;  CargoTargetDir    = The path to the directory for the build artifacts, i.e.&lt;br/&gt;                      &amp;#34;target&amp;#34;.&lt;br/&gt;  CargoProfile      = The cargo profile used to build the binaries&lt;br/&gt;                      (usually &amp;#34;debug&amp;#34; or &amp;#34;release&amp;#34;).&lt;br/&gt;  Version           = The version for the installer. The default is the&lt;br/&gt;                      &amp;#34;Major.Minor.Fix&amp;#34; semantic versioning number of the Rust&lt;br/&gt;                      package.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  Please do not remove these pre-processor If-Else blocks. These are used with&lt;br/&gt;  the `cargo wix` subcommand to automatically determine the installation&lt;br/&gt;  destination for 32-bit versus 64-bit installers. Removal of these lines will&lt;br/&gt;  cause installation errors.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&amp;lt;?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?&amp;gt;&lt;br/&gt;    &amp;lt;?define PlatformProgramFilesFolder = &amp;#34;ProgramFiles64Folder&amp;#34; ?&amp;gt;&lt;br/&gt;&amp;lt;?else ?&amp;gt;&lt;br/&gt;    &amp;lt;?define PlatformProgramFilesFolder = &amp;#34;ProgramFilesFolder&amp;#34; ?&amp;gt;&lt;br/&gt;&amp;lt;?endif ?&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;Wix xmlns=&amp;#39;&lt;a href=&#34;http://schemas.microsoft.com/wix/2006/wi&amp;#39;&amp;gt&#34;&gt;http://schemas.microsoft.com/wix/2006/wi&amp;#39;&amp;gt&lt;/a&gt;;&lt;br/&gt;&lt;br/&gt;    &amp;lt;Product&lt;br/&gt;        Id=&amp;#39;*&amp;#39;&lt;br/&gt;        Name=&amp;#39;get_file_hash&amp;#39;&lt;br/&gt;        UpgradeCode=&amp;#39;DED69220-26E3-4406-B564-7F2B58C56F57&amp;#39;&lt;br/&gt;        Manufacturer=&amp;#39;gnostr admin@gnostr.org&amp;#39;&lt;br/&gt;        Language=&amp;#39;1033&amp;#39;&lt;br/&gt;        Codepage=&amp;#39;1252&amp;#39;&lt;br/&gt;        Version=&amp;#39;$(var.Version)&amp;#39;&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Package Id=&amp;#39;*&amp;#39;&lt;br/&gt;            Keywords=&amp;#39;Installer&amp;#39;&lt;br/&gt;            Description=&amp;#39;A utility crate providing a procedural macro to compute and embed file hashes at compile time.&amp;#39;&lt;br/&gt;            Manufacturer=&amp;#39;gnostr admin@gnostr.org&amp;#39;&lt;br/&gt;            InstallerVersion=&amp;#39;450&amp;#39;&lt;br/&gt;            Languages=&amp;#39;1033&amp;#39;&lt;br/&gt;            Compressed=&amp;#39;yes&amp;#39;&lt;br/&gt;            InstallScope=&amp;#39;perMachine&amp;#39;&lt;br/&gt;            SummaryCodepage=&amp;#39;1252&amp;#39;&lt;br/&gt;            /&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;MajorUpgrade&lt;br/&gt;            Schedule=&amp;#39;afterInstallInitialize&amp;#39;&lt;br/&gt;            DowngradeErrorMessage=&amp;#39;A newer version of [ProductName] is already installed. Setup will now exit.&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Media Id=&amp;#39;1&amp;#39; Cabinet=&amp;#39;media1.cab&amp;#39; EmbedCab=&amp;#39;yes&amp;#39; DiskPrompt=&amp;#39;CD-ROM #1&amp;#39;/&amp;gt;&lt;br/&gt;        &amp;lt;Property Id=&amp;#39;DiskPrompt&amp;#39; Value=&amp;#39;get_file_hash Installation&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Directory Id=&amp;#39;TARGETDIR&amp;#39; Name=&amp;#39;SourceDir&amp;#39;&amp;gt;&lt;br/&gt;            &amp;lt;Directory Id=&amp;#39;$(var.PlatformProgramFilesFolder)&amp;#39; Name=&amp;#39;PFiles&amp;#39;&amp;gt;&lt;br/&gt;                &amp;lt;Directory Id=&amp;#39;APPLICATIONFOLDER&amp;#39; Name=&amp;#39;get_file_hash&amp;#39;&amp;gt;&lt;br/&gt;                    &lt;br/&gt;                    &amp;lt;!--&lt;br/&gt;                      Enabling the license sidecar file in the installer is a four step process:&lt;br/&gt;&lt;br/&gt;                      1. Uncomment the `Component` tag and its contents.&lt;br/&gt;                      2. Change the value for the `Source` attribute in the `File` tag to a path&lt;br/&gt;                         to the file that should be included as the license sidecar file. The path&lt;br/&gt;                         can, and probably should be, relative to this file.&lt;br/&gt;                      3. Change the value for the `Name` attribute in the `File` tag to the&lt;br/&gt;                         desired name for the file when it is installed alongside the `bin` folder&lt;br/&gt;                         in the installation directory. This can be omitted if the desired name is&lt;br/&gt;                         the same as the file name.&lt;br/&gt;                      4. Uncomment the `ComponentRef` tag with the Id attribute value of &amp;#34;License&amp;#34;&lt;br/&gt;                         further down in this file.&lt;br/&gt;                    --&amp;gt;&lt;br/&gt;                    &amp;lt;!--&lt;br/&gt;                    &amp;lt;Component Id=&amp;#39;License&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                        &amp;lt;File Id=&amp;#39;LicenseFile&amp;#39; Name=&amp;#39;ChangeMe&amp;#39; DiskId=&amp;#39;1&amp;#39; Source=&amp;#39;C:\Path\To\File&amp;#39; KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                    &amp;lt;/Component&amp;gt;&lt;br/&gt;                    --&amp;gt;&lt;br/&gt;&lt;br/&gt;                    &amp;lt;Directory Id=&amp;#39;Bin&amp;#39; Name=&amp;#39;bin&amp;#39;&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;Path&amp;#39; Guid=&amp;#39;8DB39A25-8B99-4C25-8CF5-4704353C7C6E&amp;#39; KeyPath=&amp;#39;yes&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;Environment&lt;br/&gt;                                Id=&amp;#39;PATH&amp;#39;&lt;br/&gt;                                Name=&amp;#39;PATH&amp;#39;&lt;br/&gt;                                Value=&amp;#39;[Bin]&amp;#39;&lt;br/&gt;                                Permanent=&amp;#39;no&amp;#39;&lt;br/&gt;                                Part=&amp;#39;last&amp;#39;&lt;br/&gt;                                Action=&amp;#39;set&amp;#39;&lt;br/&gt;                                System=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary0&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe0&amp;#39;&lt;br/&gt;                                Name=&amp;#39;get_file_hash.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\get_file_hash.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary1&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe1&amp;#39;&lt;br/&gt;                                Name=&amp;#39;n34.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\n34.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary2&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe2&amp;#39;&lt;br/&gt;                                Name=&amp;#39;n34-relay.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\n34-relay.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary3&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe3&amp;#39;&lt;br/&gt;                                Name=&amp;#39;readme.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\readme.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                    &amp;lt;/Directory&amp;gt;&lt;br/&gt;                &amp;lt;/Directory&amp;gt;&lt;br/&gt;            &amp;lt;/Directory&amp;gt;&lt;br/&gt;        &amp;lt;/Directory&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Feature&lt;br/&gt;            Id=&amp;#39;Binaries&amp;#39;&lt;br/&gt;            Title=&amp;#39;Application&amp;#39;&lt;br/&gt;            Description=&amp;#39;Installs all binaries and the license.&amp;#39;&lt;br/&gt;            Level=&amp;#39;1&amp;#39;&lt;br/&gt;            ConfigurableDirectory=&amp;#39;APPLICATIONFOLDER&amp;#39;&lt;br/&gt;            AllowAdvertise=&amp;#39;no&amp;#39;&lt;br/&gt;            Display=&amp;#39;expand&amp;#39;&lt;br/&gt;            Absent=&amp;#39;disallow&amp;#39;&amp;gt;&lt;br/&gt;            &lt;br/&gt;            &amp;lt;!--&lt;br/&gt;              Uncomment the following `ComponentRef` tag to add the license&lt;br/&gt;              sidecar file to the installer.&lt;br/&gt;            --&amp;gt;&lt;br/&gt;            &amp;lt;!--&amp;lt;ComponentRef Id=&amp;#39;License&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary0&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary1&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary2&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary3&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;            &amp;lt;Feature&lt;br/&gt;                Id=&amp;#39;Environment&amp;#39;&lt;br/&gt;                Title=&amp;#39;PATH Environment Variable&amp;#39;&lt;br/&gt;                Description=&amp;#39;Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.&amp;#39;&lt;br/&gt;                Level=&amp;#39;1&amp;#39;&lt;br/&gt;                Absent=&amp;#39;allow&amp;#39;&amp;gt;&lt;br/&gt;                &amp;lt;ComponentRef Id=&amp;#39;Path&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;/Feature&amp;gt;&lt;br/&gt;        &amp;lt;/Feature&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;SetProperty Id=&amp;#39;ARPINSTALLLOCATION&amp;#39; Value=&amp;#39;[APPLICATIONFOLDER]&amp;#39; After=&amp;#39;CostFinalize&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the following `Icon` and `Property` tags to change the product icon.&lt;br/&gt;&lt;br/&gt;          The product icon is the graphic that appears in the Add/Remove&lt;br/&gt;          Programs control panel for the application.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;Icon Id=&amp;#39;ProductICO&amp;#39; SourceFile=&amp;#39;wix\Product.ico&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;Property Id=&amp;#39;ARPPRODUCTICON&amp;#39; Value=&amp;#39;ProductICO&amp;#39; /&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Property Id=&amp;#39;ARPHELPLINK&amp;#39; Value=&amp;#39;&lt;a href=&#34;https://github.com/gnostr-org/get_file_hash&amp;#39;/&amp;gt&#34;&gt;https://github.com/gnostr-org/get_file_hash&amp;#39;/&amp;gt&lt;/a&gt;;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;UI&amp;gt;&lt;br/&gt;            &amp;lt;UIRef Id=&amp;#39;WixUI_FeatureTree&amp;#39;/&amp;gt;&lt;br/&gt;            &lt;br/&gt;            &amp;lt;!--&lt;br/&gt;              Enabling the EULA dialog in the installer is a three step process:&lt;br/&gt;&lt;br/&gt;                1. Comment out or remove the two `Publish` tags that follow the&lt;br/&gt;                   `WixVariable` tag.&lt;br/&gt;                2. Uncomment the `&amp;lt;WixVariable Id=&amp;#39;WixUILicenseRtf&amp;#39; Value=&amp;#39;Path\to\Eula.rft&amp;#39;&amp;gt;` tag further down&lt;br/&gt;                3. Replace the `Value` attribute of the `WixVariable` tag with&lt;br/&gt;                   the path to a RTF file that will be used as the EULA and&lt;br/&gt;                   displayed in the license agreement dialog.&lt;br/&gt;            --&amp;gt;&lt;br/&gt;            &amp;lt;Publish Dialog=&amp;#39;WelcomeDlg&amp;#39; Control=&amp;#39;Next&amp;#39; Event=&amp;#39;NewDialog&amp;#39; Value=&amp;#39;CustomizeDlg&amp;#39; Order=&amp;#39;99&amp;#39;&amp;gt;1&amp;lt;/Publish&amp;gt;&lt;br/&gt;            &amp;lt;Publish Dialog=&amp;#39;CustomizeDlg&amp;#39; Control=&amp;#39;Back&amp;#39; Event=&amp;#39;NewDialog&amp;#39; Value=&amp;#39;WelcomeDlg&amp;#39; Order=&amp;#39;99&amp;#39;&amp;gt;1&amp;lt;/Publish&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;/UI&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Enabling the EULA dialog in the installer requires uncommenting&lt;br/&gt;          the following `WixUILicenseRTF` tag and changing the `Value`&lt;br/&gt;          attribute.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!-- &amp;lt;WixVariable Id=&amp;#39;WixUILicenseRtf&amp;#39; Value=&amp;#39;Relative\Path\to\Eula.rtf&amp;#39;/&amp;gt; --&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the next `WixVariable` tag to customize the installer&amp;#39;s&lt;br/&gt;          Graphical User Interface (GUI) and add a custom banner image across&lt;br/&gt;          the top of each screen. See the WiX Toolset documentation for details&lt;br/&gt;          about customization.&lt;br/&gt;&lt;br/&gt;          The banner BMP dimensions are 493 x 58 pixels.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;WixVariable Id=&amp;#39;WixUIBannerBmp&amp;#39; Value=&amp;#39;wix\Banner.bmp&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the next `WixVariable` tag to customize the installer&amp;#39;s&lt;br/&gt;          Graphical User Interface (GUI) and add a custom image to the first&lt;br/&gt;          dialog, or screen. See the WiX Toolset documentation for details about&lt;br/&gt;          customization.&lt;br/&gt;&lt;br/&gt;          The dialog BMP dimensions are 493 x 312 pixels.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;WixVariable Id=&amp;#39;WixUIDialogBmp&amp;#39; Value=&amp;#39;wix\Dialog.bmp&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;/Product&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;/Wix&amp;gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:51:57Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsz4lapak50pn5ljdf4rdts8ttqh8hh0l0ng8qwwjsneqhcgg6tzpszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvys004m</id>
    
      <title type="html">#![allow(unused)] // n34 - A CLI to interact with NIP-34 and ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsz4lapak50pn5ljdf4rdts8ttqh8hh0l0ng8qwwjsneqhcgg6tzpszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvys004m" />
    <content type="html">
      #![allow(unused)]&lt;br/&gt;// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Command line interface module&lt;br/&gt;pub mod cli;&lt;br/&gt;/// N34 errors&lt;br/&gt;pub mod error;&lt;br/&gt;/// Nostr utils module&lt;br/&gt;pub mod nostr_utils;&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    process::ExitCode,&lt;br/&gt;    sync::atomic::{AtomicBool, Ordering},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Parser;&lt;br/&gt;use clap_verbosity_flag::Verbosity;&lt;br/&gt;use tracing::Level;&lt;br/&gt;use tracing_subscriber::{Layer, filter, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;use self::cli::Cli;&lt;br/&gt;&lt;br/&gt;/// Whether the editor is currently open. Prevents logging while the editor is&lt;br/&gt;/// open.&lt;br/&gt;static EDITOR_OPEN: AtomicBool = AtomicBool::new(false);&lt;br/&gt;&lt;br/&gt;/// Configures the logging level based on the provided verbosity.&lt;br/&gt;///&lt;br/&gt;/// When verbosity is set to TRACE, includes file and line numbers in logs.&lt;br/&gt;fn set_log_level(verbosity: Verbosity) {&lt;br/&gt;    let is_trace = verbosity&lt;br/&gt;        .tracing_level()&lt;br/&gt;        .is_some_and(|l| l == tracing::Level::TRACE);&lt;br/&gt;&lt;br/&gt;    let logs_filter = filter::dynamic_filter_fn(move |m, _| {&lt;br/&gt;        // Disable all logs while editor is open&lt;br/&gt;        verbosity.tracing_level().unwrap_or(Level::ERROR) &amp;gt;= *m.level()&lt;br/&gt;            &amp;amp;&amp;amp; !EDITOR_OPEN.load(Ordering::Relaxed)&lt;br/&gt;    });&lt;br/&gt;&lt;br/&gt;    let logs_layer = tracing_subscriber::fmt::layer()&lt;br/&gt;        .with_file(is_trace)&lt;br/&gt;        .with_line_number(is_trace)&lt;br/&gt;        .without_time();&lt;br/&gt;    let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter));&lt;br/&gt;    tracing::subscriber::set_global_default(subscriber).ok();&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// ```rust&lt;br/&gt;// 	#[tokio::main]&lt;br/&gt;// 	async fn main() -&amp;gt; ExitCode {&lt;br/&gt;// 	    let cli = match cli::post_cli(Cli::parse()) {&lt;br/&gt;// 	        Ok(cli) =&amp;gt; cli,&lt;br/&gt;// 	        Err(err) =&amp;gt; {&lt;br/&gt;// 	            eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;// 	            return ExitCode::FAILURE;&lt;br/&gt;// 	        }&lt;br/&gt;// 	    };&lt;br/&gt;// 	&lt;br/&gt;// 	    set_log_level(cli.verbosity);&lt;br/&gt;// 	&lt;br/&gt;// 	    if let Err(err) = cli.run().await {&lt;br/&gt;// 	        tracing::error!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;// 	        return err.exit_code();&lt;br/&gt;// 	    }&lt;br/&gt;// 	&lt;br/&gt;// 	    ExitCode::SUCCESS&lt;br/&gt;// 	}&lt;br/&gt;// ```&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:51:47Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs0rhr07gtmzapd8nqzujndnqhthydgksmhpve7hhf2hhjlmxqd2vczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsva2uf84</id>
    
      <title type="html">use std::process::Command; use std::fs; use sha2::{Digest, ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs0rhr07gtmzapd8nqzujndnqhthydgksmhpve7hhf2hhjlmxqd2vczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsva2uf84" />
    <content type="html">
      use std::process::Command;&lt;br/&gt;use std::fs;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;fn calculate_sha256(file_path: &amp;amp;str) -&amp;gt; String {&lt;br/&gt;    let content = fs::read(file_path).expect(&amp;#34;Unable to read file&amp;#34;);&lt;br/&gt;    let mut hasher = Sha256::new();&lt;br/&gt;    hasher.update(&amp;amp;content);&lt;br/&gt;    hasher.finalize()&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|b| format!(&amp;#34;{:02x}&amp;#34;, b))&lt;br/&gt;        .collect()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn test_get_file_hash_binary_no_features() {&lt;br/&gt;    let output = Command::new(&amp;#34;cargo&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;run&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;--bin&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;get_file_hash&amp;#34;)&lt;br/&gt;        .output()&lt;br/&gt;        .expect(&amp;#34;Failed to execute command&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let stdout = String::from_utf8_lossy(&amp;amp;output.stdout);&lt;br/&gt;    let stderr = String::from_utf8_lossy(&amp;amp;output.stderr);&lt;br/&gt;&lt;br/&gt;    // Assert that the command ran successfully&lt;br/&gt;    assert!(output.status.success(), &amp;#34;Command failed with stderr: {}&amp;#34;, stderr);&lt;br/&gt;&lt;br/&gt;    // Manually calculate the hash of the binary&amp;#39;s source file&lt;br/&gt;    let expected_hash = calculate_sha256(&amp;#34;src/bin/get_file_hash.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // Assert that the output contains the correct hash&lt;br/&gt;    // Check for the raw hash first&lt;br/&gt;    assert!(stdout.contains(&amp;amp;expected_hash), &amp;#34;Output did not contain raw expected hash. Expected: {}, Actual: {}&amp;#34;, expected_hash, stdout);&lt;br/&gt;&lt;br/&gt;    // Then check for the formatted string, including backticks&lt;br/&gt;    // Use a regex-like check for more flexibility with newlines if needed, or refine to exact match&lt;br/&gt;    let expected_hash_line = format!(&amp;#34;*   **SHA-256 Hash:** `{}`&amp;#34;, expected_hash);&lt;br/&gt;    assert!(stdout.contains(&amp;amp;expected_hash_line), &amp;#34;Output did not contain expected hash line. Expected line: {}, Actual: {}&amp;#34;, expected_hash_line, stdout);&lt;br/&gt;&lt;br/&gt;    // Assert that the output contains &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    assert!(stdout.contains(&amp;#34;Integrity Verified.&amp;#34;), &amp;#34;Output did not contain &amp;#39;Integrity Verified.&amp;#39;. stdout: {}&amp;#34;, stdout);&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;Output from get_file_hash binary (no features):&lt;br/&gt;{}&amp;#34;, stdout);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn test_get_file_hash_binary_with_nostr_feature() {&lt;br/&gt;    let output = Command::new(&amp;#34;cargo&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;run&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;--bin&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;get_file_hash&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;--features&amp;#34;)&lt;br/&gt;        .arg(&amp;#34;nostr&amp;#34;)&lt;br/&gt;        .output()&lt;br/&gt;        .expect(&amp;#34;Failed to execute command&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let stdout = String::from_utf8_lossy(&amp;amp;output.stdout);&lt;br/&gt;    let stderr = String::from_utf8_lossy(&amp;amp;output.stderr);&lt;br/&gt;&lt;br/&gt;    // Assert that the command ran successfully&lt;br/&gt;    assert!(output.status.success(), &amp;#34;Command failed with stderr: {}&amp;#34;, stderr);&lt;br/&gt;&lt;br/&gt;    // Manually calculate the hash of the binary&amp;#39;s source file&lt;br/&gt;    let expected_hash = calculate_sha256(&amp;#34;src/bin/get_file_hash.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // Assert that the output contains the correct hash&lt;br/&gt;    assert!(stdout.contains(&amp;amp;expected_hash), &amp;#34;Output did not contain raw expected hash. Expected: {}, Actual: {}&amp;#34;, expected_hash, stdout);&lt;br/&gt;&lt;br/&gt;    // Then check for the formatted string, including backticks&lt;br/&gt;    let expected_hash_line = format!(&amp;#34;*   **SHA-256 Hash:** `{}`&amp;#34;, expected_hash);&lt;br/&gt;    assert!(stdout.contains(&amp;amp;expected_hash_line), &amp;#34;Output did not contain expected hash line. Expected line: {}, Actual: {}&amp;#34;, expected_hash_line, stdout);&lt;br/&gt;&lt;br/&gt;    // Assert that the output contains &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    assert!(stdout.contains(&amp;#34;Integrity Verified.&amp;#34;), &amp;#34;Output did not contain &amp;#39;Integrity Verified.&amp;#39;. stdout: {}&amp;#34;, stdout);&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;Output from get_file_hash binary (with nostr feature):&lt;br/&gt;{}&amp;#34;, stdout);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:51:46Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsth08tflfrga9zqhav304ztkm55xp26ev2cczlxd3j7apa3pvd6lgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvz7a4h0</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsth08tflfrga9zqhav304ztkm55xp26ev2cczlxd3j7apa3pvd6lgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvz7a4h0" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{net::AddrParseError, process::ExitCode};&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Kind, builder::Error as EventBuilderError},&lt;br/&gt;    signer::SignerError,&lt;br/&gt;};&lt;br/&gt;use nostr_sdk::client::Error as ClientError;&lt;br/&gt;&lt;br/&gt;use crate::cli::ConfigError;&lt;br/&gt;&lt;br/&gt;/// The input data was incorrect in some way. This should only be used for&lt;br/&gt;/// user’s data and not system file.&lt;br/&gt;const DATA_ERROR: u8 = 65;&lt;br/&gt;&lt;br/&gt;/// An internal software error has been detected. This should be limited to&lt;br/&gt;/// non-operating system related errors.&lt;br/&gt;const SOFTWARE_ERROR: u8 = 70;&lt;br/&gt;&lt;br/&gt;/// An error occurred while doing I/O on some file.&lt;br/&gt;const IO_ERROR: u8 = 74;&lt;br/&gt;&lt;br/&gt;/// Something was found in an unconfigured or misconfigured state.&lt;br/&gt;const CONFIG_ERROR: u8 = 78;&lt;br/&gt;&lt;br/&gt;pub type N34Result&amp;lt;T&amp;gt; = Result&amp;lt;T, N34Error&amp;gt;;&lt;br/&gt;&lt;br/&gt;/// N34 errors&lt;br/&gt;#[derive(Debug, thiserror::Error)]&lt;br/&gt;pub enum N34Error {&lt;br/&gt;    #[error(&amp;#34;IO: {0}&amp;#34;)]&lt;br/&gt;    Io(#[from] std::io::Error),&lt;br/&gt;    #[error(&amp;#34;Signer Error: {0}&amp;#34;)]&lt;br/&gt;    SignerError(#[from] SignerError),&lt;br/&gt;    #[error(&amp;#34;Invalid Browser Signer Proxy Address: {0}&amp;#34;)]&lt;br/&gt;    Addr(#[from] AddrParseError),&lt;br/&gt;    #[error(&amp;#34;Browser Signer Proxy Error: {0}&amp;#34;)]&lt;br/&gt;    BrowserSignerProxy(#[from] nostr_browser_signer_proxy::Error),&lt;br/&gt;    #[error(&amp;#34;Keyring error: {0}&amp;#34;)]&lt;br/&gt;    Keyring(#[from] nostr_keyring::Error),&lt;br/&gt;    #[error(&amp;#34;{0}&amp;#34;)]&lt;br/&gt;    Config(#[from] ConfigError),&lt;br/&gt;    #[error(&amp;#34;No editor specified in the `EDITOR` environment variable&amp;#34;)]&lt;br/&gt;    EditorNotFound,&lt;br/&gt;    #[error(&amp;#34;The file you edited is empty. Please save your changes before exiting the editor.&amp;#34;)]&lt;br/&gt;    EmptyEditorFile,&lt;br/&gt;    #[error(&amp;#34;The editor `{0}` exit with unsuccessful exit code `{1}`&amp;#34;)]&lt;br/&gt;    EditorErr(String, i32),&lt;br/&gt;    #[error(&amp;#34;Client Error: {0}&amp;#34;)]&lt;br/&gt;    Client(#[from] ClientError),&lt;br/&gt;    #[error(&amp;#34;Unable to locate the repository. The repository may not exists in the given relays&amp;#34;)]&lt;br/&gt;    NotFoundRepo,&lt;br/&gt;    #[error(&amp;#34;Failed building an event: {0}&amp;#34;)]&lt;br/&gt;    EventBuilder(#[from] EventBuilderError),&lt;br/&gt;    #[error(&amp;#34;Invalid repository id, it can&amp;#39;t be empty and must be kebab-case&amp;#34;)]&lt;br/&gt;    InvalidRepoId,&lt;br/&gt;    #[error(&amp;#34;Invalid event: {0}&amp;#34;)]&lt;br/&gt;    InvalidEvent(String),&lt;br/&gt;    #[error(&amp;#34;Bech32 error: {0}&amp;#34;)]&lt;br/&gt;    Bech32(#[from] nostr::nips::nip19::Error),&lt;br/&gt;    #[error(&amp;#34;Event error: {0}&amp;#34;)]&lt;br/&gt;    Event(#[from] nostr::event::Error),&lt;br/&gt;    #[error(&amp;#34;Event not found in the specified relays&amp;#34;)]&lt;br/&gt;    EventNotFound,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Can&amp;#39;t reply to this event. Only Git issues, patches, and their comments can be replied \&lt;br/&gt;         to.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    CanNotReplyToEvent,&lt;br/&gt;    #[error(&amp;#34;No repository address given and couldn&amp;#39;t read `nostr-address` file: {0}&amp;#34;)]&lt;br/&gt;    CanNotReadNostrAddressFile(std::io::Error),&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;The `nostr-address` file is empty.  Please add a valid Nostr repository address (naddr) \&lt;br/&gt;         to the file or provide it manually as a flag.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    EmptyNostrAddressFile,&lt;br/&gt;    #[error(&amp;#34;Invalid `nostr-address` file content: {0}&amp;#34;)]&lt;br/&gt;    InvalidNostrAddressFileContent(String),&lt;br/&gt;    #[error(&amp;#34;This command requires at least one relay, but none were provided&amp;#34;)]&lt;br/&gt;    EmptyRelays,&lt;br/&gt;    #[error(&amp;#34;One naddr is required for this command&amp;#34;)]&lt;br/&gt;    EmptyNaddrs,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;This command requires a signer to sign events. Use `--secret-key`, `--nip07` or \&lt;br/&gt;         `--bunker-url` to provide a signer&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    SignerRequired,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Invalid repository address. Expected one of these formats:\n- NIP-05 identifier with \&lt;br/&gt;         repository ID: `&amp;lt;user@domain.com&amp;gt;/&amp;lt;repo_id&amp;gt;`\n- Valid NIP-19 naddr string (starts with \&lt;br/&gt;         &amp;#39;naddr1...&amp;#39;)\n- Existing set name (merges all repositories in set)\nError: No set named \&lt;br/&gt;         &amp;#39;{0}&amp;#39; exists.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    InvalidNaddrArg(String),&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Invalid relays. Expected a relay url or a set name that contains some relays\nError: No \&lt;br/&gt;         set named &amp;#39;{0}&amp;#39; exists.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    InvalidRelaysArg(String),&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;The set &amp;#39;{0}&amp;#39; doesn&amp;#39;t contain any addresses. Use &amp;#39;sets update&amp;#39; to add addresses to it.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    EmptySetNaddrs(String),&lt;br/&gt;    #[error(&amp;#34;The set &amp;#39;{0}&amp;#39; doesn&amp;#39;t contain any relays. Use &amp;#39;sets update&amp;#39; to add relays to it.&amp;#34;)]&lt;br/&gt;    EmptySetRelays(String),&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Issue not found, make sure it is in the relays and make sure that the ID is an issue ID&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    CanNotFoundIssue,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Patch not found, make sure it is in the relays and make sure that the ID is an patch ID&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    CanNotFoundPatch,&lt;br/&gt;    #[error(r#&amp;#34;The given patch id is not a root patch. It must contains `[&amp;#34;t&amp;#34;, &amp;#34;root&amp;#34;]` tag&amp;#34;#)]&lt;br/&gt;    NotRootPatch,&lt;br/&gt;    #[error(&amp;#34;This status kind can&amp;#39;t be set for an issue: {0}&amp;#34;)]&lt;br/&gt;    InvalidIssueStatus(Kind),&lt;br/&gt;    #[error(&amp;#34;This status kind can&amp;#39;t be set for a patch: {0}&amp;#34;)]&lt;br/&gt;    InvalidPatchStatus(Kind),&lt;br/&gt;    #[error(&amp;#34;Can&amp;#39;t find the root patch of the given patch-revision&amp;#34;)]&lt;br/&gt;    RevisionRootNotFound,&lt;br/&gt;    #[error(&amp;#34;Invalid status for the issue/patch: {0}&amp;#34;)]&lt;br/&gt;    InvalidStatus(String),&lt;br/&gt;    #[error(&amp;#34;Not valid bunker URL&amp;#34;)]&lt;br/&gt;    NotBunkerUrl,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;No secret key found in the keyring. Please use the secret key at least once while \&lt;br/&gt;         keyring is enabled to store it&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    SecretKeyKeyringWithoutEntry,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl N34Error {&lt;br/&gt;    /// Returns the exit code associated with this error&lt;br/&gt;    pub fn exit_code(&amp;amp;self) -&amp;gt; ExitCode {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Io(_) | Self::CanNotReadNostrAddressFile(_) =&amp;gt; ExitCode::from(IO_ERROR),&lt;br/&gt;            Self::Config(_) =&amp;gt; ExitCode::from(CONFIG_ERROR),&lt;br/&gt;            Self::EditorErr(..) =&amp;gt; ExitCode::from(SOFTWARE_ERROR),&lt;br/&gt;            Self::InvalidRepoId&lt;br/&gt;            | Self::EmptyNostrAddressFile&lt;br/&gt;            | Self::InvalidNostrAddressFileContent(_)&lt;br/&gt;            | Self::EmptyRelays&lt;br/&gt;            | Self::EmptyNaddrs&lt;br/&gt;            | Self::SignerRequired&lt;br/&gt;            | Self::InvalidNaddrArg(_)&lt;br/&gt;            | Self::InvalidRelaysArg(_)&lt;br/&gt;            | Self::EmptySetNaddrs(_)&lt;br/&gt;            | Self::EmptySetRelays(_)&lt;br/&gt;            | Self::NotRootPatch =&amp;gt; ExitCode::from(DATA_ERROR),&lt;br/&gt;            _ =&amp;gt; ExitCode::FAILURE,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:51:36Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqswlzhdahhtns82qvpgfmv4jh5at4hqgwge9m5gsdpyxsg595xsa7qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvt3x457</id>
    
      <title type="html">//! A crate providing the `get_file_hash!` procedural macro. //! ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqswlzhdahhtns82qvpgfmv4jh5at4hqgwge9m5gsdpyxsg595xsa7qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvt3x457" />
    <content type="html">
      //! A crate providing the `get_file_hash!` procedural macro.&lt;br/&gt;//!&lt;br/&gt;//! This macro allows you to compute the SHA-256 hash of a file at compile time,&lt;br/&gt;//! embedding the resulting hash string directly into your Rust executable.&lt;br/&gt;&lt;br/&gt;pub use get_file_hash_core::get_file_hash;&lt;br/&gt;&lt;br/&gt;/// The SHA-256 hash of this crate&amp;#39;s `build.rs` at the time of compilation.&lt;br/&gt;pub const BUILD_HASH: &amp;amp;str = env!(&amp;#34;BUILD_HASH&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// The SHA-256 hash of this crate&amp;#39;s `Cargo.toml` at the time of compilation.&lt;br/&gt;pub const CARGO_TOML_HASH: &amp;amp;str = env!(&amp;#34;CARGO_TOML_HASH&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// The SHA-256 hash of this crate&amp;#39;s `src/lib.rs` at the time of compilation.&lt;br/&gt;pub const LIB_HASH: &amp;amp;str = env!(&amp;#34;LIB_HASH&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// The name of the package as specified in Cargo.toml.&lt;br/&gt;pub const CARGO_PKG_NAME: &amp;amp;str = env!(&amp;#34;CARGO_PKG_NAME&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// The version of the package as specified in Cargo.toml.&lt;br/&gt;pub const CARGO_PKG_VERSION: &amp;amp;str = env!(&amp;#34;CARGO_PKG_VERSION&amp;#34;);&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;/// The git commit hash of the repository at the time of compilation.&lt;br/&gt;pub const GIT_COMMIT_HASH: &amp;amp;str = env!(&amp;#34;GIT_COMMIT_HASH&amp;#34;);&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;/// The git branch of the repository at the time of compilation.&lt;br/&gt;pub const GIT_BRANCH: &amp;amp;str = env!(&amp;#34;GIT_BRANCH&amp;#34;);&lt;br/&gt;&lt;br/&gt;#[cfg(test)]&lt;br/&gt;mod tests {&lt;br/&gt;    use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;    use super::*;&lt;br/&gt;&lt;br/&gt;    /// Verifies that the exported CARGO_TOML_HASH is not empty.&lt;br/&gt;    #[test]&lt;br/&gt;    fn test_injected_hash_exists() {&lt;br/&gt;        assert!(!BUILD_HASH.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified build.rs Hash:&lt;br/&gt;{}&amp;#34;, BUILD_HASH);&lt;br/&gt;&lt;br/&gt;        assert!(!CARGO_TOML_HASH.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified Cargo.toml Hash:&lt;br/&gt;{}&amp;#34;, CARGO_TOML_HASH);&lt;br/&gt;&lt;br/&gt;        assert!(!LIB_HASH.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified src/lib.rs Hash:\n{}&amp;#34;, LIB_HASH);&lt;br/&gt;&lt;br/&gt;        assert!(!CARGO_PKG_NAME.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified Package Name:\n{}&amp;#34;, CARGO_PKG_NAME);&lt;br/&gt;&lt;br/&gt;        assert!(!CARGO_PKG_VERSION.is_empty());&lt;br/&gt;        println!(&amp;#34;Verified Package Version:\n{}&amp;#34;, CARGO_PKG_VERSION);&lt;br/&gt;&lt;br/&gt;        #[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;        {&lt;br/&gt;            assert!(!GIT_COMMIT_HASH.is_empty());&lt;br/&gt;            println!(&amp;#34;Verified Git Commit Hash:\n{}&amp;#34;, GIT_COMMIT_HASH);&lt;br/&gt;&lt;br/&gt;            assert!(!GIT_BRANCH.is_empty());&lt;br/&gt;            println!(&amp;#34;Verified Git Branch:\n{}&amp;#34;, GIT_BRANCH);&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Tests that the `get_file_hash!` macro correctly computes the SHA-256&lt;br/&gt;    /// hash of `lib.rs` and that it matches a manually computed hash of the&lt;br/&gt;    /// same file.&lt;br/&gt;    #[test]&lt;br/&gt;    fn test_get_lib_hash() {&lt;br/&gt;        let file_content = include_bytes!(&amp;#34;lib.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;        let mut hasher = Sha256::new();&lt;br/&gt;        hasher.update(file_content);&lt;br/&gt;        let expected_hash = hasher&lt;br/&gt;            .finalize()&lt;br/&gt;            .iter()&lt;br/&gt;            .map(|b| format!(&amp;#34;{:02x}&amp;#34;, b))&lt;br/&gt;            .collect::&amp;lt;String&amp;gt;();&lt;br/&gt;&lt;br/&gt;        let actual_hash = get_file_hash!(&amp;#34;lib.rs&amp;#34;);&lt;br/&gt;        assert_eq!(actual_hash, expected_hash);&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:51:30Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsgqrx03vy6ymjc7sftydgnue4u224g5fg0a08tky7z05t0dzmwtxszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvra8k6w</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsgqrx03vy6ymjc7sftydgnue4u224g5fg0a08tky7z05t0dzmwtxszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvra8k6w" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::str::FromStr;&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{EventId, Kind},&lt;br/&gt;    nips::{&lt;br/&gt;        nip01::Coordinate,&lt;br/&gt;        nip05::{Nip05Address, Nip05Profile},&lt;br/&gt;        nip19::{self, FromBech32, Nip19Coordinate},&lt;br/&gt;    },&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;    util::BoxedFuture,&lt;br/&gt;};&lt;br/&gt;use nostr_connect::client::AuthUrlHandler;&lt;br/&gt;use tokio::runtime::Handle;&lt;br/&gt;&lt;br/&gt;use super::parsers;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{RepoRelaySet, traits::RepoRelaySetsExt},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Either a NIP-19 coordinate (naddr) or a named set.&lt;br/&gt;#[derive(Debug, Clone)]&lt;br/&gt;pub enum NaddrOrSet {&lt;br/&gt;    /// NIP-19 coordinate.&lt;br/&gt;    Naddr(Nip19Coordinate),&lt;br/&gt;    /// Name of a set (may not exist).&lt;br/&gt;    Set(String),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Either relay URL or a named set.&lt;br/&gt;#[derive(Debug, Clone)]&lt;br/&gt;pub enum RelayOrSet {&lt;br/&gt;    /// Relay URL.&lt;br/&gt;    Relay(RelayUrl),&lt;br/&gt;    /// Name of a set (may not exist).&lt;br/&gt;    Set(String),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses and represents a Nostr `nevent1` or `note1`.&lt;br/&gt;#[derive(Debug, Clone)]&lt;br/&gt;pub struct NostrEvent {&lt;br/&gt;    /// Unique identifier for the event.&lt;br/&gt;    pub event_id: EventId,&lt;br/&gt;    /// List of relay URLs associated with the event. Empty if parsing a&lt;br/&gt;    /// `note1`.&lt;br/&gt;    pub relays:   Vec&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub struct EchoAuthUrl;&lt;br/&gt;&lt;br/&gt;impl AuthUrlHandler for EchoAuthUrl {&lt;br/&gt;    fn on_auth_url(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        auth_url: nostr::Url,&lt;br/&gt;    ) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;_, Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt;&amp;gt; {&lt;br/&gt;        Box::pin(async move {&lt;br/&gt;            println!(&amp;#34;The bunker requires authentication. Please open this URL: {auth_url}&amp;#34;);&lt;br/&gt;            Ok(())&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl NaddrOrSet {&lt;br/&gt;    /// Returns the naddr if `Naddr` or try to get the relays from the set.&lt;br/&gt;    /// Returns error if the set naddrs are empty or the set not found.&lt;br/&gt;    pub fn get_naddrs(self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Naddr(nip19_coordinate) =&amp;gt; Ok(vec![nip19_coordinate]),&lt;br/&gt;            Self::Set(name) =&amp;gt; {&lt;br/&gt;                let set = sets&lt;br/&gt;                    .get_set(&amp;amp;name)&lt;br/&gt;                    .map_err(|_| N34Error::InvalidNaddrArg(name.clone()))?;&lt;br/&gt;                if set.naddrs.is_empty() {&lt;br/&gt;                    Err(N34Error::EmptySetNaddrs(name))&lt;br/&gt;                } else {&lt;br/&gt;                    Ok(Vec::from_iter(set.naddrs.clone()))&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl RelayOrSet {&lt;br/&gt;    /// Returns the relay if `Relay` or try to get the relays from the set.&lt;br/&gt;    /// Returns error if the set relays are empty or the set not found&lt;br/&gt;    pub fn get_relays(self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;RelayUrl&amp;gt;&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Relay(relay) =&amp;gt; Ok(vec![relay]),&lt;br/&gt;            Self::Set(name) =&amp;gt; {&lt;br/&gt;                let set = sets&lt;br/&gt;                    .get_set(&amp;amp;name)&lt;br/&gt;                    .map_err(|_| N34Error::InvalidRelaysArg(name.clone()))?;&lt;br/&gt;                if set.relays.is_empty() {&lt;br/&gt;                    Err(N34Error::EmptySetRelays(name))&lt;br/&gt;                } else {&lt;br/&gt;                    Ok(Vec::from_iter(set.relays.clone()))&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl NostrEvent {&lt;br/&gt;    /// Create a new [`NostrEvent`] instance&lt;br/&gt;    fn new(event_id: EventId, relays: Vec&amp;lt;RelayUrl&amp;gt;) -&amp;gt; Self {&lt;br/&gt;        Self { event_id, relays }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl FromStr for NaddrOrSet {&lt;br/&gt;    type Err = String;&lt;br/&gt;&lt;br/&gt;    /// Parses a Git repository address which can be either:&lt;br/&gt;    /// - A bech32-encoded naddr (e.g. &amp;#34;naddr1...&amp;#34;) for Git repository&lt;br/&gt;    ///   announcements (kind 30617)&lt;br/&gt;    /// - A NIP-05 identifier with repository ID (e.g. &amp;#34;4rs.nl/n34&amp;#34; or&lt;br/&gt;    ///   &amp;#34;_@4rs.nl/n34&amp;#34;)&lt;br/&gt;    /// - A set name.&lt;br/&gt;    ///&lt;br/&gt;    /// Returns an error for invalid formats, failed bech32 decoding, wrong&lt;br/&gt;    /// event kind.&lt;br/&gt;    fn from_str(naddr_or_set: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, Self::Err&amp;gt; {&lt;br/&gt;        let naddr_or_set = naddr_or_set.trim();&lt;br/&gt;&lt;br/&gt;        if naddr_or_set.contains(&amp;#34;/&amp;#34;) {&lt;br/&gt;            let (nip5, repo_id) = naddr_or_set.split_once(&amp;#34;/&amp;#34;).expect(&amp;#34;There is a `/`&amp;#34;);&lt;br/&gt;            parse_nip5_repo(nip5, repo_id)&lt;br/&gt;        } else if naddr_or_set.starts_with(&amp;#34;naddr1&amp;#34;) || naddr_or_set.starts_with(&amp;#34;nostr:naddr1&amp;#34;) {&lt;br/&gt;            parsers::parse_repo_naddr(naddr_or_set.trim_start_matches(&amp;#34;nostr:&amp;#34;)).map(Self::Naddr)&lt;br/&gt;        } else {&lt;br/&gt;            Ok(Self::Set(naddr_or_set.to_owned()))&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl FromStr for RelayOrSet {&lt;br/&gt;    type Err = String;&lt;br/&gt;&lt;br/&gt;    /// Parse a string into a relay URL or a set name.&lt;br/&gt;    /// If the string is a valid URL (e.g., &amp;#34;wss://example.com&amp;#34;), it&amp;#39;s treated&lt;br/&gt;    /// as a relay URL. Otherwise, it&amp;#39;s treated as a set name, and its&lt;br/&gt;    /// associated relays will be merged.&lt;br/&gt;    fn from_str(relay_or_set: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, Self::Err&amp;gt; {&lt;br/&gt;        let relay_or_set = relay_or_set.trim();&lt;br/&gt;&lt;br/&gt;        if relay_or_set.starts_with(&amp;#34;wss://&amp;#34;) {&lt;br/&gt;            RelayUrl::from_str(relay_or_set)&lt;br/&gt;                .map_err(|err| err.to_string())&lt;br/&gt;                .map(Self::Relay)&lt;br/&gt;        } else {&lt;br/&gt;            Ok(Self::Set(relay_or_set.to_owned()))&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl FromStr for NostrEvent {&lt;br/&gt;    type Err = String;&lt;br/&gt;&lt;br/&gt;    fn from_str(s: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, Self::Err&amp;gt; {&lt;br/&gt;        let str_event = s.trim().trim_start_matches(&amp;#34;nostr:&amp;#34;);&lt;br/&gt;        if str_event.starts_with(&amp;#34;nevent1&amp;#34;) {&lt;br/&gt;            let event = nip19::Nip19Event::from_bech32(str_event).map_err(|e| e.to_string())?;&lt;br/&gt;            Ok(Self::new(event.event_id, event.relays))&lt;br/&gt;        } else if str_event.starts_with(&amp;#34;note1&amp;#34;) {&lt;br/&gt;            Ok(Self::new(&lt;br/&gt;                EventId::from_bech32(str_event).map_err(|e| e.to_string())?,&lt;br/&gt;                Vec::new(),&lt;br/&gt;            ))&lt;br/&gt;        } else {&lt;br/&gt;            Err(&amp;#34;Invalid event id, must starts with `note1` or `nevent1`&amp;#34;.to_owned())&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn parse_nip5_repo(nip5: &amp;amp;str, repo_id: &amp;amp;str) -&amp;gt; Result&amp;lt;NaddrOrSet, String&amp;gt; {&lt;br/&gt;    let (username, domain) = nip5.split_once(&amp;#34;@&amp;#34;).unwrap_or((&amp;#34;_&amp;#34;, nip5));&lt;br/&gt;&lt;br/&gt;    let nip5_address =&lt;br/&gt;        Nip05Address::parse(&amp;amp;format!(&amp;#34;{username}@{domain}&amp;#34;)).map_err(|err| err.to_string())?;&lt;br/&gt;&lt;br/&gt;    let nip5_json = tokio::task::block_in_place(|| {&lt;br/&gt;        Handle::current().block_on(async {&lt;br/&gt;            reqwest::get(nip5_address.url().as_str())&lt;br/&gt;                .await&lt;br/&gt;                .map_err(|err| err.to_string())?&lt;br/&gt;                .text()&lt;br/&gt;                .await&lt;br/&gt;                .map_err(|err| err.to_string())&lt;br/&gt;        })&lt;br/&gt;    })?;&lt;br/&gt;&lt;br/&gt;    let nip5_profile =&lt;br/&gt;        Nip05Profile::from_raw_json(&amp;amp;nip5_address, &amp;amp;nip5_json).map_err(|err| err.to_string())?;&lt;br/&gt;&lt;br/&gt;    Ok(NaddrOrSet::Naddr(Nip19Coordinate::new(&lt;br/&gt;        Coordinate::new(Kind::GitRepoAnnouncement, nip5_profile.public_key).identifier(repo_id),&lt;br/&gt;        nip5_profile.relays,&lt;br/&gt;    )))&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:51:23Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsx7vfng2nks6f7p076skhyvrj49xhw9tceydk9ye88n2yt8tvejdgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv5rtsvn</id>
    
      <title type="html">Relay URL,Latitude,Longitude wot.nostr.party,36.1627,-86.7816 ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsx7vfng2nks6f7p076skhyvrj49xhw9tceydk9ye88n2yt8tvejdgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv5rtsvn" />
    <content type="html">
      Relay URL,Latitude,Longitude&lt;br/&gt;wot.nostr.party,36.1627,-86.7816&lt;br/&gt;nostr.simplex.icu,50.1109,8.68213&lt;br/&gt;relay.snort.social,53.3498,-6.26031&lt;br/&gt;nostr.stakey.net,52.3676,4.90414&lt;br/&gt;relay.mccormick.cx,52.3563,4.95714&lt;br/&gt;nostr.overmind.lol,43.6532,-79.3832&lt;br/&gt;nostriches.club,43.6532,-79.3832&lt;br/&gt;kanagrovv-pyramid.kozow.com,43.4305,-83.9638&lt;br/&gt;relay.toastr.net,40.8054,-74.0241&lt;br/&gt;relay.primal.net,43.6532,-79.3832&lt;br/&gt;r.bitcoinhold.net,43.6532,-79.3832&lt;br/&gt;wot.dtonon.com,43.6532,-79.3832&lt;br/&gt;santo.iguanatech.net,40.8302,-74.1299&lt;br/&gt;relay2.ngengine.org,43.6532,-79.3832&lt;br/&gt;nostr.oxtr.dev,50.4754,12.3683&lt;br/&gt;simplex.icu,50.1109,8.68213&lt;br/&gt;bitchat.nostr1.com,38.6327,-90.1961&lt;br/&gt;relay.nostu.be,40.4167,-3.70329&lt;br/&gt;nostr.mifen.me,43.6532,-79.3832&lt;br/&gt;relay.vantis.ninja,43.6532,-79.3832&lt;br/&gt;nostr-kyomu-haskell.onrender.com,37.7775,-122.397&lt;br/&gt;nostr-relay.amethyst.name,39.0067,-77.4291&lt;br/&gt;relay.bitcoindistrict.org,43.6532,-79.3832&lt;br/&gt;nos.xmark.cc,50.6924,3.20113&lt;br/&gt;relay01.lnfi.network,35.6764,139.65&lt;br/&gt;nostr-dev.wellorder.net,45.5201,-122.99&lt;br/&gt;nostr.rblb.it,43.7094,10.6582&lt;br/&gt;offchain.pub,39.1585,-94.5728&lt;br/&gt;wot.nostr.place,32.7767,-96.797&lt;br/&gt;fanfares.nostr1.com,38.6327,-90.1961&lt;br/&gt;ephemeral.snowflare.cc,43.6532,-79.3832&lt;br/&gt;relay.visionfusen.org,43.6532,-79.3832&lt;br/&gt;nostr.carroarmato0.be,51.0368,3.21186&lt;br/&gt;social.amanah.eblessing.co,48.1046,11.6002&lt;br/&gt;relay.cyphernomad.com,60.4032,25.0321&lt;br/&gt;nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397&lt;br/&gt;relay.ngengine.org,43.6532,-79.3832&lt;br/&gt;relay.nosto.re,51.1792,5.89444&lt;br/&gt;rusty-uat.siberian-albacore.ts.net:8443,35.6764,139.65&lt;br/&gt;nostr.luisschwab.net,43.6532,-79.3832&lt;br/&gt;nostr.aruku.ovh,1.27994,103.849&lt;br/&gt;srtrelay.c-stellar.net,43.6532,-79.3832&lt;br/&gt;nostr.tagomago.me,3.139,101.687&lt;br/&gt;relay.threenine.services,51.5222,-0.62916&lt;br/&gt;nostr.robosats.org,64.1476,-21.9392&lt;br/&gt;nostr.2b9t.xyz,34.0549,-118.243&lt;br/&gt;strfry.apps3.slidestr.net,40.4167,-3.70329&lt;br/&gt;kitchen.zap.cooking,43.6532,-79.3832&lt;br/&gt;relay.homeinhk.xyz,45.5152,-122.678&lt;br/&gt;relay.seq1.net,43.6532,-79.3832&lt;br/&gt;relay.bornheimer.app,50.1109,8.68213&lt;br/&gt;relay.vrtmrz.net,43.6532,-79.3832&lt;br/&gt;nostr.red5d.dev,43.6532,-79.3832&lt;br/&gt;relay-freeharmonypeople.space,38.7223,-9.13934&lt;br/&gt;aaa-api.freefrom.space/v1/ws,43.6532,-79.3832&lt;br/&gt;dev.relay.edufeed.org,49.4521,11.0767&lt;br/&gt;nostr.myshosholoza.co.za,52.3913,4.66545&lt;br/&gt;nostr.plantroon.com,50.1013,8.62643&lt;br/&gt;wot.dergigi.com,64.1476,-21.9392&lt;br/&gt;npub1spxdug4m3y24hpx5crm0el4zhkk0wafs8kp6m0xu0wecygqej2xqq8gyhx.fips.network,43.6532,-79.3832&lt;br/&gt;nostrelay.circum.space,52.3676,4.90414&lt;br/&gt;relay.dreamith.to,43.6532,-79.3832&lt;br/&gt;relay.nostriches.club,43.6532,-79.3832&lt;br/&gt;nostr-relayrs.gateway.in.th,15.5163,103.194&lt;br/&gt;nostr.chrissexton.org,43.6532,-79.3832&lt;br/&gt;relay.flashapp.me,43.652,-79.3633&lt;br/&gt;nostr-relay-1.trustlessenterprise.com,43.6532,-79.3832&lt;br/&gt;strfry.shock.network,39.0438,-77.4874&lt;br/&gt;relay.snotr.nl:49999,52.0195,4.42946&lt;br/&gt;spatia-arcana.com,34.0362,-118.443&lt;br/&gt;nostr.computingcache.com,34.0356,-118.442&lt;br/&gt;nostr.self-determined.de,53.5,10.25&lt;br/&gt;relay.sharegap.net,43.6532,-79.3832&lt;br/&gt;spookstr2.nostr1.com,38.6327,-90.1961&lt;br/&gt;v-relay.d02.vrtmrz.net,34.6937,135.502&lt;br/&gt;bridge.tagomago.me,3.139,101.687&lt;br/&gt;antiprimal.net,43.6532,-79.3832&lt;br/&gt;relay.nostrdice.com,-33.8688,151.209&lt;br/&gt;purpura.cloud,43.6532,-79.3832&lt;br/&gt;espelho.girino.org,43.6532,-79.3832&lt;br/&gt;relay.mostro.network,40.8302,-74.1299&lt;br/&gt;temp.iris.to,43.6532,-79.3832&lt;br/&gt;pyramid.self-determined.de,53.5,10.25&lt;br/&gt;relay.jeffg.fyi,43.6532,-79.3832&lt;br/&gt;nostr.aruku.kro.kr,37.3589,127.115&lt;br/&gt;chat-relay.zap-work.com,43.6532,-79.3832&lt;br/&gt;relay.islandbitcoin.com,12.8498,77.6545&lt;br/&gt;nostr.zoracle.org,45.6018,-121.185&lt;br/&gt;relay-dev.satlantis.io,40.8302,-74.1299&lt;br/&gt;relay.earthly.city,34.0362,-118.443&lt;br/&gt;speakeasy.cellar.social,49.4543,11.0746&lt;br/&gt;relay.bnos.space,43.6532,-79.3832&lt;br/&gt;relay.henryxplace.eu.org:9988,31.2304,121.474&lt;br/&gt;relay.bitmacro.cloud,43.6532,-79.3832&lt;br/&gt;nostr.thebiglake.org,32.71,-96.6745&lt;br/&gt;relay.lab.rytswd.com,49.4543,11.0746&lt;br/&gt;nostr.nadajnik.org,50.1109,8.68213&lt;br/&gt;relay.evanverma.com,40.8302,-74.1299&lt;br/&gt;relay2.angor.io,48.1046,11.6002&lt;br/&gt;prl.plus,55.7628,37.5983&lt;br/&gt;myvoiceourstory.org,37.3598,-121.981&lt;br/&gt;relay.arx-ccn.com,50.4754,12.3683&lt;br/&gt;wot.sudocarlos.com,43.6532,-79.3832&lt;br/&gt;relayrs.notoshi.win,43.6532,-79.3832&lt;br/&gt;nostrcity-club.fly.dev,48.8575,2.35138&lt;br/&gt;relay.tagayasu.xyz,45.4215,-75.6972&lt;br/&gt;nostr.blankfors.se,60.1699,24.9384&lt;br/&gt;nrs-01.darkcloudarcade.com,39.1008,-94.5811&lt;br/&gt;relay.lightning.pub,39.0438,-77.4874&lt;br/&gt;nostr-02.yakihonne.com,1.32123,103.695&lt;br/&gt;relay.nostrverse.net,43.6532,-79.3832&lt;br/&gt;nostr.wecsats.io,43.6532,-79.3832&lt;br/&gt;relay.illuminodes.com,47.6062,-122.332&lt;br/&gt;api.freefrom.space/v1/ws,43.6532,-79.3832&lt;br/&gt;nostr-relay.psfoundation.info,39.0438,-77.4874&lt;br/&gt;relay.samt.st,40.8302,-74.1299&lt;br/&gt;nostr-relay.cbrx.io,43.6532,-79.3832&lt;br/&gt;inbox.mycelium.social,38.627,-90.1994&lt;br/&gt;relay.anmore.me,49.281,-123.117&lt;br/&gt;no.str.cr,10.074,-84.2155&lt;br/&gt;nstr.a0a1.space,52.3563,4.95714&lt;br/&gt;relay.typedcypher.com,51.5072,-0.127586&lt;br/&gt;relay.bitmacro.pro,43.6532,-79.3832&lt;br/&gt;relay.nostrzh.org,43.6532,-79.3832&lt;br/&gt;ynostr.yael.at,60.1699,24.9384&lt;br/&gt;nostr-relay.zeabur.app,25.0797,121.234&lt;br/&gt;dynasty.libretechsystems.xyz,55.4724,9.87335&lt;br/&gt;nostr.bitcoiner.social,47.6743,-117.112&lt;br/&gt;nostr.girino.org,43.6532,-79.3832&lt;br/&gt;nostr2.girino.org,43.6532,-79.3832&lt;br/&gt;nostr-verified.wellorder.net,45.5201,-122.99&lt;br/&gt;relay.fundstr.me,42.3601,-71.0589&lt;br/&gt;relay.mapboss.co.th,13.7234,100.784&lt;br/&gt;relay.qstr.app,51.5072,-0.127586&lt;br/&gt;nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832&lt;br/&gt;relay.klabo.world,47.674,-122.122&lt;br/&gt;relay.minibolt.info,43.6532,-79.3832&lt;br/&gt;x.kojira.io,43.6532,-79.3832&lt;br/&gt;relay-dev.gulugulu.moe,43.6532,-79.3832&lt;br/&gt;relay.nostriot.com,41.5695,-83.9786&lt;br/&gt;relayone.soundhsa.com,39.1008,-94.5811&lt;br/&gt;nr.yay.so,46.2126,6.1154&lt;br/&gt;relay.bithome.site,52.3563,4.95714&lt;br/&gt;relay.damus.io,43.6532,-79.3832&lt;br/&gt;nostr.mikoshi.de,50.1109,8.68213&lt;br/&gt;nostr.defucc.me,50.1109,8.68213&lt;br/&gt;relay.malxte.de,52.52,13.405&lt;br/&gt;relay.orangepill.ovh,49.1689,-0.358841&lt;br/&gt;bbw-nostr.xyz,41.5284,-87.4237&lt;br/&gt;kasztanowa.bieda.it,43.6532,-79.3832&lt;br/&gt;bitcoiner.social,47.6743,-117.112&lt;br/&gt;relay.lacompagniemaximus.com,45.3147,-73.8785&lt;br/&gt;relay.mostr.pub,43.6532,-79.3832&lt;br/&gt;relay.lanavault.space,60.1699,24.9384&lt;br/&gt;kotukonostr.onrender.com,37.7775,-122.397&lt;br/&gt;relay.ditto.pub,43.6532,-79.3832&lt;br/&gt;relay.erybody.com,41.4513,-81.7021&lt;br/&gt;nostr.dlcdevkit.com,40.0992,-83.1141&lt;br/&gt;ribo.us.nostria.app,41.5868,-93.625&lt;br/&gt;relay.paulstephenborile.com,49.4543,11.0746&lt;br/&gt;testnet-relay.samt.st,40.8302,-74.1299&lt;br/&gt;relay.purplefrog.cloud,35.6916,139.768&lt;br/&gt;relay.agorist.space,52.3734,4.89406&lt;br/&gt;nostr-relay.zimage.com,34.0549,-118.243&lt;br/&gt;nostr.azzamo.net,52.2633,21.0283&lt;br/&gt;strfry.elswa-dev.online,50.1109,8.68213&lt;br/&gt;wot.shaving.kiwi,43.6532,-79.3832&lt;br/&gt;okn.czas.plus,50.1109,8.68213&lt;br/&gt;bcast.seutoba.com.br,43.6532,-79.3832&lt;br/&gt;relay.sigit.io,50.4754,12.3683&lt;br/&gt;syb.lol,34.0549,-118.243&lt;br/&gt;relay.libernet.app,43.6532,-79.3832&lt;br/&gt;relay.angor.io,48.1046,11.6002&lt;br/&gt;relay.staging.commonshub.brussels,49.4543,11.0746&lt;br/&gt;strfry.atlantislabs.space,43.6532,-79.3832&lt;br/&gt;nostr.wom.wtf,43.6532,-79.3832&lt;br/&gt;nostrride.io,37.3986,-121.964&lt;br/&gt;nostr.dpinkerton.com,39.1008,-94.5811&lt;br/&gt;r.0kb.io,32.789,-96.7989&lt;br/&gt;nostr.hekster.org,37.3986,-121.964&lt;br/&gt;satsage.xyz,37.3986,-121.964&lt;br/&gt;nostr.islandarea.net,35.4669,-97.6473&lt;br/&gt;ve.agorawlc.com,50.4754,12.3683&lt;br/&gt;relay.openfarmtools.org,60.1699,24.9384&lt;br/&gt;top.testrelay.top,43.6532,-79.3832&lt;br/&gt;relay-rpi.edufeed.org,49.4521,11.0767&lt;br/&gt;pyramid.cult.cash,32.9483,-96.7299&lt;br/&gt;relay.edino.net,56.6268,47.9193&lt;br/&gt;nostr.snowbla.de,60.1699,24.9384&lt;br/&gt;relay.wavefunc.live,39.7392,-104.99&lt;br/&gt;tenex.chat,50.4754,12.3683&lt;br/&gt;relay.getsafebox.app,43.6532,-79.3832&lt;br/&gt;nostr.bond,50.1109,8.68213&lt;br/&gt;nostrelites.org,41.8781,-87.6298&lt;br/&gt;relay.plebeian.market,50.1109,8.68213&lt;br/&gt;relay.laantungir.net,-19.4692,-42.5315&lt;br/&gt;relay.decentnewsroom.com,50.4754,12.3683&lt;br/&gt;nostr-relay.nextblockvending.com,47.2343,-119.853&lt;br/&gt;relay.spacetomatoes.net,42.3601,-71.0589&lt;br/&gt;nostrbtc.com,43.6532,-79.3832&lt;br/&gt;relay.puresignal.news,43.6532,-79.3832&lt;br/&gt;relay-testnet.k8s.layer3.news,37.3387,-121.885&lt;br/&gt;relay.binaryrobot.com,43.6532,-79.3832&lt;br/&gt;relay.wavlake.com,41.2619,-95.8608&lt;br/&gt;inbox.scuba323.com,40.8218,-74.45&lt;br/&gt;nostr.spaceshell.xyz,43.6532,-79.3832&lt;br/&gt;relay.nostr.place,32.7767,-96.797&lt;br/&gt;holland-excited-charming-experiencing.trycloudflare.com,43.6532,-79.3832&lt;br/&gt;theoutpost.life,64.1476,-21.9392&lt;br/&gt;relay.fckstate.net,59.3293,18.0686&lt;br/&gt;bcast.girino.org,43.6532,-79.3832&lt;br/&gt;discovery.us.nostria.app,52.3676,4.90414&lt;br/&gt;relay.bullishbounty.com,43.6532,-79.3832&lt;br/&gt;nostr.88mph.life,51.5072,-0.127586&lt;br/&gt;nostr.tadryanom.me,43.6532,-79.3832&lt;br/&gt;nostr.sathoarder.com,48.5734,7.75211&lt;br/&gt;relay.nostr.net,43.6532,-79.3832&lt;br/&gt;zw.agorawlc.com,50.4754,12.3683&lt;br/&gt;relay.internationalright-wing.org,-22.5022,-48.7114&lt;br/&gt;nostr.vulpem.com,49.4543,11.0746&lt;br/&gt;wot.codingarena.top,50.4754,12.3683&lt;br/&gt;reraw.pbla2fish.cc,43.6532,-79.3832&lt;br/&gt;plebchain.club,43.6532,-79.3832&lt;br/&gt;orly-relay.imwald.eu,48.8575,2.35138&lt;br/&gt;relay.satnam.pub,43.6532,-79.3832&lt;br/&gt;cs-relay.nostrdev.com,50.4754,12.3683&lt;br/&gt;schnorr.me,43.6532,-79.3832&lt;br/&gt;nostr-relay.online,43.6532,-79.3832&lt;br/&gt;relay.routstr.com,43.6532,-79.3832&lt;br/&gt;relay.ohstr.com,43.6532,-79.3832&lt;br/&gt;relay.lanacoin-eternity.com,40.8302,-74.1299&lt;br/&gt;wot.nostr.net,43.6532,-79.3832&lt;br/&gt;nostr.ps1829.com,33.8851,130.883&lt;br/&gt;yabu.me,35.6092,139.73&lt;br/&gt;soloco.nl,43.6532,-79.3832&lt;br/&gt;librerelay.aaroniumii.com,43.6532,-79.3832&lt;br/&gt;relay.mmwaves.de,48.8575,2.35138&lt;br/&gt;relay.artx.market,43.6548,-79.3885&lt;br/&gt;nostr.jerrynya.fun,31.2304,121.474&lt;br/&gt;relay-arg.zombi.cloudrodion.com,1.35208,103.82&lt;br/&gt;relay.edufeed.org,49.4521,11.0767&lt;br/&gt;discovery.eu.nostria.app,52.3676,4.90414&lt;br/&gt;relay.layer.systems,49.0291,8.35695&lt;br/&gt;nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874&lt;br/&gt;relay.0xchat.com,43.6532,-79.3832&lt;br/&gt;nos.lol,50.4754,12.3683&lt;br/&gt;lightning.red,53.3498,-6.26031&lt;br/&gt;slick.mjex.me,39.0418,-77.4744&lt;br/&gt;relay.boredvictor.xyz,41.3888,2.15899&lt;br/&gt;nostr.rtvslawenia.com,49.4543,11.0746&lt;br/&gt;relay.mitchelltribe.com,39.0438,-77.4874&lt;br/&gt;nostr.4rs.nl,49.0291,8.35696&lt;br/&gt;relay.olas.app,50.4754,12.3683&lt;br/&gt;memlay.v0l.io,53.3498,-6.26031&lt;br/&gt;nostr-01.yakihonne.com,1.29524,103.79&lt;br/&gt;relay.satmaxt.xyz,43.6532,-79.3832&lt;br/&gt;nostrcheck.tnsor.network,43.6532,-79.3832&lt;br/&gt;relay.guggero.org,46.0037,8.95105&lt;br/&gt;ai.techunder.tech:56711,22.5429,114.06&lt;br/&gt;premium.primal.net,43.6532,-79.3832&lt;br/&gt;nostr.tac.lol,47.4748,-122.273&lt;br/&gt;relay.zone667.com,60.1699,24.9384&lt;br/&gt;nostr-relay.gateway.in.th,15.5163,103.194&lt;br/&gt;vault.iris.to,43.6532,-79.3832&lt;br/&gt;strfry.bonsai.com,37.8716,-122.273&lt;br/&gt;ribo.eu.nostria.app,52.3676,4.90414&lt;br/&gt;relay.wellorder.net,45.5201,-122.99&lt;br/&gt;relay.tapestry.ninja,40.8054,-74.0241&lt;br/&gt;relay.dwadziesciajeden.pl,52.2297,21.0122&lt;br/&gt;relay.satlantis.io,32.8769,-80.0114&lt;br/&gt;nostr.pbfs.io,50.4754,12.3683&lt;br/&gt;freelay.sovbit.host,64.1476,-21.9392&lt;br/&gt;articles.layer3.news,37.3387,-121.885&lt;br/&gt;nostr.na.social,43.6532,-79.3832&lt;br/&gt;relay.fountain.fm,43.6532,-79.3832&lt;br/&gt;dev.relay.stream,43.6532,-79.3832&lt;br/&gt;nostr.n7ekb.net,36.1527,-95.9902&lt;br/&gt;relay5.bitransfer.org,43.6532,-79.3832&lt;br/&gt;relay.og.coop,43.6532,-79.3832&lt;br/&gt;nostr-server-production.up.railway.app,45.5019,-73.5674&lt;br/&gt;bucket.coracle.social,37.7775,-122.397&lt;br/&gt;relay.gulugulu.moe,43.6532,-79.3832&lt;br/&gt;relay.nostr-check.me,43.6532,-79.3832&lt;br/&gt;nostr.faultables.net,43.6532,-79.3832&lt;br/&gt;strfry.openhoofd.nl,51.9229,4.40833&lt;br/&gt;nostr.rblb.it:7777,43.7094,10.6582&lt;br/&gt;relay.nostrcheck.me,43.6532,-79.3832&lt;br/&gt;0x-nostr-relay.fly.dev,48.8575,2.35138&lt;br/&gt;nostr.thalheim.io,60.1699,24.9384&lt;br/&gt;relay-nl.zombi.cloudrodion.com,50.8943,6.06237&lt;br/&gt;relay.shadowbip.com,51.5072,-0.127586&lt;br/&gt;nostr-relay.corb.net,38.8353,-104.822&lt;br/&gt;purplerelay.com,43.6532,-79.3832&lt;br/&gt;nostr-pub.wellorder.net,45.5201,-122.99&lt;br/&gt;herbstmeister.com,34.0549,-118.243&lt;br/&gt;nostrcheck.me,43.6532,-79.3832&lt;br/&gt;pyramid.nostr.technology,52.3947,4.66399&lt;br/&gt;nostr.spicyz.io,43.6532,-79.3832&lt;br/&gt;nrs-02.darkcloudarcade.com,39.9526,-75.1652&lt;br/&gt;nestr.nedao.ch,47.0151,6.98832&lt;br/&gt;nostr.nodesmap.com,59.3327,18.0656&lt;br/&gt;nittom.nostr1.com,38.6327,-90.1961&lt;br/&gt;public.crostr.com,43.6532,-79.3832&lt;br/&gt;relay.cypherflow.ai,48.8575,2.35138&lt;br/&gt;nostr.bitczat.pl,60.1699,24.9384&lt;br/&gt;relayone.geektank.ai,39.1008,-94.5811&lt;br/&gt;testrelay.era21.space,43.6532,-79.3832&lt;br/&gt;relay.npubhaus.com,43.6532,-79.3832&lt;br/&gt;relay.bitmacro.io,48.8566,2.35222&lt;br/&gt;nostr.data.haus,50.4754,12.3683&lt;br/&gt;relay.credenso.cafe,43.3601,-80.3127&lt;br/&gt;relay.ru.ac.th,13.7607,100.627&lt;br/&gt;relay-fra.zombi.cloudrodion.com,48.8566,2.35222&lt;br/&gt;nostr.chaima.info,50.1109,8.68213&lt;br/&gt;nostr.mom,50.4754,12.3683
    </content>
    <updated>2026-04-05T05:51:18Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsxw7dwfje5knxssttrsdlwphz8mf9k0r9u4gq94cg8pdmcpzql9vqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdtpthp</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsxw7dwfje5knxssttrsdlwphz8mf9k0r9u4gq94cg8pdmcpzql9vqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdtpthp" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use nostr::{event::EventId, nips::nip19::Nip19Coordinate, types::RelayUrl};&lt;br/&gt;&lt;br/&gt;use super::CliOptions;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        ConfigError,&lt;br/&gt;        RepoRelaySet,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent, RelayOrSet},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// A trait defining the interface for command runners in the CLI.&lt;br/&gt;pub trait CommandRunner {&lt;br/&gt;    /// Whether this command needs the relays option (false by default).&lt;br/&gt;    /// Only applies to commands, not subcommands.&lt;br/&gt;    const NEED_RELAYS: bool = false;&lt;br/&gt;    /// Indicates if this command requires the signer. Defaults to true.&lt;br/&gt;    /// Only applies to commands, not subcommands.&lt;br/&gt;    const NEED_SIGNER: bool = true;&lt;br/&gt;&lt;br/&gt;    /// Executes the command and returns a Result indicating success or failure.&lt;br/&gt;    fn run(self, options: CliOptions) -&amp;gt; impl Future&amp;lt;Output = N34Result&amp;lt;()&amp;gt;&amp;gt; &#43; Send;&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(VecNostrEventExt)]&lt;br/&gt;impl Vec&amp;lt;NostrEvent&amp;gt; {&lt;br/&gt;    /// Extracts `EventId` from each `NostrEvent` and collects them into a&lt;br/&gt;    /// `Vec&amp;lt;EventId&amp;gt;`.&lt;br/&gt;    pub fn into_event_ids(self) -&amp;gt; Vec&amp;lt;EventId&amp;gt; {&lt;br/&gt;        self.into_iter().map(|e| e.event_id).collect()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(NaddrOrSetVecExt)]&lt;br/&gt;impl Vec&amp;lt;NaddrOrSet&amp;gt; {&lt;br/&gt;    /// Converts this vector of [`NaddrOrSet`] into a flat vector of&lt;br/&gt;    /// [`Nip19Coordinate`] using the given sets.&lt;br/&gt;    pub fn flat_naddrs(self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;        self.into_iter()&lt;br/&gt;            .map(|n| n.get_naddrs(sets))&lt;br/&gt;            .try_fold(Vec::new(), |mut acc, item| {&lt;br/&gt;                acc.extend(item?);&lt;br/&gt;                Ok(acc)&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(RelayOrSetVecExt)]&lt;br/&gt;impl Vec&amp;lt;RelayOrSet&amp;gt; {&lt;br/&gt;    /// Converts this vector of [`RelayOrSet`] into a flat vector of&lt;br/&gt;    /// [`RelayUrl`] using the given sets.&lt;br/&gt;    pub fn flat_relays(self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;RelayUrl&amp;gt;&amp;gt; {&lt;br/&gt;        self.into_iter()&lt;br/&gt;            .map(|n| n.get_relays(sets))&lt;br/&gt;            .try_fold(Vec::new(), |mut acc, item| {&lt;br/&gt;                acc.extend(item?);&lt;br/&gt;                Ok(acc)&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(OptionNaddrOrSetVecExt)]&lt;br/&gt;impl Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt; {&lt;br/&gt;    /// Converts this vector of [`NaddrOrSet`] into a flat vector of&lt;br/&gt;    /// [`Nip19Coordinate`] using the given sets.&lt;br/&gt;    pub fn flat_naddrs(&amp;amp;self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt;&amp;gt; {&lt;br/&gt;        // Clones self here to simplify command code&lt;br/&gt;        self.clone()&lt;br/&gt;            .map(|naddrs| naddrs.flat_naddrs(sets))&lt;br/&gt;            .transpose()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(MutRepoRelaySetsExt)]&lt;br/&gt;impl Vec&amp;lt;RepoRelaySet&amp;gt; {&lt;br/&gt;    /// Removes duplicate repository addresses from each set.&lt;br/&gt;    ///&lt;br/&gt;    /// Relays are automatically deduplicated by the HashSet, but&lt;br/&gt;    /// repository addresses may appear duplicated if relays are sorted&lt;br/&gt;    /// differently or when relay counts vary. This compares addresses by&lt;br/&gt;    /// their coordinates, ignoring any embedded relay details.&lt;br/&gt;    pub fn dedup_naddrs(&amp;amp;mut self) {&lt;br/&gt;        self.iter_mut().for_each(RepoRelaySet::dedup_naddrs);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finds and returns a mutable reference a set with the given name. Returns&lt;br/&gt;    /// an error if no set with this name exists.&lt;br/&gt;    pub fn get_mut_set(&amp;amp;mut self, name: impl AsRef&amp;lt;str&amp;gt;) -&amp;gt; N34Result&amp;lt;&amp;amp;mut RepoRelaySet&amp;gt; {&lt;br/&gt;        let name = name.as_ref();&lt;br/&gt;        let set = self&lt;br/&gt;            .iter_mut()&lt;br/&gt;            .find(|set| set.name == name)&lt;br/&gt;            .ok_or_else(|| N34Error::from(ConfigError::SetNotFound(name.to_owned())))?;&lt;br/&gt;&lt;br/&gt;        tracing::trace!(&lt;br/&gt;            name = %name, set = ?set,&lt;br/&gt;            &amp;#34;Successfully located a set with the giving name&amp;#34;&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        Ok(set)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Creates and pushes a new set with the given name.&lt;br/&gt;    ///&lt;br/&gt;    /// Returns an error if a set with the same name already exists.&lt;br/&gt;    pub fn push_set(&lt;br/&gt;        &amp;amp;mut self,&lt;br/&gt;        name: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;        repos: impl IntoIterator&amp;lt;Item = Nip19Coordinate&amp;gt;,&lt;br/&gt;        relays: impl IntoIterator&amp;lt;Item = RelayUrl&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let set_name: String = name.into();&lt;br/&gt;        tracing::trace!(sets = ?self, &amp;#34;Pushing set &amp;#39;{set_name}&amp;#39; to sets collection&amp;#34;);&lt;br/&gt;&lt;br/&gt;        if self.as_slice().exists(&amp;amp;set_name) {&lt;br/&gt;            return Err(ConfigError::SetDuplicateName(set_name).into());&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        self.push(RepoRelaySet::new(set_name, repos, relays));&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Removes the set with the given name if it exists. Returns an error if&lt;br/&gt;    /// the set is not found.&lt;br/&gt;    pub fn remove_set(&amp;amp;mut self, name: impl Into&amp;lt;String&amp;gt;) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let set_name: String = name.into();&lt;br/&gt;        tracing::trace!(set_name, sets = ?self, &amp;#34;Removing set &amp;#39;{set_name}&amp;#39; from sets collection&amp;#34;);&lt;br/&gt;&lt;br/&gt;        if !self.as_slice().exists(&amp;amp;set_name) {&lt;br/&gt;            return Err(ConfigError::SetNotFound(set_name).into());&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        self.retain(|s| s.name != set_name);&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Removes the given relays from the specified set.&lt;br/&gt;    pub fn remove_relays(&lt;br/&gt;        &amp;amp;mut self,&lt;br/&gt;        name: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;        relays: impl Iterator&amp;lt;Item = RelayUrl&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let relays = Vec::from_iter(relays);&lt;br/&gt;        let set = self.get_mut_set(name.into())?;&lt;br/&gt;&lt;br/&gt;        set.relays.retain(|r| !relays.contains(r));&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Removes the given naddrs from the specified set.&lt;br/&gt;    pub fn remove_naddrs(&lt;br/&gt;        &amp;amp;mut self,&lt;br/&gt;        name: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;        naddrs: impl Iterator&amp;lt;Item = Nip19Coordinate&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let coordinates = Vec::from_iter(naddrs.map(|n| n.coordinate));&lt;br/&gt;        let set = self.get_mut_set(name.into())?;&lt;br/&gt;&lt;br/&gt;        set.naddrs.retain(|n| !coordinates.contains(&amp;amp;n.coordinate));&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(RepoRelaySetsExt)]&lt;br/&gt;impl &amp;amp;[RepoRelaySet] {&lt;br/&gt;    /// Checks for duplicate set names. Returns an error if any duplicates are&lt;br/&gt;    /// found.&lt;br/&gt;    pub fn ensure_names(&amp;amp;self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let mut names = Vec::with_capacity(self.len());&lt;br/&gt;        names.extend(self.iter().map(|s| s.name.to_owned()));&lt;br/&gt;&lt;br/&gt;        names.sort_unstable();&lt;br/&gt;&lt;br/&gt;        if let Some(duplicate) = duplicate_in_sorted(&amp;amp;names) {&lt;br/&gt;            return Err(ConfigError::SetDuplicateName(duplicate.clone()).into());&lt;br/&gt;        }&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if a set with the given name exists.&lt;br/&gt;    pub fn exists(&amp;amp;self, set_name: &amp;amp;str) -&amp;gt; bool {&lt;br/&gt;        self.iter().any(|set| set.name == set_name)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finds and returns a reference a set with the given name. Returns an&lt;br/&gt;    /// error if no set with this name exists.&lt;br/&gt;    pub fn get_set(&amp;amp;self, name: impl AsRef&amp;lt;str&amp;gt;) -&amp;gt; N34Result&amp;lt;&amp;amp;RepoRelaySet&amp;gt; {&lt;br/&gt;        let name = name.as_ref();&lt;br/&gt;        let set = self&lt;br/&gt;            .iter()&lt;br/&gt;            .find(|set| set.name == name)&lt;br/&gt;            .ok_or_else(|| N34Error::from(ConfigError::SetNotFound(name.to_owned())))?;&lt;br/&gt;        tracing::trace!(&lt;br/&gt;            name = %name, set = ?set,&lt;br/&gt;            &amp;#34;Successfully located a set with the giving name&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        Ok(set)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Helper function that checks for duplicates in a sorted slice&lt;br/&gt;fn duplicate_in_sorted&amp;lt;T: PartialEq &#43; Clone&amp;gt;(items: &amp;amp;[T]) -&amp;gt; Option&amp;lt;&amp;amp;T&amp;gt; {&lt;br/&gt;    items.windows(2).find(|w| w[0] == w[1]).map(|w| &amp;amp;w[0])&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:51:12Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs8jwdq9tp8xfer0khxgrfcwuqa3am32zmdtgm886sf80h6jghlp5qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0drmg9</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs8jwdq9tp8xfer0khxgrfcwuqa3am32zmdtgm886sf80h6jghlp5qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0drmg9" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    collections::HashSet,&lt;br/&gt;    fs,&lt;br/&gt;    path::{Path, PathBuf},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    Kind,&lt;br/&gt;    nips::{&lt;br/&gt;        nip19::{FromBech32, Nip19Coordinate, ToBech32},&lt;br/&gt;        nip46::NostrConnectURI,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;use serde::{Deserialize, Serialize, Serializer};&lt;br/&gt;&lt;br/&gt;use super::CliConfig;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::DEFAULT_FALLBACK_PATH,&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;pub fn parse_repo_naddr(repo_naddr: &amp;amp;str) -&amp;gt; Result&amp;lt;Nip19Coordinate, String&amp;gt; {&lt;br/&gt;    let naddr = Nip19Coordinate::from_bech32(repo_naddr).map_err(|err| err.to_string())?;&lt;br/&gt;    if naddr.relays.is_empty() {&lt;br/&gt;        tracing::warn!(&amp;#34;The repository naddr does not contain any relay hints&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    (naddr.kind == Kind::GitRepoAnnouncement)&lt;br/&gt;        .then_some(naddr)&lt;br/&gt;        .ok_or_else(|| &amp;#34;Invalid naddr: must be of kind 30617 (GitRepoAnnouncement)&amp;#34;.to_owned())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses a nostr-address file into a NIP-19 coordinates. Expects the file to&lt;br/&gt;/// contain a repository announcements.&lt;br/&gt;pub fn parse_nostr_address_file(file_path: &amp;amp;Path) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;    let addresses = fs::read_to_string(file_path)&lt;br/&gt;        .map_err(N34Error::CanNotReadNostrAddressFile)?&lt;br/&gt;        .split(&amp;#34;\n&amp;#34;)&lt;br/&gt;        .filter_map(|line| {&lt;br/&gt;            (!line.starts_with(&amp;#34;#&amp;#34;) &amp;amp;&amp;amp; !line.trim().is_empty())&lt;br/&gt;                .then_some(parse_repo_naddr(line).map_err(N34Error::InvalidNostrAddressFileContent))&lt;br/&gt;        })&lt;br/&gt;        .collect::&amp;lt;N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt;&amp;gt;()?;&lt;br/&gt;    if addresses.is_empty() {&lt;br/&gt;        return Err(N34Error::EmptyNostrAddressFile);&lt;br/&gt;    }&lt;br/&gt;    Ok(addresses)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Loads CLI configuration from given path. Uses default config path if input&lt;br/&gt;/// matches fallback path.&lt;br/&gt;pub fn parse_config_path(config_path: &amp;amp;str) -&amp;gt; N34Result&amp;lt;CliConfig&amp;gt; {&lt;br/&gt;    let mut path = PathBuf::from(config_path.trim());&lt;br/&gt;&lt;br/&gt;    if config_path == DEFAULT_FALLBACK_PATH {&lt;br/&gt;        path = super::defaults::config_path()?;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    CliConfig::load(path)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses a bunker URL and checks if it&amp;#39;s a valid Nostr Connect URI.&lt;br/&gt;/// Returns an error if the URL is not a valid bunker URL.&lt;br/&gt;pub fn parse_bunker_url(bunker_url: &amp;amp;str) -&amp;gt; N34Result&amp;lt;NostrConnectURI&amp;gt; {&lt;br/&gt;    match NostrConnectURI::parse(bunker_url) {&lt;br/&gt;        Ok(url) if url.is_bunker() =&amp;gt; Ok(url),&lt;br/&gt;        _ =&amp;gt; Err(N34Error::NotBunkerUrl),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Serializes a set of NIP-19 coordinates as a list of bech32 strings.&lt;br/&gt;pub fn ser_naddrs&amp;lt;S&amp;gt;(naddr: &amp;amp;HashSet&amp;lt;Nip19Coordinate&amp;gt;, serializer: S) -&amp;gt; Result&amp;lt;S::Ok, S::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    S: Serializer,&lt;br/&gt;{&lt;br/&gt;    let str_naddrs = naddr&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|n| n.to_bech32().map_err(|err| err.to_string()))&lt;br/&gt;        .collect::&amp;lt;Result&amp;lt;Vec&amp;lt;_&amp;gt;, _&amp;gt;&amp;gt;()&lt;br/&gt;        .map_err(serde::ser::Error::custom)?;&lt;br/&gt;&lt;br/&gt;    str_naddrs.serialize(serializer)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Deserializes a list of bech32 strings into a set of NIP-19 coordinates.&lt;br/&gt;pub fn de_naddrs&amp;lt;&amp;#39;de, D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;HashSet&amp;lt;Nip19Coordinate&amp;gt;, D::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    D: serde::Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;{&lt;br/&gt;    Vec::&amp;lt;String&amp;gt;::deserialize(deserializer)?&lt;br/&gt;        .into_iter()&lt;br/&gt;        .map(|naddr| Nip19Coordinate::from_bech32(&amp;amp;naddr))&lt;br/&gt;        .collect::&amp;lt;Result&amp;lt;HashSet&amp;lt;_&amp;gt;, _&amp;gt;&amp;gt;()&lt;br/&gt;        .map_err(serde::de::Error::custom)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn ser_bunker_url&amp;lt;S&amp;gt;(&lt;br/&gt;    bunker_url: &amp;amp;Option&amp;lt;NostrConnectURI&amp;gt;,&lt;br/&gt;    serializer: S,&lt;br/&gt;) -&amp;gt; Result&amp;lt;S::Ok, S::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    S: Serializer,&lt;br/&gt;{&lt;br/&gt;    bunker_url&lt;br/&gt;        .as_ref()&lt;br/&gt;        .map(|u| u.to_string())&lt;br/&gt;        .serialize(serializer)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn de_bunker_url&amp;lt;&amp;#39;de, D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;Option&amp;lt;NostrConnectURI&amp;gt;, D::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    D: serde::Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;{&lt;br/&gt;    Option::&amp;lt;String&amp;gt;::deserialize(deserializer)?&lt;br/&gt;        .map(|u| parse_bunker_url(&amp;amp;u))&lt;br/&gt;        .transpose()&lt;br/&gt;        .map_err(serde::de::Error::custom)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:51:01Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs2rtega9en06df495096ww4jcatue9ffu0ajy8rey2myd8syjm5nszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8cjjg9</id>
    
      <title type="html">#![cfg(feature = &amp;#34;nostr&amp;#34;)] use frost_secp256k1_tr as ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs2rtega9en06df495096ww4jcatue9ffu0ajy8rey2myd8syjm5nszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8cjjg9" />
    <content type="html">
      #![cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;&lt;br/&gt;use frost_secp256k1_tr as frost;&lt;br/&gt;use frost::keys::PublicKeyPackage;&lt;br/&gt;use frost::round2::SignatureShare;&lt;br/&gt;use frost::SigningPackage;&lt;br/&gt;use hex;&lt;br/&gt;use rand::thread_rng;&lt;br/&gt;use std::collections::BTreeMap;&lt;br/&gt;use sha2::Sha256;&lt;br/&gt;use serde_json;&lt;br/&gt;use sha2::Digest;&lt;br/&gt;&lt;br/&gt;pub fn process_relay_share(&lt;br/&gt;    relay_payload_hex: &amp;amp;str,&lt;br/&gt;    signer_id_u16: u16,&lt;br/&gt;    _signing_package: &amp;amp;SigningPackage,&lt;br/&gt;    _pubkey_package: &amp;amp;PublicKeyPackage,&lt;br/&gt;) -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    // In a real scenario, this function would deserialize the share, perform&lt;br/&gt;    // individual verification, and store it for aggregation.&lt;br/&gt;    // For this example, we&amp;#39;ll just acknowledge receipt.&lt;br/&gt;    let _share_bytes = hex::decode(relay_payload_hex)?;&lt;br/&gt;    let _share = SignatureShare::deserialize(&amp;amp;_share_bytes)?;&lt;br/&gt;    let _identifier = frost::Identifier::try_from(signer_id_u16)?;&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;✅ Share from Signer {} processed (simplified).&amp;#34;, signer_id_u16);&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn simulate_frost_mailbox_coordinator() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    let mut rng = thread_rng();&lt;br/&gt;    let (max_signers, min_signers) = (2, 2);&lt;br/&gt;&lt;br/&gt;    let (shares, pubkey_package) = frost::keys::generate_with_dealer(&lt;br/&gt;        max_signers,&lt;br/&gt;        min_signers,&lt;br/&gt;        frost::keys::IdentifierList::Default,&lt;br/&gt;        &amp;amp;mut rng,&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    let signer1_id = frost::Identifier::try_from(1 as u16)?;&lt;br/&gt;    let key_package1: frost::keys::KeyPackage = shares[&amp;amp;signer1_id].clone().try_into()?;&lt;br/&gt;    let signer2_id = frost::Identifier::try_from(2 as u16)?;&lt;br/&gt;    let key_package2: frost::keys::KeyPackage = shares[&amp;amp;signer2_id].clone().try_into()?;&lt;br/&gt;&lt;br/&gt;    let message = b&amp;#34;BIP-64MOD: Anchor Data Proposal v1&amp;#34;;&lt;br/&gt;&lt;br/&gt;    let (nonces1, comms1) = frost::round1::commit(key_package1.signing_share(), &amp;amp;mut rng);&lt;br/&gt;    let (nonces2, comms2) = frost::round1::commit(key_package2.signing_share(), &amp;amp;mut rng);&lt;br/&gt;&lt;br/&gt;    let mut session_commitments = BTreeMap::new();&lt;br/&gt;    session_commitments.insert(signer1_id, comms1);&lt;br/&gt;    session_commitments.insert(signer2_id, comms2);&lt;br/&gt;&lt;br/&gt;    let signing_package = frost::SigningPackage::new(session_commitments.clone(), message);&lt;br/&gt;&lt;br/&gt;    let share1 = frost::round2::sign(&amp;amp;signing_package, &amp;amp;nonces1, &amp;amp;key_package1)?;&lt;br/&gt;    let share1_hex = hex::encode(share1.serialize());&lt;br/&gt;&lt;br/&gt;    let share2 = frost::round2::sign(&amp;amp;signing_package, &amp;amp;nonces2, &amp;amp;key_package2)?;&lt;br/&gt;    let share2_hex = hex::encode(share2.serialize());&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;Coordinator listening for Nostr events (simulated)...&amp;#34;);&lt;br/&gt;&lt;br/&gt;    process_relay_share(&amp;amp;share1_hex, 1_u16, &amp;amp;signing_package, &amp;amp;pubkey_package)?;&lt;br/&gt;    process_relay_share(&amp;amp;share2_hex, 2_u16, &amp;amp;signing_package, &amp;amp;pubkey_package)?;&lt;br/&gt;    println!(&amp;#34;All required shares processed. Coordinator would now aggregate.&amp;#34;);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Simulates a Signer producing a FROST signature share and preparing a Nostr event&lt;br/&gt;/// to be sent to a coordinator via a &amp;#34;mailbox&amp;#34; relay.&lt;br/&gt;///&lt;br/&gt;/// In a real ROAST setup, signers would generate their share and post it&lt;br/&gt;/// encrypted (e.g., using NIP-44) to a coordinator&amp;#39;s &amp;#34;mailbox&amp;#34; on a Nostr relay.&lt;br/&gt;/// This function demonstrates the creation of the signature share and the&lt;br/&gt;/// construction of a *simplified* Nostr event JSON.&lt;br/&gt;///&lt;br/&gt;/// # Arguments&lt;br/&gt;///&lt;br/&gt;/// * `_identifier` - The FROST identifier of the signer. (Currently unused in this specific function body).&lt;br/&gt;/// * `signing_package` - The FROST signing package received from the coordinator.&lt;br/&gt;/// * `nonces` - The signer&amp;#39;s nonces generated in Round 1.&lt;br/&gt;/// * `key_package` - The signer&amp;#39;s FROST key package.&lt;br/&gt;/// * `coordinator_pubkey` - The hex-encoded public key of the ROAST coordinator,&lt;br/&gt;///                          used to tag the Nostr event.&lt;br/&gt;///&lt;br/&gt;/// # Returns&lt;br/&gt;///&lt;br/&gt;/// A `Result` containing the JSON string of the Nostr event if successful,&lt;br/&gt;/// or a `Box&amp;lt;dyn std::error::Error&amp;gt;` if an error occurs.&lt;br/&gt;pub fn create_signer_event(&lt;br/&gt;    _identifier: frost::Identifier,&lt;br/&gt;    signing_package: &amp;amp;frost::SigningPackage,&lt;br/&gt;    nonces: &amp;amp;frost::round1::SigningNonces,&lt;br/&gt;    key_package: &amp;amp;frost::keys::KeyPackage,&lt;br/&gt;    coordinator_pubkey: &amp;amp;str, // The Hex pubkey of the ROAST coordinator&lt;br/&gt;) -&amp;gt; Result&amp;lt;String, Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;&lt;br/&gt;    // 1. Generate the partial signature share (Round 2 of FROST)&lt;br/&gt;    // This share is the core cryptographic output from the signer.&lt;br/&gt;    let share = frost::round2::sign(signing_package, nonces, key_package)?;&lt;br/&gt;    let share_bytes = share.serialize();&lt;br/&gt;    let share_hex = hex::encode(share_bytes);&lt;br/&gt;&lt;br/&gt;    // 2. Create a Session ID to tag the event&lt;br/&gt;    // This ID is derived from the signing package hash, allowing the coordinator&lt;br/&gt;    // to correlate shares belonging to the same signing session.&lt;br/&gt;    let mut hasher = Sha256::new();&lt;br/&gt;    hasher.update(signing_package.serialize()?);&lt;br/&gt;    let session_id = hex::encode(hasher.finalize());&lt;br/&gt;&lt;br/&gt;    // 3. Construct the Nostr Event JSON (Simplified)&lt;br/&gt;    // This JSON represents the event that a signer would post to a relay.&lt;br/&gt;    // In a production ROAST system, the &amp;#39;content&amp;#39; field (the signature share)&lt;br/&gt;    // would be encrypted for the coordinator using NIP-44.&lt;br/&gt;    let event = serde_json::json!({&lt;br/&gt;        &amp;#34;kind&amp;#34;: 4, // Example: Using Kind 4 (Private Message), though custom Kinds could be used for Sovereign Stack.&lt;br/&gt;        &amp;#34;pubkey&amp;#34;: hex::encode(key_package.verifying_key().serialize()?.as_slice()), // Signer&amp;#39;s public key&lt;br/&gt;        &amp;#34;created_at&amp;#34;: 1712050000, // Example timestamp&lt;br/&gt;        &amp;#34;tags&amp;#34;: [&lt;br/&gt;            [&amp;#34;p&amp;#34;, coordinator_pubkey],       // &amp;#39;p&amp;#39; tag: Directs the event to the coordinator.&lt;br/&gt;            [&amp;#34;i&amp;#34;, session_id],               // &amp;#39;i&amp;#39; tag: Provides a session identifier for filtering/requests.&lt;br/&gt;            [&amp;#34;t&amp;#34;, &amp;#34;frost-signature-share&amp;#34;]   // &amp;#39;t&amp;#39; tag: A searchable label for the event type.&lt;br/&gt;        ],&lt;br/&gt;        &amp;#34;content&amp;#34;: share_hex, // The actual signature share (would be encrypted in production).&lt;br/&gt;        &amp;#34;id&amp;#34;: &amp;#34;...&amp;#34;, // Event ID (filled by relay upon publishing)&lt;br/&gt;        &amp;#34;sig&amp;#34;: &amp;#34;...&amp;#34; // Event signature (filled by relay upon publishing)&lt;br/&gt;    });&lt;br/&gt;&lt;br/&gt;    Ok(event.to_string())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn simulate_frost_mailbox_post_signer() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    use rand::thread_rng;&lt;br/&gt;    use std::collections::BTreeMap;&lt;br/&gt;    use frost_secp256k1_tr as frost;&lt;br/&gt;&lt;br/&gt;    // This example simulates a single signer&amp;#39;s role in a ROAST mailbox post workflow.&lt;br/&gt;    // The general workflow is:&lt;br/&gt;    // 1. Coordinator sends a request for signatures (e.g., on a BIP-64MOD proposal).&lt;br/&gt;    // 2. Signers receive the proposal, perform local verification.&lt;br/&gt;    // 3. Each signer generates their signature share and posts it (encrypted) to a&lt;br/&gt;    //    Nostr relay, targeting the coordinator&amp;#39;s mailbox.&lt;br/&gt;    // 4. The coordinator collects enough shares to aggregate the final signature.&lt;br/&gt;&lt;br/&gt;    let mut rng = thread_rng();&lt;br/&gt;    // For this example, we simulate a 2-of-2 threshold for simplicity.&lt;br/&gt;    let (max_signers, min_signers) = (2, 2);&lt;br/&gt;&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // 1. Key Generation (Simulated Trusted Dealer)&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // In a real distributed setup, this would be DKG. Here, a &amp;#34;trusted dealer&amp;#34;&lt;br/&gt;    // generates the shares and public key package.&lt;br/&gt;    let (shares, _pubkey_package) = frost::keys::generate_with_dealer(&lt;br/&gt;        max_signers,&lt;br/&gt;        min_signers,&lt;br/&gt;        frost::keys::IdentifierList::Default,&lt;br/&gt;        &amp;amp;mut rng,&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    // For a 2-of-2 scheme, we have two signers. Let&amp;#39;s pick signer 1.&lt;br/&gt;    let signer1_id = frost::Identifier::try_from(1 as u16)?;&lt;br/&gt;    let key_package1: frost::keys::KeyPackage = shares[&amp;amp;signer1_id].clone().try_into()?;&lt;br/&gt;&lt;br/&gt;    let signer2_id = frost::Identifier::try_from(2 as u16)?;&lt;br/&gt;    let key_package2: frost::keys::KeyPackage = shares[&amp;amp;signer2_id].clone().try_into()?;&lt;br/&gt;&lt;br/&gt;    // The message that is to be signed (e.g., a hash of a Git commit or a Nostr event ID).&lt;br/&gt;    let message = b&amp;#34;This is a test message for ROAST mailbox post.&amp;#34;;&lt;br/&gt;&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // 2. Round 1: Commitment Phase (Signer&amp;#39;s role)&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // Each signer generates nonces and commitments.&lt;br/&gt;    let (nonces1, comms1) = frost::round1::commit(key_package1.signing_share(), &amp;amp;mut rng);&lt;br/&gt;    let (nonces2, comms2) = frost::round1::commit(key_package2.signing_share(), &amp;amp;mut rng);&lt;br/&gt;    &lt;br/&gt;    // The coordinator collects these commitments. Here, we simulate by putting them in a BTreeMap.&lt;br/&gt;    let mut session_commitments = BTreeMap::new();&lt;br/&gt;    session_commitments.insert(signer1_id, comms1);&lt;br/&gt;    session_commitments.insert(signer2_id, comms2);&lt;br/&gt;&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // 3. Signing Package Creation (Coordinator&amp;#39;s role, simulated for context)&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // The coordinator combines the collected commitments and the message to be signed&lt;br/&gt;    // into a signing package, which is then sent back to the signers.&lt;br/&gt;    let signing_package = frost::SigningPackage::new(session_commitments, message);&lt;br/&gt;&lt;br/&gt;    // Dummy coordinator public key. In a real scenario, this would be the&lt;br/&gt;    // actual public key of the ROAST coordinator, used for event tagging&lt;br/&gt;    // and encryption (NIP-44).&lt;br/&gt;    let coordinator_pubkey_hex = &amp;#34;0000000000000000000000000000000000000000000000000000000000000001&amp;#34;;&lt;br/&gt;&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // 4. Create the Signer Event (Signer&amp;#39;s role)&lt;br/&gt;    ////////////////////////////////////////////////////////////////////////////&lt;br/&gt;    // We demonstrate for signer 1. Signer 2 would perform a similar action.&lt;br/&gt;    let event_json_signer1 = create_signer_event(&lt;br/&gt;        signer1_id,&lt;br/&gt;        &amp;amp;signing_package,&lt;br/&gt;        &amp;amp;nonces1,&lt;br/&gt;        &amp;amp;key_package1,&lt;br/&gt;        coordinator_pubkey_hex,&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;Generated Nostr Event for Signer 1 Mailbox Post:&lt;br/&gt;{}&amp;#34;, event_json_signer1);&lt;br/&gt;&lt;br/&gt;    // Similarly, Signer 2 would generate their event:&lt;br/&gt;    let event_json_signer2 = create_signer_event(&lt;br/&gt;        signer2_id,&lt;br/&gt;        &amp;amp;signing_package,&lt;br/&gt;        &amp;amp;nonces2,&lt;br/&gt;        &amp;amp;key_package2,&lt;br/&gt;        coordinator_pubkey_hex,&lt;br/&gt;    )?;&lt;br/&gt;    println!(&amp;#34;Generated Nostr Event for Signer 2 Mailbox Post:&lt;br/&gt;{}&amp;#34;, event_json_signer2);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}
    </content>
    <updated>2026-04-05T05:50:45Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqszy9gmz0g7yhmdspz08pmza9rnznvgu02k7glth9z9cc3vhkhkwtczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvq9050q</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqszy9gmz0g7yhmdspz08pmza9rnznvgu02k7glth9z9cc3vhkhkwtczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvq9050q" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    net::{Ipv4Addr, SocketAddr, SocketAddrV4},&lt;br/&gt;    time::Duration,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use nostr_browser_signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};&lt;br/&gt;&lt;br/&gt;/// The default socket address used for the NIP-07 signer proxy, set to&lt;br/&gt;/// localhost on port 51034.&lt;br/&gt;pub const DEFAULT_NIP07_PROXY_ADDR: SocketAddr =&lt;br/&gt;    SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 51034));&lt;br/&gt;&lt;br/&gt;/// How long to wait for the proxy response (3 minutes).&lt;br/&gt;pub const BROWSER_SIGNER_PROXY_TIMEOUT: Duration = Duration::from_secs(60 * 3);&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Represents the state used for CLI options.&lt;br/&gt;pub struct OptionsState {&lt;br/&gt;    /// The browser signer proxy, will be used if `--nip07` is enabled&lt;br/&gt;    pub browser_signer_proxy: BrowserSignerProxy,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl Default for OptionsState {&lt;br/&gt;    fn default() -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            browser_signer_proxy: default_browser_signer_proxy(),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Build the default browser signer proxy&lt;br/&gt;#[inline]&lt;br/&gt;fn default_browser_signer_proxy() -&amp;gt; BrowserSignerProxy {&lt;br/&gt;    BrowserSignerProxy::new(&lt;br/&gt;        BrowserSignerProxyOptions::default()&lt;br/&gt;            .timeout(BROWSER_SIGNER_PROXY_TIMEOUT)&lt;br/&gt;            .ip_addr(DEFAULT_NIP07_PROXY_ADDR.ip())&lt;br/&gt;            .port(DEFAULT_NIP07_PROXY_ADDR.port()),&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:50:42Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqszk8wwnkcdxterw38q8vy3dkcdw3lweuuwayr4ns0k8y4fzz45lvqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhs3rfj</id>
    
      <title type="html">#[cfg(all(not(debug_assertions), feature = &amp;#34;nostr&amp;#34;))] ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqszk8wwnkcdxterw38q8vy3dkcdw3lweuuwayr4ns0k8y4fzz45lvqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhs3rfj" />
    <content type="html">
      #[cfg(all(not(debug_assertions), feature = &amp;#34;nostr&amp;#34;))]&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() {&lt;br/&gt;    use std::fs;&lt;br/&gt;    use std::path::PathBuf;&lt;br/&gt;&lt;br/&gt;    let manifest_dir = PathBuf::from(std::env::var(&amp;#34;CARGO_MANIFEST_DIR&amp;#34;).unwrap());&lt;br/&gt;    let crate_src_path = manifest_dir.join(&amp;#34;src&amp;#34;).join(&amp;#34;online_relays_gps.csv&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // Only download if the file doesn&amp;#39;t exist or is empty&lt;br/&gt;    if !crate_src_path.exists() || fs::metadata(&amp;amp;crate_src_path).map(|m| m.len() == 0).unwrap_or(true) {&lt;br/&gt;        println!(&amp;#34;cargo:warning=Downloading online_relays_gps.csv...&amp;#34;);&lt;br/&gt;        let url = &amp;#34;&lt;a href=&#34;https://raw.githubusercontent.com/permissionlesstech/bitchat/main/relays/online_relays_gps.csv&amp;#34&#34;&gt;https://raw.githubusercontent.com/permissionlesstech/bitchat/main/relays/online_relays_gps.csv&amp;#34&lt;/a&gt;;;&lt;br/&gt;        match reqwest::get(url).await {&lt;br/&gt;            Ok(response) =&amp;gt; {&lt;br/&gt;                if response.status().is_success() {&lt;br/&gt;                    match response.text().await {&lt;br/&gt;                        Ok(content) =&amp;gt; {&lt;br/&gt;                            fs::write(&amp;amp;crate_src_path, content).expect(&amp;#34;Unable to write online_relays_gps.csv&amp;#34;);&lt;br/&gt;                            println!(&amp;#34;cargo:warning=Successfully downloaded online_relays_gps.csv to {:?}&amp;#34;, crate_src_path);&lt;br/&gt;                        },&lt;br/&gt;                        Err(e) =&amp;gt; {&lt;br/&gt;                            println!(&amp;#34;cargo:warning=Failed to get text from response: {}&amp;#34;, e);&lt;br/&gt;                        }&lt;br/&gt;                    }&lt;br/&gt;                } else {&lt;br/&gt;                    println!(&amp;#34;cargo:warning=Failed to download online_relays_gps.csv: HTTP status {}&amp;#34;, response.status());&lt;br/&gt;                }&lt;br/&gt;            },&lt;br/&gt;            Err(e) =&amp;gt; {&lt;br/&gt;                println!(&amp;#34;cargo:warning=Failed to fetch online_relays_gps.csv: {}&amp;#34;, e);&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(not(all(not(debug_assertions), feature = &amp;#34;nostr&amp;#34;)))]&lt;br/&gt;fn main() {&lt;br/&gt;    // Placeholder for when the nostr feature is not enabled or in debug mode&lt;br/&gt;    println!(&amp;#34;cargo:warning=Skipping online_relays_gps.csv download (nostr feature not enabled or debug mode)&amp;#34;);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:50:33Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsf8du5spng7jnld9xqxynjz5pw2znclvf2ymx48r6yl5amnttmxqczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvstsqm7</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsf8du5spng7jnld9xqxynjz5pw2znclvf2ymx48r6yl5amnttmxqczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvstsqm7" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Commands module&lt;br/&gt;pub mod commands;&lt;br/&gt;/// Common commands used by multiply commands&lt;br/&gt;pub mod common_commands;&lt;br/&gt;/// The CLI config&lt;br/&gt;pub mod config;&lt;br/&gt;/// Default lazy values for CLI arguments&lt;br/&gt;pub mod defaults;&lt;br/&gt;/// Macros for CLI application.&lt;br/&gt;pub mod macros;&lt;br/&gt;/// Represents the state used for CLI options.&lt;br/&gt;pub mod options_state;&lt;br/&gt;/// CLI arguments parsers&lt;br/&gt;pub mod parsers;&lt;br/&gt;/// CLI traits&lt;br/&gt;pub mod traits;&lt;br/&gt;/// Common helper types used throughout the CLI.&lt;br/&gt;pub mod types;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;use clap::Parser;&lt;br/&gt;use clap_verbosity_flag::Verbosity;&lt;br/&gt;use nostr::key::Keys;&lt;br/&gt;use nostr::key::SecretKey;&lt;br/&gt;use nostr_browser_signer_proxy::BrowserSignerProxy;&lt;br/&gt;use nostr_browser_signer_proxy::BrowserSignerProxyOptions;&lt;br/&gt;use nostr_keyring::KeyringError;&lt;br/&gt;use nostr_keyring::NostrKeyring;&lt;br/&gt;use types::RelayOrSet;&lt;br/&gt;&lt;br/&gt;pub use self::commands::*;&lt;br/&gt;pub use self::config::*;&lt;br/&gt;use self::traits::CommandRunner;&lt;br/&gt;use crate::cli::options_state::BROWSER_SIGNER_PROXY_TIMEOUT;&lt;br/&gt;use crate::error::N34Error;&lt;br/&gt;use crate::error::N34Result;&lt;br/&gt;use crate::nostr_utils::traits::NostrKeyringErrorUtils;&lt;br/&gt;&lt;br/&gt;/// Header message, used in the help message&lt;br/&gt;const HEADER: &amp;amp;str = r#&amp;#34;Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;License GNU GPL-3.0-or-later &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;&lt;br/&gt;This is free software: you are free to change and redistribute it.&lt;br/&gt;There is NO WARRANTY, to the extent permitted by law.&lt;br/&gt;&lt;br/&gt;Git repository: &lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git&amp;#34;#&#34;&gt;https://git.4rs.nl/awiteb/n34.git&amp;#34;#&lt;/a&gt;;&lt;br/&gt;&lt;br/&gt;/// Footer message, used in the help message&lt;br/&gt;const FOOTER: &amp;amp;str = r#&amp;#34;Please report bugs to &amp;lt;naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqauesksc39&amp;gt;.&amp;#34;#;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Name of the file storing the repository address&lt;br/&gt;pub const NOSTR_ADDRESS_FILE: &amp;amp;str = &amp;#34;nostr-address&amp;#34;;&lt;br/&gt;&lt;br/&gt;#[derive(Parser, Debug)]&lt;br/&gt;#[command(about, version, before_long_help = HEADER, after_long_help = FOOTER)]&lt;br/&gt;/// A command-line interface for interacting with NIP-34 and other Nostr&lt;br/&gt;/// code-related stuff.&lt;br/&gt;pub struct Cli {&lt;br/&gt;    #[command(flatten)]&lt;br/&gt;    pub options:   commands::CliOptions,&lt;br/&gt;    /// Controls the verbosity level of output&lt;br/&gt;    #[command(flatten)]&lt;br/&gt;    pub verbosity: Verbosity,&lt;br/&gt;    /// The subcommand to execute&lt;br/&gt;    #[command(subcommand)]&lt;br/&gt;    pub command:   commands::Commands,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl Cli {&lt;br/&gt;    /// Keyring service name of n34&lt;br/&gt;    pub const N34_KEYRING_SERVICE_NAME: &amp;amp;str = &amp;#34;n34&amp;#34;;&lt;br/&gt;    /// Keyring entry name of the n34 keypair&lt;br/&gt;    pub const N34_KEY_PAIR_ENTRY: &amp;amp;str = &amp;#34;n34_keypair&amp;#34;;&lt;br/&gt;    /// Keyring entry name of the user secret key&lt;br/&gt;    pub const USER_KEY_PAIR_ENTRY: &amp;amp;str = &amp;#34;user_keypair&amp;#34;;&lt;br/&gt;&lt;br/&gt;    /// Executes the command&lt;br/&gt;    pub async fn run(self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        self.command.run(self.options).await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the n34 keypair from the keyring or generates and stores a new one&lt;br/&gt;    /// if none exists.&lt;br/&gt;    pub fn n34_keypair() -&amp;gt; N34Result&amp;lt;Keys&amp;gt; {&lt;br/&gt;        let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);&lt;br/&gt;&lt;br/&gt;        match keyring.get(Self::N34_KEY_PAIR_ENTRY) {&lt;br/&gt;            Ok(keys) =&amp;gt; Ok(keys),&lt;br/&gt;            Err(nostr_keyring::Error::Keyring(KeyringError::NoEntry)) =&amp;gt; {&lt;br/&gt;                let new_keys = Keys::generate();&lt;br/&gt;                keyring.set(Self::N34_KEY_PAIR_ENTRY, &amp;amp;new_keys)?;&lt;br/&gt;                Ok(new_keys)&lt;br/&gt;            }&lt;br/&gt;            Err(err) =&amp;gt; Err(N34Error::Keyring(err)),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Retrieves the user&amp;#39;s keypair from the keyring. If no key exists and one&lt;br/&gt;    /// is provided, stores and returns it. If no key exists and none is&lt;br/&gt;    /// provided, returns an error.&lt;br/&gt;    pub fn user_keypair(secret_key: Option&amp;lt;SecretKey&amp;gt;) -&amp;gt; N34Result&amp;lt;Keys&amp;gt; {&lt;br/&gt;        let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);&lt;br/&gt;        let keyring_key = keyring.get(Self::USER_KEY_PAIR_ENTRY);&lt;br/&gt;&lt;br/&gt;        if let Err(ref err) = keyring_key&lt;br/&gt;            &amp;amp;&amp;amp; err.is_keyring_no_entry()&lt;br/&gt;            &amp;amp;&amp;amp; let Some(secret_key) = secret_key&lt;br/&gt;        {&lt;br/&gt;            let keypair = Keys::new(secret_key);&lt;br/&gt;            keyring.set(Self::USER_KEY_PAIR_ENTRY, &amp;amp;keypair)?;&lt;br/&gt;            return Ok(keypair);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        keyring_key.map_err(|err| {&lt;br/&gt;            if err.is_keyring_no_entry() {&lt;br/&gt;                N34Error::SecretKeyKeyringWithoutEntry&lt;br/&gt;            } else {&lt;br/&gt;                N34Error::Keyring(err)&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Processes the CLI configuration by applying fallback values from config if&lt;br/&gt;/// needed. Returns the processed Cli configuration if successful.&lt;br/&gt;pub fn post_cli(mut cli: Cli) -&amp;gt; N34Result&amp;lt;Cli&amp;gt; {&lt;br/&gt;    cli.options.pow = cli.options.pow.or(cli.options.config.pow);&lt;br/&gt;&lt;br/&gt;    if cli.options.relays.is_empty()&lt;br/&gt;        &amp;amp;&amp;amp; let Some(relays) = &amp;amp;cli.options.config.fallback_relays&lt;br/&gt;    {&lt;br/&gt;        cli.options.relays = relays.iter().cloned().map(RelayOrSet::Relay).collect();&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Automatically sets the signer based on the configuration if no signer&lt;br/&gt;    // is provided.&lt;br/&gt;    if !cli.options.nip07&lt;br/&gt;        &amp;amp;&amp;amp; cli.options.bunker_url.is_none()&lt;br/&gt;        &amp;amp;&amp;amp; (cli.options.secret_key.is_none() || cli.options.config.keyring_secret_key)&lt;br/&gt;    {&lt;br/&gt;        if let Some(addr) = cli.options.config.nip07 {&lt;br/&gt;            cli.options.nip07 = true;&lt;br/&gt;            cli.options.state.browser_signer_proxy = BrowserSignerProxy::new(&lt;br/&gt;                BrowserSignerProxyOptions::default()&lt;br/&gt;                    .timeout(BROWSER_SIGNER_PROXY_TIMEOUT)&lt;br/&gt;                    .ip_addr(addr.ip())&lt;br/&gt;                    .port(addr.port()),&lt;br/&gt;            );&lt;br/&gt;        } else if let Some(bunker_url) = &amp;amp;cli.options.config.bunker_url {&lt;br/&gt;            cli.options.bunker_url = Some(bunker_url.clone());&lt;br/&gt;        } else if cli.options.config.keyring_secret_key {&lt;br/&gt;            cli.options.secret_key = Some(&lt;br/&gt;                Cli::user_keypair(cli.options.secret_key)?&lt;br/&gt;                    .secret_key()&lt;br/&gt;                    .clone(),&lt;br/&gt;            );&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(cli)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:50:25Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsdauun38tw62vjc3rvsvn5v3ssy55klvw0auqkykgv04hw7afjdxqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvyvmvps</id>
    
      <title type="html">[package] name = &amp;#34;get_file_hash_core&amp;#34; version = { ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsdauun38tw62vjc3rvsvn5v3ssy55klvw0auqkykgv04hw7afjdxqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvyvmvps" />
    <content type="html">
      [package]&lt;br/&gt;name = &amp;#34;get_file_hash_core&amp;#34;&lt;br/&gt;version = { workspace = true }&lt;br/&gt;edition = { workspace = true }&lt;br/&gt;description = { workspace = true }&lt;br/&gt;license = { workspace = true }&lt;br/&gt;documentation = { workspace = true }&lt;br/&gt;homepage = { workspace = true }&lt;br/&gt;repository = { workspace = true }&lt;br/&gt;authors = { workspace = true }&lt;br/&gt;&lt;br/&gt;[features]&lt;br/&gt;nostr = [&amp;#34;dep:nostr&amp;#34;, &amp;#34;dep:nostr-sdk&amp;#34;, &amp;#34;dep:serde_json&amp;#34;, &amp;#34;dep:sha2&amp;#34;, &amp;#34;dep:hex&amp;#34;, &amp;#34;dep:reqwest&amp;#34;, &amp;#34;dep:tokio&amp;#34;, &amp;#34;dep:csv&amp;#34;, &amp;#34;dep:url&amp;#34;, &amp;#34;dep:frost-secp256k1-tr&amp;#34;, &amp;#34;dep:rand&amp;#34;]&lt;br/&gt;frost = [&amp;#34;dep:nostr&amp;#34;, &amp;#34;dep:nostr-sdk&amp;#34;, &amp;#34;dep:serde_json&amp;#34;, &amp;#34;dep:sha2&amp;#34;, &amp;#34;dep:hex&amp;#34;, &amp;#34;dep:reqwest&amp;#34;, &amp;#34;dep:tokio&amp;#34;, &amp;#34;dep:csv&amp;#34;, &amp;#34;dep:url&amp;#34;, &amp;#34;dep:frost-secp256k1-tr&amp;#34;, &amp;#34;dep:rand&amp;#34;]&lt;br/&gt;&lt;br/&gt;[dependencies]&lt;br/&gt;sha2 = { workspace = true, optional = true }&lt;br/&gt;nostr = { workspace = true, optional = true }&lt;br/&gt;serde_json = { workspace = true, optional = true }&lt;br/&gt;nostr-sdk = { workspace = true, optional = true }&lt;br/&gt;hex = { workspace = true, optional = true }&lt;br/&gt;csv = { workspace = true, optional = true }&lt;br/&gt;url = { workspace = true, optional = true }&lt;br/&gt;frost-secp256k1-tr = { workspace = true, optional = true }&lt;br/&gt;rand = { workspace = true, optional = true }&lt;br/&gt;&lt;br/&gt;[dev-dependencies]&lt;br/&gt;sha2 = { workspace = true }&lt;br/&gt;tempfile = { workspace = true }&lt;br/&gt;nostr = { workspace = true }&lt;br/&gt;nostr-sdk = { workspace = true }&lt;br/&gt;serde_json = { workspace = true }&lt;br/&gt;hex = { workspace = true }&lt;br/&gt;tokio = { workspace = true, features = [&amp;#34;macros&amp;#34;, &amp;#34;rt-multi-thread&amp;#34;] }&lt;br/&gt;csv = { workspace = true }&lt;br/&gt;url = { workspace = true }&lt;br/&gt;frost-secp256k1-tr = { workspace = true }&lt;br/&gt;serial_test = { workspace = true, features = [&amp;#34;test_logging&amp;#34;] }&lt;br/&gt;log = { workspace = true }&lt;br/&gt;&lt;br/&gt;[build-dependencies]&lt;br/&gt;reqwest = { workspace = true, features = [&amp;#34;json&amp;#34;], optional = true }&lt;br/&gt;tokio = { workspace = true, features = [&amp;#34;macros&amp;#34;, &amp;#34;rt-multi-thread&amp;#34;], optional = true }&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:50:19Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsqzqwenns23q39n57ak07zyqtwvuqw2lqgy4vrakhq9px6zyec54gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrx98qt</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsqzqwenns23q39n57ak07zyqtwvuqw2lqgy4vrakhq9px6zyec54gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrx98qt" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use super::traits::CommandRunner;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Returns whether the command runner type `T` requires relays.&lt;br/&gt;pub fn get_relays_state&amp;lt;T: CommandRunner&amp;gt;(_v: &amp;amp;T) -&amp;gt; bool {&lt;br/&gt;    T::NEED_RELAYS&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns whether the command runner type `T` requires a signer.&lt;br/&gt;pub fn get_signer_state&amp;lt;T: CommandRunner&amp;gt;(_v: &amp;amp;T) -&amp;gt; bool {&lt;br/&gt;    T::NEED_SIGNER&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Executes a command with required setup checks. The first parameter is the&lt;br/&gt;/// command to match on (often `self`), followed by options. Optional&lt;br/&gt;/// subcommands come next, and commands with arguments (after `&amp;amp;`) are listed&lt;br/&gt;/// last.&lt;br/&gt;#[macro_export]&lt;br/&gt;macro_rules! run_command {&lt;br/&gt;    ($command:ident, $options:ident, $($subcommands:ident)* &amp;amp; $($commands:ident)*) =&amp;gt; {&lt;br/&gt;        match $command {&lt;br/&gt;            $(&lt;br/&gt;                Self::$subcommands { subcommands } =&amp;gt; subcommands.run($options).await,&lt;br/&gt;            )*&lt;br/&gt;            $(&lt;br/&gt;                Self::$commands ( args ) =&amp;gt; {&lt;br/&gt;                    if $crate::cli::macros::get_relays_state(&amp;amp;args) {&lt;br/&gt;                        $options.ensure_relays()?;&lt;br/&gt;                    }&lt;br/&gt;                    if $crate::cli::macros::get_signer_state(&amp;amp;args) {&lt;br/&gt;                        $options.ensure_signer()?;&lt;br/&gt;                    }&lt;br/&gt;                    args.run($options).await&lt;br/&gt;                },&lt;br/&gt;            )*&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:50:13Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsqhrwu42mdhz2tfy45rjmvmyvxrsawea6n7d69p2vatjvq3wp252qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvknxazr</id>
    
      <title type="html">//! A simple command-line tool that calculates and displays the ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsqhrwu42mdhz2tfy45rjmvmyvxrsawea6n7d69p2vatjvq3wp252qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvknxazr" />
    <content type="html">
      //! A simple command-line tool that calculates and displays the SHA-256 hash of&lt;br/&gt;//! its own source file.&lt;br/&gt;//!&lt;br/&gt;//! This utility demonstrates how to use the `get_file_hash!` macro to obtain&lt;br/&gt;//! the hash of a specified file at compile time and incorporate it into runtime&lt;br/&gt;//! logic.&lt;br/&gt;use get_file_hash::{BUILD_HASH, CARGO_TOML_HASH, LIB_HASH};&lt;br/&gt;use get_file_hash_core::get_file_hash;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART0: &amp;amp;str = r##&amp;#34;# `get_file_hash` macro&lt;br/&gt;&lt;br/&gt;This project provides a Rust procedural macro, `get_file_hash!`, designed to compute the SHA-256 hash of a specified file at compile time. This hash is then embedded directly into your compiled executable. This feature is invaluable for:&lt;br/&gt;&lt;br/&gt;*   **Integrity Verification:** Ensuring the deployed code hasn&amp;#39;t been tampered with.&lt;br/&gt;*   **Versioning:** Embedding a unique identifier linked to the exact source code version.&lt;br/&gt;*   **Cache Busting:** Generating unique names for assets based on their content.&lt;br/&gt;&lt;br/&gt;## Project Structure&lt;br/&gt;&lt;br/&gt;*   `get_file_hash_core`: A foundational crate containing the `get_file_hash!` macro definition.&lt;br/&gt;*   `get_file_hash`: The main library crate that re-exports the macro.&lt;br/&gt;*   `src/bin/get_file_hash.rs`: An example executable demonstrating the macro&amp;#39;s usage by hashing its own source file and updating this `README.md`.&lt;br/&gt;*   `build.rs`: A build script that also utilizes the `get_file_hash!` macro to hash `Cargo.toml` during the build process.&lt;br/&gt;&lt;br/&gt;## Usage of `get_file_hash!` Macro&lt;br/&gt;&lt;br/&gt;To use the `get_file_hash!` macro, ensure you have `get_file_hash` (or `get_file_hash_core` for direct usage) as a dependency in your `Cargo.toml`.&lt;br/&gt;&lt;br/&gt;### Example&lt;br/&gt;&lt;br/&gt;```rust&lt;br/&gt;use get_file_hash::get_file_hash;&lt;br/&gt;use get_file_hash::CARGO_TOML_HASH;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;fn main() {&lt;br/&gt;    // The macro resolves the path relative to CARGO_MANIFEST_DIR&lt;br/&gt;    let readme_hash = get_file_hash!(&amp;#34;src/bin/readme.rs&amp;#34;);&lt;br/&gt;    let lib_hash = get_file_hash!(&amp;#34;src/lib.rs&amp;#34;);&lt;br/&gt;    println!(&amp;#34;The SHA-256 hash of src/lib.rs is: {}&amp;#34;, lib_hash);&lt;br/&gt;    println!(&amp;#34;The SHA-256 hash of src/bin/readme.rs is: {}&amp;#34;, readme_hash);&lt;br/&gt;    println!(&amp;#34;The SHA-256 hash of Cargo.toml is: {}&amp;#34;, CARGO_TOML_HASH);&lt;br/&gt;}&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;&amp;#34;##;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART1: &amp;amp;str = r&amp;#34;## Release&lt;br/&gt;## [`README.md`](./README.md)&lt;br/&gt;&lt;br/&gt;```bash&lt;br/&gt;cargo run --bin readme &amp;gt; README.md&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;## [`src/bin/readme.rs`](src/bin/readme.rs)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `src/bin/readme.rs`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART2: &amp;amp;str = r&amp;#34;##&lt;br/&gt;&lt;br/&gt;## [`build.rs`](build.rs)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `build.rs`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART3: &amp;amp;str = r&amp;#34;##&lt;br/&gt;&lt;br/&gt;## [`Cargo.toml`](Cargo.toml)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `Cargo.toml`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART4: &amp;amp;str = r&amp;#34;##&lt;br/&gt;&lt;br/&gt;## [`src/lib.rs`](src/lib.rs)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `src/lib.rs`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART_NIP34: &amp;amp;str = r&amp;#34;## NIP-34 Integration: Git Repository Events on Nostr&lt;br/&gt;&lt;br/&gt;This library provides a set of powerful macros and functions for integrating Git repository events with the Nostr protocol, adhering to the [NIP-34: Git Repositories on Nostr](&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/34.md&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/34.md&lt;/a&gt;) specification.&lt;br/&gt;&lt;br/&gt;These tools allow you to publish various Git-related events to Nostr relays, enabling decentralized tracking and collaboration for your code repositories.&lt;br/&gt;&lt;br/&gt;### Available NIP-34 Macros&lt;br/&gt;&lt;br/&gt;Each macro provides a convenient way to publish specific NIP-34 event kinds:&lt;br/&gt;&lt;br/&gt;*   [`repository_announcement!`](#repository_announcement)&lt;br/&gt;    *   Publishes a `Repository Announcement` event (Kind 30617) to announce a new or updated Git repository.&lt;br/&gt;*   [`publish_patch!`](#publish_patch)&lt;br/&gt;    *   Publishes a `Patch` event (Kind 1617) containing a Git patch (diff) for a specific commit.&lt;br/&gt;*   [`publish_pull_request!`](#publish_pull_request)&lt;br/&gt;    *   Publishes a `Pull Request` event (Kind 1618) to propose changes and facilitate code review.&lt;br/&gt;*   [`publish_pr_update!`](#publish_pr_update)&lt;br/&gt;    *   Publishes a `Pull Request Update` event (Kind 1619) to update an existing pull request.&lt;br/&gt;*   [`publish_repository_state!`](#publish_repository_state)&lt;br/&gt;    *   Publishes a `Repository State` event (Kind 1620) to announce the current state of a branch (e.g., its latest commit).&lt;br/&gt;*   [`publish_issue!`](#publish_issue)&lt;br/&gt;    *   Publishes an `Issue` event (Kind 1621) to report bugs, request features, or track tasks.&lt;br/&gt;&lt;br/&gt;### Running NIP-34 Examples&lt;br/&gt;&lt;br/&gt;To see these macros in action, navigate to the `examples/` directory and run each example individually with the `nostr` feature enabled:&lt;br/&gt;&lt;br/&gt;```bash&lt;br/&gt;cargo run --example repository_announcement --features nostr&lt;br/&gt;cargo run --example publish_patch --features nostr&lt;br/&gt;cargo run --example publish_pull_request --features nostr&lt;br/&gt;cargo run --example publish_pr_update --features nostr&lt;br/&gt;cargo run --example publish_repository_state --features nostr&lt;br/&gt;cargo run --example publish_issue --features nostr&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// The main entry point of the application.&lt;br/&gt;///&lt;br/&gt;/// This function calculates the SHA-256 hash of the `get_file_hash.rs` source&lt;br/&gt;/// file using a custom procedural macro and then prints the hash to the&lt;br/&gt;/// console. It also includes a basic integrity verification check.&lt;br/&gt;fn main() {&lt;br/&gt;    // Calculate the SHA-256 hash of the current file (`readme.rs`) at&lt;br/&gt;    // compile time. The `get_file_hash!` macro reads the file content and&lt;br/&gt;    // computes its hash.&lt;br/&gt;    let self_hash = get_file_hash!(&amp;#34;readme.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let status_message = if self_hash.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let build_message = if BUILD_HASH.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;    let cargo_message = if CARGO_TOML_HASH.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;    let lib_message = if LIB_HASH.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    print!(&amp;#34;{}{}{}&amp;#34;, README_TEMPLATE_PART0, README_TEMPLATE_PART1, README_TEMPLATE_PART_NIP34);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** {}&amp;#34;, self_hash);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, status_message);&lt;br/&gt;    //&lt;br/&gt;    print!(&amp;#34;{}&amp;#34;, README_TEMPLATE_PART2);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** {}&amp;#34;, BUILD_HASH);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, build_message);&lt;br/&gt;    //&lt;br/&gt;    print!(&amp;#34;{}&amp;#34;, README_TEMPLATE_PART3);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** {}&amp;#34;, CARGO_TOML_HASH);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, cargo_message);&lt;br/&gt;    //&lt;br/&gt;    print!(&amp;#34;{}&amp;#34;, README_TEMPLATE_PART4);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** {}&amp;#34;, LIB_HASH);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, lib_message);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:50:07Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsvk5z66atukgrrl0lla6v68rkafletdsdwxgz0t7hegxh6ghchfyqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvr52k8m</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsvk5z66atukgrrl0lla6v68rkafletdsdwxgz0t7hegxh6ghchfyqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvr52k8m" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::path::PathBuf;&lt;br/&gt;&lt;br/&gt;use crate::{cli::ConfigError, error::N34Result};&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Default config path&lt;br/&gt;pub fn config_path() -&amp;gt; N34Result&amp;lt;PathBuf&amp;gt; {&lt;br/&gt;    Ok(dirs::config_dir()&lt;br/&gt;        .ok_or(ConfigError::CanNotFindConfigPath)?&lt;br/&gt;        .join(&amp;#34;n34&amp;#34;)&lt;br/&gt;        .join(&amp;#34;config.toml&amp;#34;))&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:50:02Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsfqvfmm49m5xcy53h8zhgx0j033k0uf2nrgxa4qz3vht9l6gj50nczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvzd6kz6</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsfqvfmm49m5xcy53h8zhgx0j033k0uf2nrgxa4qz3vht9l6gj50nczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvzd6kz6" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Command line interface module&lt;br/&gt;use n34::cli;&lt;br/&gt;// /// N34 errors&lt;br/&gt;// use n34::error;&lt;br/&gt;// /// Nostr utils module&lt;br/&gt;// use n34::nostr_utils;&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    process::ExitCode,&lt;br/&gt;    sync::atomic::{AtomicBool, Ordering},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Parser;&lt;br/&gt;use clap_verbosity_flag::Verbosity;&lt;br/&gt;use tracing::Level;&lt;br/&gt;use tracing_subscriber::{Layer, filter, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;use self::cli::Cli;&lt;br/&gt;&lt;br/&gt;/// Whether the editor is currently open. Prevents logging while the editor is&lt;br/&gt;/// open.&lt;br/&gt;static EDITOR_OPEN: AtomicBool = AtomicBool::new(false);&lt;br/&gt;&lt;br/&gt;/// Configures the logging level based on the provided verbosity.&lt;br/&gt;///&lt;br/&gt;/// When verbosity is set to TRACE, includes file and line numbers in logs.&lt;br/&gt;fn set_log_level(verbosity: Verbosity) {&lt;br/&gt;    let is_trace = verbosity&lt;br/&gt;        .tracing_level()&lt;br/&gt;        .is_some_and(|l| l == tracing::Level::TRACE);&lt;br/&gt;&lt;br/&gt;    let logs_filter = filter::dynamic_filter_fn(move |m, _| {&lt;br/&gt;        // Disable all logs while editor is open&lt;br/&gt;        verbosity.tracing_level().unwrap_or(Level::ERROR) &amp;gt;= *m.level()&lt;br/&gt;            &amp;amp;&amp;amp; !EDITOR_OPEN.load(Ordering::Relaxed)&lt;br/&gt;    });&lt;br/&gt;&lt;br/&gt;    let logs_layer = tracing_subscriber::fmt::layer()&lt;br/&gt;        .with_file(is_trace)&lt;br/&gt;        .with_line_number(is_trace)&lt;br/&gt;        .without_time();&lt;br/&gt;    let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter));&lt;br/&gt;    tracing::subscriber::set_global_default(subscriber).ok();&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() -&amp;gt; ExitCode {&lt;br/&gt;    let cli = match cli::post_cli(Cli::parse()) {&lt;br/&gt;        Ok(cli) =&amp;gt; cli,&lt;br/&gt;        Err(err) =&amp;gt; {&lt;br/&gt;            eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;            return ExitCode::FAILURE;&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    set_log_level(cli.verbosity);&lt;br/&gt;&lt;br/&gt;    if let Err(err) = cli.run().await {&lt;br/&gt;        tracing::error!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;        return err.exit_code();&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    ExitCode::SUCCESS&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:55Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsf49tvajfleuvgpsyu5cde92ng4kl6ary7qf8xdalvm4w05kqt6gszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfeg5vx</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsf49tvajfleuvgpsyu5cde92ng4kl6ary7qf8xdalvm4w05kqt6gszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfeg5vx" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{collections::HashSet, fs, net::SocketAddr, path::PathBuf};&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    nips::{nip19::Nip19Coordinate, nip46::NostrConnectURI},&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::traits::{MutRepoRelaySetsExt, RepoRelaySetsExt},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Errors that can occur when working with configuration files.&lt;br/&gt;#[derive(thiserror::Error, Debug)]&lt;br/&gt;pub enum ConfigError {&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Could not determine the default config path: both `$XDG_CONFIG_HOME` and `$HOME` \&lt;br/&gt;         environment variables are missing or unset.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    CanNotFindConfigPath,&lt;br/&gt;    #[error(&amp;#34;Couldn&amp;#39;t read the config file: {0}&amp;#34;)]&lt;br/&gt;    ReadFile(std::io::Error),&lt;br/&gt;    #[error(&amp;#34;Couldn&amp;#39;t write in the config file: {0}&amp;#34;)]&lt;br/&gt;    WriteFile(std::io::Error),&lt;br/&gt;    #[error(&amp;#34;Couldn&amp;#39;t serialize the config. This is a bug, please report it: {0}&amp;#34;)]&lt;br/&gt;    Serialize(toml::ser::Error),&lt;br/&gt;    #[error(&amp;#34;Failed to parse the config file: {0}&amp;#34;)]&lt;br/&gt;    ParseFile(toml::de::Error),&lt;br/&gt;    #[error(&amp;#34;Duplicate configuration set name detected: &amp;#39;{0}&amp;#39;. Each set  must have a unique name.&amp;#34;)]&lt;br/&gt;    SetDuplicateName(String),&lt;br/&gt;    #[error(&amp;#34;No set with the given name `{0}`&amp;#34;)]&lt;br/&gt;    SetNotFound(String),&lt;br/&gt;    #[error(&amp;#34;You can&amp;#39;t create an new empty set.&amp;#34;)]&lt;br/&gt;    NewEmptySet,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Configuration for the command-line interface.&lt;br/&gt;#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)]&lt;br/&gt;pub struct CliConfig {&lt;br/&gt;    /// Path to the configuration file (not serialized)&lt;br/&gt;    #[serde(skip)]&lt;br/&gt;    path:                   PathBuf,&lt;br/&gt;    /// Groups of repositories and relays.&lt;br/&gt;    #[serde(default, skip_serializing_if = &amp;#34;Vec::is_empty&amp;#34;)]&lt;br/&gt;    pub sets:               Vec&amp;lt;RepoRelaySet&amp;gt;,&lt;br/&gt;    /// The default PoW difficulty&lt;br/&gt;    #[serde(skip_serializing_if = &amp;#34;Option::is_none&amp;#34;)]&lt;br/&gt;    pub pow:                Option&amp;lt;u8&amp;gt;,&lt;br/&gt;    /// List of fallback relays used if no fallback relays was provided.&lt;br/&gt;    #[serde(skip_serializing_if = &amp;#34;Option::is_none&amp;#34;)]&lt;br/&gt;    pub fallback_relays:    Option&amp;lt;Vec&amp;lt;RelayUrl&amp;gt;&amp;gt;,&lt;br/&gt;    /// Default Nostr bunker URL used for signing events.&lt;br/&gt;    #[serde(&lt;br/&gt;        default,&lt;br/&gt;        skip_serializing_if = &amp;#34;Option::is_none&amp;#34;,&lt;br/&gt;        deserialize_with = &amp;#34;super::parsers::de_bunker_url&amp;#34;,&lt;br/&gt;        serialize_with = &amp;#34;super::parsers::ser_bunker_url&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    pub bunker_url:         Option&amp;lt;NostrConnectURI&amp;gt;,&lt;br/&gt;    /// Whether to use the system keyring to store the secret key.&lt;br/&gt;    #[serde(default)]&lt;br/&gt;    pub keyring_secret_key: bool,&lt;br/&gt;    /// Signs events using the browser&amp;#39;s NIP-07 extension.&lt;br/&gt;    #[serde(default)]&lt;br/&gt;    pub nip07:              Option&amp;lt;SocketAddr&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// A named group of repositories and relays.&lt;br/&gt;#[derive(serde::Serialize, serde::Deserialize, Default, Clone, Debug)]&lt;br/&gt;pub struct RepoRelaySet {&lt;br/&gt;    /// Unique identifier for this group.&lt;br/&gt;    pub name:   String,&lt;br/&gt;    /// Repository addresses in this group.&lt;br/&gt;    #[serde(&lt;br/&gt;        default,&lt;br/&gt;        skip_serializing_if = &amp;#34;HashSet::is_empty&amp;#34;,&lt;br/&gt;        serialize_with = &amp;#34;super::parsers::ser_naddrs&amp;#34;,&lt;br/&gt;        deserialize_with = &amp;#34;super::parsers::de_naddrs&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    pub naddrs: HashSet&amp;lt;Nip19Coordinate&amp;gt;,&lt;br/&gt;    /// Relay URLs in this group.&lt;br/&gt;    #[serde(default, skip_serializing_if = &amp;#34;HashSet::is_empty&amp;#34;)]&lt;br/&gt;    pub relays: HashSet&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CliConfig {&lt;br/&gt;    /// Reads and parse a TOML config file from the given path, creating it if&lt;br/&gt;    /// missing.&lt;br/&gt;    pub fn load(file_path: PathBuf) -&amp;gt; N34Result&amp;lt;Self&amp;gt; {&lt;br/&gt;        tracing::info!(path = %file_path.display(), &amp;#34;Loading configuration from file&amp;#34;);&lt;br/&gt;        // Make sure the file is exist&lt;br/&gt;        if let Some(parent) = file_path.parent()&lt;br/&gt;            &amp;amp;&amp;amp; !parent.exists()&lt;br/&gt;        {&lt;br/&gt;            fs::create_dir_all(parent)?;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let _ = fs::File::create_new(&amp;amp;file_path);&lt;br/&gt;&lt;br/&gt;        let mut config: Self =&lt;br/&gt;            toml::from_str(&amp;amp;fs::read_to_string(&amp;amp;file_path).map_err(ConfigError::ReadFile)?)&lt;br/&gt;                .map_err(ConfigError::ParseFile)?;&lt;br/&gt;        config.path = file_path;&lt;br/&gt;&lt;br/&gt;        config.post_sets()?;&lt;br/&gt;&lt;br/&gt;        Ok(config)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Dump the config as toml in a file&lt;br/&gt;    pub fn dump(mut self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        tracing::debug!(config = ?self, &amp;#34;Writing configuration to {}&amp;#34;, self.path.display());&lt;br/&gt;        self.post_sets()?;&lt;br/&gt;&lt;br/&gt;        fs::write(&lt;br/&gt;            &amp;amp;self.path,&lt;br/&gt;            toml::to_string_pretty(&amp;amp;self).map_err(ConfigError::Serialize)?,&lt;br/&gt;        )&lt;br/&gt;        .map_err(ConfigError::WriteFile)?;&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Performs post-processing validation on the sets after loading or before&lt;br/&gt;    /// dumping.&lt;br/&gt;    fn post_sets(&amp;amp;mut self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        self.sets.as_slice().ensure_names()?;&lt;br/&gt;        self.sets.dedup_naddrs();&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl RepoRelaySet {&lt;br/&gt;    /// Create a new [`RepoRelaySet`]&lt;br/&gt;    pub fn new(&lt;br/&gt;        name: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;        naddrs: impl IntoIterator&amp;lt;Item = Nip19Coordinate&amp;gt;,&lt;br/&gt;        relays: impl IntoIterator&amp;lt;Item = RelayUrl&amp;gt;,&lt;br/&gt;    ) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            name:   name.into(),&lt;br/&gt;            naddrs: HashSet::from_iter(naddrs),&lt;br/&gt;            relays: HashSet::from_iter(relays),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Removes duplicate repository addresses by comparing their coordinates,&lt;br/&gt;    /// ignoring embedded relays.&lt;br/&gt;    pub fn dedup_naddrs(&amp;amp;mut self) {&lt;br/&gt;        let mut seen = HashSet::new();&lt;br/&gt;        self.naddrs.retain(|n| seen.insert(n.coordinate.clone()));&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:45Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqst362axlgsue355k37hjm88k9sstudsxsfs5e50n2l9sm2gwlhx7szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvseenky</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqst362axlgsue355k37hjm88k9sstudsxsfs5e50n2l9sm2gwlhx7szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvseenky" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{net::SocketAddr, process::ExitCode, sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::Extension;&lt;br/&gt;use hyper::{Method, header};&lt;br/&gt;use tokio::signal;&lt;br/&gt;use tower_http::{&lt;br/&gt;    cors,&lt;br/&gt;    decompression::RequestDecompressionLayer,&lt;br/&gt;    trace::{DefaultMakeSpan, TraceLayer},&lt;br/&gt;};&lt;br/&gt;use tracing::level_filters::LevelFilter;&lt;br/&gt;use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;/// Relay endpoints&lt;br/&gt;use n34_relay::endpoints;&lt;br/&gt;/// Relay errors.&lt;br/&gt;use n34_relay::errors;&lt;br/&gt;// /// Extension traits&lt;br/&gt;// use n34_relay::ext_traits;&lt;br/&gt;/// GRASP git server&lt;br/&gt;use n34_relay::git_server;&lt;br/&gt;// /// Relay pathes.&lt;br/&gt;// use n34_relay::pathes;&lt;br/&gt;// /// Raw axum websocket&lt;br/&gt;// use n34_relay::raw_websocket;&lt;br/&gt;/// Our relay.&lt;br/&gt;use n34_relay::relay;&lt;br/&gt;/// Relay configuration.&lt;br/&gt;use n34_relay::relay_config;&lt;br/&gt;/// Router state&lt;br/&gt;use n34_relay::router_state;&lt;br/&gt;/// Some useful utils.&lt;br/&gt;use n34_relay::utils;&lt;br/&gt;&lt;br/&gt;use self::{errors::RelayResult, relay_config::RelayConfig, router_state::RouterState};&lt;br/&gt;&lt;br/&gt;/// Sets up default logging with two outputs, stderr and a log file.&lt;br/&gt;///&lt;br/&gt;/// Log level for stderr is controlled by `RUST_LOG` environment variable,&lt;br/&gt;/// defaults to `ERROR`. The log file always uses `TRACE` level.&lt;br/&gt;fn setup_logs() -&amp;gt; errors::RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    tracing::subscriber::set_global_default(&lt;br/&gt;        tracing_subscriber::registry()&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(true)&lt;br/&gt;                    .with_writer(std::io::stderr)&lt;br/&gt;                    .without_time()&lt;br/&gt;                    .with_filter(EnvFilter::from_default_env()),&lt;br/&gt;            )&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(false)&lt;br/&gt;                    .with_writer(utils::logs_file()?)&lt;br/&gt;                    .with_file(false)&lt;br/&gt;                    .with_line_number(false)&lt;br/&gt;                    .with_filter(LevelFilter::TRACE),&lt;br/&gt;            ),&lt;br/&gt;    )&lt;br/&gt;    .ok();&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn shutdown_signal() {&lt;br/&gt;    let ctrl_c = async {&lt;br/&gt;        signal::ctrl_c()&lt;br/&gt;            .await&lt;br/&gt;            .expect(&amp;#34;Failed to install CTRL&#43;C handler&amp;#34;);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(unix)]&lt;br/&gt;    let terminate = async {&lt;br/&gt;        use tokio::signal::unix::SignalKind;&lt;br/&gt;&lt;br/&gt;        signal::unix::signal(SignalKind::terminate())&lt;br/&gt;            .expect(&amp;#34;Failed to create SIGTERM handler&amp;#34;)&lt;br/&gt;            .recv()&lt;br/&gt;            .await;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(not(unix))]&lt;br/&gt;    let terminate = std::future::pending::&amp;lt;()&amp;gt;();&lt;br/&gt;&lt;br/&gt;    tokio::select! {&lt;br/&gt;        _ = ctrl_c =&amp;gt; {},&lt;br/&gt;        _ = terminate =&amp;gt; {},&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn try_main() -&amp;gt; RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    setup_logs()?;&lt;br/&gt;    let config = Arc::new(RelayConfig::reload()?);&lt;br/&gt;    let relay_db = config.get_relay_db().await?;&lt;br/&gt;    let n34_relay = Arc::new(relay::build_relay(Arc::clone(&amp;amp;config), Arc::clone(&amp;amp;relay_db)).await);&lt;br/&gt;    let addr = SocketAddr::new(config.net.ip, config.net.port);&lt;br/&gt;&lt;br/&gt;    tracing::debug!(&amp;#34;Running relay with configuration: {config:#?}&amp;#34;);&lt;br/&gt;    tracing::info!(&amp;#34;Relay is running at `{}`&amp;#34;, n34_relay.url().await);&lt;br/&gt;&lt;br/&gt;    let mut app = axum::Router::new()&lt;br/&gt;        // main handler. GET and POST&lt;br/&gt;        .route(&amp;#34;/&amp;#34;, axum::routing::get(endpoints::main_handler).post(endpoints::main_handler));&lt;br/&gt;&lt;br/&gt;    if config.grasp.enable {&lt;br/&gt;        tracing::info!(&amp;#34;Git server is running&amp;#34;);&lt;br/&gt;        app = app.merge(git_server::router(&amp;amp;config));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    app = app&lt;br/&gt;        // enable cross-origin access&lt;br/&gt;        .route(&lt;br/&gt;            &amp;#34;/&amp;#34;,&lt;br/&gt;            axum::routing::options(|| async { hyper::StatusCode::NO_CONTENT }),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            cors::CorsLayer::new()&lt;br/&gt;                .allow_origin(cors::Any)&lt;br/&gt;                .allow_methods([Method::GET, Method::POST])&lt;br/&gt;                .allow_headers([header::CONTENT_TYPE]),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            TraceLayer::new_for_http()&lt;br/&gt;                .make_span_with(DefaultMakeSpan::default().include_headers(true)),&lt;br/&gt;        )&lt;br/&gt;        .layer(RequestDecompressionLayer::new())&lt;br/&gt;        .layer(Extension(Arc::new(RouterState::new(&lt;br/&gt;            config, n34_relay, relay_db,&lt;br/&gt;        ))));&lt;br/&gt;&lt;br/&gt;    axum::serve(&lt;br/&gt;        tokio::net::TcpListener::bind(addr).await?,&lt;br/&gt;        app.into_make_service_with_connect_info::&amp;lt;SocketAddr&amp;gt;(),&lt;br/&gt;    )&lt;br/&gt;    .with_graceful_shutdown(shutdown_signal())&lt;br/&gt;    .await?;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() -&amp;gt; ExitCode {&lt;br/&gt;    if let Err(err) = try_main().await {&lt;br/&gt;        eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;        return ExitCode::FAILURE;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    tracing::info!(&amp;#34;Exited gracefully without any errors&amp;#34;);&lt;br/&gt;    ExitCode::SUCCESS&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:44Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsvfztf4r3hsrws63x5t8ye34yyngd6z5lxp6lllk84xtfgkfaxy4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv587a3u</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsvfztf4r3hsrws63x5t8ye34yyngd6z5lxp6lllk84xtfgkfaxy4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv587a3u" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{iter, str::FromStr, sync::Arc};&lt;br/&gt;&lt;br/&gt;use either::Either;&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventBuilder, EventId, Kind, Tag, TagKind},&lt;br/&gt;    filter::Filter,&lt;br/&gt;    hashes::sha1::Hash as Sha1Hash,&lt;br/&gt;    nips::{nip10::Marker, nip19::ToBech32},&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use super::{&lt;br/&gt;    issue::IssueStatus,&lt;br/&gt;    patch::PatchStatus,&lt;br/&gt;    types::{NaddrOrSet, NostrEvent},&lt;br/&gt;};&lt;br/&gt;use crate::{&lt;br/&gt;    cli::traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;    nostr_utils::{NostrClient, traits::NaddrsUtils, utils},&lt;br/&gt;};&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, patch::GitPatch},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;    nostr_utils::traits::{GitIssueUtils, GitPatchUtils, ReposUtils},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Updates the issue&amp;#39;s status to `new_status` after validating it with&lt;br/&gt;/// `check_fn`.&lt;br/&gt;pub async fn issue_status_command(&lt;br/&gt;    options: CliOptions,&lt;br/&gt;    issue_id: NostrEvent,&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    new_status: IssueStatus,&lt;br/&gt;    check_fn: impl FnOnce(&amp;amp;IssueStatus) -&amp;gt; N34Result&amp;lt;()&amp;gt;,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;    let naddrs = utils::naddrs_or_file(&lt;br/&gt;        naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;        &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;    )?;&lt;br/&gt;    let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;    let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;    let user_pubk = client.pubkey().await?;&lt;br/&gt;    client&lt;br/&gt;        .add_relays(&amp;amp;[naddrs.extract_relays(), issue_id.relays].concat())&lt;br/&gt;        .await;&lt;br/&gt;&lt;br/&gt;    let owners = naddrs.extract_owners();&lt;br/&gt;    let coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;    let repos = client.fetch_repos(&amp;amp;coordinates).await?;&lt;br/&gt;    let maintainers = repos.extract_maintainers();&lt;br/&gt;    let relay_hint = repos.extract_relays().first().cloned();&lt;br/&gt;    client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;    let issue_event = client&lt;br/&gt;        .fetch_event(Filter::new().id(issue_id.event_id))&lt;br/&gt;        .await?&lt;br/&gt;        .ok_or(N34Error::CanNotFoundIssue)?;&lt;br/&gt;&lt;br/&gt;    let issue_status = client&lt;br/&gt;        .fetch_issue_status(&lt;br/&gt;            issue_id.event_id,&lt;br/&gt;            [maintainers.as_slice(), &amp;amp;[issue_event.pubkey], &amp;amp;owners].concat(),&lt;br/&gt;        )&lt;br/&gt;        .await?;&lt;br/&gt;&lt;br/&gt;    check_fn(&amp;amp;issue_status)?;&lt;br/&gt;&lt;br/&gt;    let status_event = EventBuilder::new(new_status.kind(), &amp;#34;&amp;#34;)&lt;br/&gt;        .pow(options.pow.unwrap_or_default())&lt;br/&gt;        .tag(utils::event_reply_tag(&lt;br/&gt;            &amp;amp;issue_id.event_id,&lt;br/&gt;            relay_hint.as_ref(),&lt;br/&gt;            Marker::Root,&lt;br/&gt;        ))&lt;br/&gt;        .tag(Tag::public_key(issue_event.pubkey))&lt;br/&gt;        .tags(maintainers.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;        .tags(owners.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;        .tags(&lt;br/&gt;            coordinates&lt;br/&gt;                .into_iter()&lt;br/&gt;                .map(|c| Tag::coordinate(c, relay_hint.clone())),&lt;br/&gt;        )&lt;br/&gt;        .dedup_tags()&lt;br/&gt;        .build(user_pubk);&lt;br/&gt;&lt;br/&gt;    let event_id = status_event.id.expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;    let user_relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;    let write_relays = [&lt;br/&gt;        relays,&lt;br/&gt;        naddrs.extract_relays(),&lt;br/&gt;        repos.extract_relays(),&lt;br/&gt;        utils::add_write_relays(user_relays_list.as_ref()),&lt;br/&gt;        client.read_relays_from_user(issue_event.pubkey).await,&lt;br/&gt;        client&lt;br/&gt;            .read_relays_from_users(&amp;amp;[maintainers, owners].concat())&lt;br/&gt;            .await,&lt;br/&gt;    ]&lt;br/&gt;    .concat();&lt;br/&gt;&lt;br/&gt;    let success = client&lt;br/&gt;        .send_event_to(status_event, user_relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;        .await?;&lt;br/&gt;    let nevent = utils::new_nevent(event_id, &amp;amp;success)?;&lt;br/&gt;    println!(&amp;#34;Issue status created: {nevent}&amp;#34;);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Updates the patch&amp;#39;s status to `new_status` after validating it with&lt;br/&gt;/// `check_fn`.&lt;br/&gt;pub async fn patch_status_command(&lt;br/&gt;    options: CliOptions,&lt;br/&gt;    patch_id: NostrEvent,&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    new_status: PatchStatus,&lt;br/&gt;    merge_or_applied_commits: Option&amp;lt;Either&amp;lt;Sha1Hash, Vec&amp;lt;Sha1Hash&amp;gt;&amp;gt;&amp;gt;,&lt;br/&gt;    merge_or_applied_patches: Vec&amp;lt;EventId&amp;gt;,&lt;br/&gt;    check_fn: impl FnOnce(&amp;amp;PatchStatus) -&amp;gt; N34Result&amp;lt;()&amp;gt;,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;    let naddrs = utils::naddrs_or_file(&lt;br/&gt;        naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;        &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;    )?;&lt;br/&gt;    let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;    let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;    let user_pubk = client.pubkey().await?;&lt;br/&gt;    client&lt;br/&gt;        .add_relays(&amp;amp;[naddrs.extract_relays(), patch_id.relays].concat())&lt;br/&gt;        .await;&lt;br/&gt;&lt;br/&gt;    let owners = naddrs.extract_owners();&lt;br/&gt;    let coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;    let repos = client.fetch_repos(&amp;amp;coordinates).await?;&lt;br/&gt;    let maintainers = repos.extract_maintainers();&lt;br/&gt;    let relay_hint = repos.extract_relays().first().cloned();&lt;br/&gt;    client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;    let patch_event = client.fetch_patch(patch_id.event_id).await?;&lt;br/&gt;&lt;br/&gt;    if patch_event.is_revision_patch() &amp;amp;&amp;amp; !new_status.is_merged_or_applied() {&lt;br/&gt;        return Err(N34Error::InvalidStatus(&lt;br/&gt;            &amp;#34;Invalid action for patch revision. Only &amp;#39;apply&amp;#39; or &amp;#39;merge&amp;#39; are allowed, &amp;#39;open&amp;#39;, \&lt;br/&gt;             &amp;#39;close&amp;#39;, and &amp;#39;draft&amp;#39; are not supported.&amp;#34;&lt;br/&gt;                .to_owned(),&lt;br/&gt;        ));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let (root_patch, root_revision) = get_patch_root_revision(&amp;amp;patch_event)?;&lt;br/&gt;    let patch_status = client&lt;br/&gt;        .fetch_patch_status(&lt;br/&gt;            root_patch,&lt;br/&gt;            root_revision,&lt;br/&gt;            [maintainers.as_slice(), &amp;amp;[patch_event.pubkey], &amp;amp;owners].concat(),&lt;br/&gt;        )&lt;br/&gt;        .await?;&lt;br/&gt;&lt;br/&gt;    check_fn(&amp;amp;patch_status)?;&lt;br/&gt;&lt;br/&gt;    let mut status_builder = EventBuilder::new(new_status.kind(), &amp;#34;&amp;#34;)&lt;br/&gt;        .pow(options.pow.unwrap_or_default())&lt;br/&gt;        .tag(utils::event_reply_tag(&lt;br/&gt;            &amp;amp;root_patch,&lt;br/&gt;            relay_hint.as_ref(),&lt;br/&gt;            Marker::Root,&lt;br/&gt;        ))&lt;br/&gt;        .tag(Tag::public_key(patch_event.pubkey))&lt;br/&gt;        .tags(maintainers.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;        .tags(owners.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;        .tags(&lt;br/&gt;            coordinates&lt;br/&gt;                .into_iter()&lt;br/&gt;                .map(|c| Tag::coordinate(c, relay_hint.clone())),&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;    if new_status.is_merged_or_applied() {&lt;br/&gt;        if let Some(merge_commit) = merge_or_applied_commits&lt;br/&gt;            .as_ref()&lt;br/&gt;            .and_then(|e| e.as_ref().left())&lt;br/&gt;        {&lt;br/&gt;            let commit = merge_commit.to_string();&lt;br/&gt;            status_builder = status_builder&lt;br/&gt;                .tag(Tag::custom(&lt;br/&gt;                    TagKind::custom(&amp;#34;merge-commit&amp;#34;),&lt;br/&gt;                    iter::once(&amp;amp;commit),&lt;br/&gt;                ))&lt;br/&gt;                .tag(Tag::reference(commit));&lt;br/&gt;        } else if let Some(applied_commits) = merge_or_applied_commits.and_then(|e| e.right()) {&lt;br/&gt;            let commits = applied_commits&lt;br/&gt;                .iter()&lt;br/&gt;                .map(ToString::to_string)&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;            status_builder = status_builder&lt;br/&gt;                .tag(Tag::custom(TagKind::custom(&amp;#34;applied-as-commits&amp;#34;), &amp;amp;commits))&lt;br/&gt;                .tags(commits.into_iter().map(Tag::reference));&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        if let Some(root_revision) = root_revision {&lt;br/&gt;            status_builder = status_builder.tag(utils::event_reply_tag(&lt;br/&gt;                &amp;amp;root_revision,&lt;br/&gt;                relay_hint.as_ref(),&lt;br/&gt;                Marker::Reply,&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        if !merge_or_applied_patches.is_empty() {&lt;br/&gt;            status_builder = status_builder.tags(&lt;br/&gt;                build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)&lt;br/&gt;                    .await,&lt;br/&gt;            );&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let status_event = status_builder.dedup_tags().build(user_pubk);&lt;br/&gt;&lt;br/&gt;    let event_id = status_event.id.expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;    let user_relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;    let write_relays = [&lt;br/&gt;        relays,&lt;br/&gt;        naddrs.extract_relays(),&lt;br/&gt;        repos.extract_relays(),&lt;br/&gt;        utils::add_write_relays(user_relays_list.as_ref()),&lt;br/&gt;        client.read_relays_from_user(patch_event.pubkey).await,&lt;br/&gt;        client&lt;br/&gt;            .read_relays_from_users(&amp;amp;[maintainers, owners].concat())&lt;br/&gt;            .await,&lt;br/&gt;    ]&lt;br/&gt;    .concat();&lt;br/&gt;&lt;br/&gt;    let success = client&lt;br/&gt;        .send_event_to(status_event, user_relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;        .await?;&lt;br/&gt;    let nevent = utils::new_nevent(event_id, &amp;amp;success)?;&lt;br/&gt;    println!(&amp;#34;Patch status created: {nevent}&amp;#34;);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Fetch and display patches and issues for given repositories.&lt;br/&gt;/// If `list_patches` is true, lists patches instead of issues.&lt;br/&gt;/// `limit` controls the maximum number of items to fetch.&lt;br/&gt;pub async fn list_patches_and_issues(&lt;br/&gt;    options: CliOptions,&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    list_patches: bool,&lt;br/&gt;    limit: usize,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;    let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(&lt;br/&gt;        naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;        &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;    )?)?;&lt;br/&gt;&lt;br/&gt;    let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;    let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;    client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;    let coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;    let repos = client.fetch_repos(&amp;amp;coordinates).await?;&lt;br/&gt;    let authorized_pubkeys = [naddrs.extract_owners(), repos.extract_maintainers()].concat();&lt;br/&gt;    client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;    // This helps discover issues and their status.&lt;br/&gt;    client&lt;br/&gt;        .add_relays(&amp;amp;client.read_relays_from_users(&amp;amp;authorized_pubkeys).await)&lt;br/&gt;        .await;&lt;br/&gt;&lt;br/&gt;    let kind = if list_patches {&lt;br/&gt;        Kind::GitPatch&lt;br/&gt;    } else {&lt;br/&gt;        Kind::GitIssue&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let mut filter = Filter::new()&lt;br/&gt;        .coordinates(coordinates.iter())&lt;br/&gt;        .kind(kind)&lt;br/&gt;        .limit(limit);&lt;br/&gt;&lt;br/&gt;    if list_patches {&lt;br/&gt;        filter = filter.hashtag(&amp;#34;root&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let arc_client = Arc::new(client);&lt;br/&gt;    // Events are sorted by kind in ascending order:&lt;br/&gt;    // 1630 (Open), 1631 (Resolved/Applied), 1632 (Closed), 1633 (Draft)&lt;br/&gt;    let events = utils::sort_by_key(&lt;br/&gt;        future::join_all(&lt;br/&gt;            arc_client&lt;br/&gt;                .fetch_events(filter)&lt;br/&gt;                .await?&lt;br/&gt;                .into_iter()&lt;br/&gt;                .take(limit)&lt;br/&gt;                .map(|event| {&lt;br/&gt;                    let c = arc_client.clone();&lt;br/&gt;                    let keys = authorized_pubkeys.clone();&lt;br/&gt;                    async move {&lt;br/&gt;                        let status = if list_patches {&lt;br/&gt;                            let (root, root_revision) = get_patch_root_revision(&amp;amp;event)?;&lt;br/&gt;                            c.fetch_patch_status(&lt;br/&gt;                                root,&lt;br/&gt;                                root_revision,&lt;br/&gt;                                [keys.as_slice(), &amp;amp;[event.pubkey]].concat(),&lt;br/&gt;                            )&lt;br/&gt;                            .await&lt;br/&gt;                            .map(Either::Left)?&lt;br/&gt;                        } else {&lt;br/&gt;                            c.fetch_issue_status(&lt;br/&gt;                                event.id,&lt;br/&gt;                                [keys.as_slice(), &amp;amp;[event.pubkey]].concat(),&lt;br/&gt;                            )&lt;br/&gt;                            .await&lt;br/&gt;                            .map(Either::Right)?&lt;br/&gt;                        };&lt;br/&gt;                        N34Result::Ok((event, status))&lt;br/&gt;                    }&lt;br/&gt;                }),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .into_iter()&lt;br/&gt;        .filter_map(|r| r.ok()),&lt;br/&gt;        |(_, status)| status.as_ref().either_into::&amp;lt;Kind&amp;gt;(),&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    let lines = events&lt;br/&gt;        .map(|(event, status)| format_patch_and_issue(&amp;amp;event, status))&lt;br/&gt;        .collect::&amp;lt;Vec&amp;lt;String&amp;gt;&amp;gt;();&lt;br/&gt;&lt;br/&gt;    let max_width = lines&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|s| s.split_once(&amp;#39;\n&amp;#39;).map_or(85, |(l, _)| l.chars().count()))&lt;br/&gt;        .max()&lt;br/&gt;        .unwrap_or(85)&lt;br/&gt;        .max(67); // length of the event id&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;{}&amp;#34;, lines.join(&amp;amp;format!(&amp;#34;{}\n&amp;#34;, &amp;#34;-&amp;#34;.repeat(max_width))));&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns a tuple of (root_id, patch_id) if this is a valid root or revision&lt;br/&gt;/// patch.&lt;br/&gt;fn get_patch_root_revision(patch_event: &amp;amp;Event) -&amp;gt; N34Result&amp;lt;(EventId, Option&amp;lt;EventId&amp;gt;)&amp;gt; {&lt;br/&gt;    if patch_event.is_revision_patch() {&lt;br/&gt;        Ok((&lt;br/&gt;            patch_event.root_patch_from_revision()?,&lt;br/&gt;            Some(patch_event.id),&lt;br/&gt;        ))&lt;br/&gt;    } else if patch_event.is_root_patch() {&lt;br/&gt;        Ok((patch_event.id, None))&lt;br/&gt;    } else {&lt;br/&gt;        Err(N34Error::NotRootPatch)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Formats an event as either a patch or an issue. For patches, extracts the&lt;br/&gt;/// subject line from the Git patch format. For issues, combines the subject&lt;br/&gt;/// with labels. The output includes status and formatted ID.&lt;br/&gt;fn format_patch_and_issue(event: &amp;amp;Event, status: Either&amp;lt;PatchStatus, IssueStatus&amp;gt;) -&amp;gt; String {&lt;br/&gt;    let subject = if status.is_left() {&lt;br/&gt;        GitPatch::from_str(&amp;amp;event.content)&lt;br/&gt;            .map(|p| p.subject)&lt;br/&gt;            .unwrap_or_else(|_| {&lt;br/&gt;                event&lt;br/&gt;                    .content&lt;br/&gt;                    .lines()&lt;br/&gt;                    .find(|line| line.trim().starts_with(&amp;#34;Subject: &amp;#34;))&lt;br/&gt;                    .unwrap_or_default()&lt;br/&gt;                    .trim()&lt;br/&gt;                    .trim_start_matches(&amp;#34;Subject: &amp;#34;)&lt;br/&gt;                    .to_owned()&lt;br/&gt;            })&lt;br/&gt;    } else {&lt;br/&gt;        let labels = event.extract_issue_labels();&lt;br/&gt;        let subject = event.extract_issue_subject();&lt;br/&gt;&lt;br/&gt;        if labels.is_empty() {&lt;br/&gt;            subject.to_owned()&lt;br/&gt;        } else {&lt;br/&gt;            format!(r#&amp;#34;&amp;#34;{subject}&amp;#34; {labels}&amp;#34;#)&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;    format!(&lt;br/&gt;        &amp;#34;({status}) {}\nID: {}\n&amp;#34;,&lt;br/&gt;        utils::smart_wrap(&amp;amp;subject, 85),&lt;br/&gt;        event.id.to_bech32().expect(&amp;#34;Infallible&amp;#34;)&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Generates a list of tags for quoting patches in merge/applied status events.&lt;br/&gt;async fn build_patches_quote(&lt;br/&gt;    client: NostrClient,&lt;br/&gt;    relay_hint: Option&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;    patches: Vec&amp;lt;EventId&amp;gt;,&lt;br/&gt;) -&amp;gt; Vec&amp;lt;Tag&amp;gt; {&lt;br/&gt;    let client = Arc::new(client);&lt;br/&gt;    let relay_hint = Arc::new(relay_hint);&lt;br/&gt;&lt;br/&gt;    future::join_all(patches.into_iter().map(|eid| {&lt;br/&gt;        let task_relay = Arc::clone(&amp;amp;relay_hint);&lt;br/&gt;        let task_client = Arc::clone(&amp;amp;client);&lt;br/&gt;&lt;br/&gt;        async move {&lt;br/&gt;            Tag::custom(&lt;br/&gt;                TagKind::q(),&lt;br/&gt;                [&lt;br/&gt;                    eid.to_hex(),&lt;br/&gt;                    task_relay&lt;br/&gt;                        .as_ref()&lt;br/&gt;                        .as_ref()&lt;br/&gt;                        .map(|r| r.to_string())&lt;br/&gt;                        .unwrap_or_default(),&lt;br/&gt;                    task_client&lt;br/&gt;                        .event_author(eid)&lt;br/&gt;                        .await&lt;br/&gt;                        .ok()&lt;br/&gt;                        .flatten()&lt;br/&gt;                        .map(|p| p.to_hex())&lt;br/&gt;                        .unwrap_or_default(),&lt;br/&gt;                ],&lt;br/&gt;            )&lt;br/&gt;        }&lt;br/&gt;    }))&lt;br/&gt;    .await&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:33Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsxya2znw98j9x7ftrceczmze7lhdj49eyz383rhwmjd3pt4w3e20gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv5rhk66</id>
    
      <title type="html">//! A simple command-line tool that calculates and displays the ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsxya2znw98j9x7ftrceczmze7lhdj49eyz383rhwmjd3pt4w3e20gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv5rhk66" />
    <content type="html">
      //! A simple command-line tool that calculates and displays the SHA-256 hash of&lt;br/&gt;//! its own source file.&lt;br/&gt;//!&lt;br/&gt;//! This utility demonstrates how to use the `get_file_hash!` macro to obtain&lt;br/&gt;//! the hash of a specified file at compile time and incorporate it into runtime&lt;br/&gt;//! logic.&lt;br/&gt;&lt;br/&gt;use get_file_hash_core::get_file_hash;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART1: &amp;amp;str = r##&amp;#34;# `get_file_hash` macro&lt;br/&gt;&lt;br/&gt;This project provides a Rust procedural macro, `get_file_hash!`, designed to compute the SHA-256 hash of a specified file at compile time. This hash is then embedded directly into your compiled executable. This feature is invaluable for:&lt;br/&gt;&lt;br/&gt;*   **Integrity Verification:** Ensuring the deployed code hasn&amp;#39;t been tampered with.&lt;br/&gt;*   **Versioning:** Embedding a unique identifier linked to the exact source code version.&lt;br/&gt;*   **Cache Busting:** Generating unique names for assets based on their content.&lt;br/&gt;&lt;br/&gt;## Project Structure&lt;br/&gt;&lt;br/&gt;*   `get_file_hash_core`: A foundational crate containing the `get_file_hash!` macro definition.&lt;br/&gt;*   `get_file_hash`: The main library crate that re-exports the macro.&lt;br/&gt;*   `src/bin/get_file_hash.rs`: An example executable demonstrating the macro&amp;#39;s usage by hashing its own source file and updating this `README.md`.&lt;br/&gt;*   `build.rs`: A build script that also utilizes the `get_file_hash!` macro to hash `Cargo.toml` during the build process.&lt;br/&gt;&lt;br/&gt;## Usage of `get_file_hash!` Macro&lt;br/&gt;&lt;br/&gt;To use the `get_file_hash!` macro, ensure you have `get_file_hash` (or `get_file_hash_core` for direct usage) as a dependency in your `Cargo.toml`.&lt;br/&gt;&lt;br/&gt;### Example&lt;br/&gt;&lt;br/&gt;```rust&lt;br/&gt;use get_file_hash::get_file_hash;&lt;br/&gt;use sha2::{Digest, Sha256};&lt;br/&gt;&lt;br/&gt;fn main() {&lt;br/&gt;    // The macro resolves the path relative to CARGO_MANIFEST_DIR&lt;br/&gt;    let file_hash = get_file_hash!(&amp;#34;src/lib.rs&amp;#34;);&lt;br/&gt;    println!(&amp;#34;The SHA-256 hash of src/lib.rs is: {}&amp;#34;, file_hash);&lt;br/&gt;}&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;&amp;#34;##;&lt;br/&gt;&lt;br/&gt;const README_TEMPLATE_PART2: &amp;amp;str = r&amp;#34;## Setup and Building&lt;br/&gt;&lt;br/&gt;1.  **Clone the repository:**&lt;br/&gt;    ```bash&lt;br/&gt;    git clone &amp;lt;repository-url&amp;gt;&lt;br/&gt;    cd &amp;lt;repository-name&amp;gt;&lt;br/&gt;    ```&lt;br/&gt;2.  **Build the project:**&lt;br/&gt;    ```bash&lt;br/&gt;    cargo build&lt;br/&gt;    ```&lt;br/&gt;    During the build, `build.rs` will execute and print the hash of `Cargo.toml`.&lt;br/&gt;3.  **Run the example executable:**&lt;br/&gt;    ```bash&lt;br/&gt;    cargo run --bin get_file_hash&lt;br/&gt;    ```&lt;br/&gt;    This will print the hash of `src/bin/get_file_hash.rs` to your console.&lt;br/&gt;&lt;br/&gt;## Updating this `README.md`&lt;br/&gt;&lt;br/&gt;The hash information in this `README.md` is automatically generated by running the example executable.&lt;br/&gt;To update it, execute:&lt;br/&gt;&lt;br/&gt;```bash&lt;br/&gt;cargo run --bin get_file_hash &amp;gt; README.md&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;## Current File Hash Information (of `src/bin/get_file_hash.rs`)&lt;br/&gt;&lt;br/&gt;*   **Target File:** `src/bin/get_file_hash.rs`&lt;br/&gt;&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// The main entry point of the application.&lt;br/&gt;///&lt;br/&gt;/// This function calculates the SHA-256 hash of the `get_file_hash.rs` source&lt;br/&gt;/// file using a custom procedural macro and then prints the hash to the&lt;br/&gt;/// console. It also includes a basic integrity verification check.&lt;br/&gt;fn main() {&lt;br/&gt;    // Calculate the SHA-256 hash of the current file (`get_file_hash.rs`) at&lt;br/&gt;    // compile time. The `get_file_hash!` macro reads the file content and&lt;br/&gt;    // computes its hash.&lt;br/&gt;    let self_hash = get_file_hash!(&amp;#34;get_file_hash.rs&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let status_message = if self_hash.starts_with(&amp;#34;e3b0&amp;#34;) {&lt;br/&gt;        &amp;#34;Warning: This hash represents an empty file.&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        &amp;#34;Integrity Verified.&amp;#34;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    print!(&amp;#34;{}{}&amp;#34;, README_TEMPLATE_PART1, README_TEMPLATE_PART2);&lt;br/&gt;    println!(&amp;#34;*   **SHA-256 Hash:** `{}`&amp;#34;, self_hash);&lt;br/&gt;    println!(&amp;#34;*   **Status:** {}.\n&amp;#34;, status_message);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:32Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsdwr9d5c0ylnh0epkwpumtj9y7udpnqyl3s6t5skmnvg0un0zh03gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7vdtc7</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsdwr9d5c0ylnh0epkwpumtj9y7udpnqyl3s6t5skmnvg0un0zh03gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7vdtc7" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::collections::HashSet;&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, RelayOrSet},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct UpdateArgs {&lt;br/&gt;    /// Name of the set to update&lt;br/&gt;    name:         String,&lt;br/&gt;    /// Add relay to the set, either as URL or set name to extract its relays.&lt;br/&gt;    /// [aliases: `--sr`]&lt;br/&gt;    #[arg(long = &amp;#34;set-relay&amp;#34;, alias(&amp;#34;sr&amp;#34;))]&lt;br/&gt;    relays:       Vec&amp;lt;RelayOrSet&amp;gt;,&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:       Vec&amp;lt;NaddrOrSet&amp;gt;,&lt;br/&gt;    /// Replace existing relays/repositories instead of adding to them&lt;br/&gt;    #[arg(long = &amp;#34;override&amp;#34;)]&lt;br/&gt;    override_set: bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for UpdateArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?;&lt;br/&gt;        let relays = self.relays.flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;&lt;br/&gt;        let set = options.config.sets.get_mut_set(&amp;amp;self.name)?;&lt;br/&gt;&lt;br/&gt;        if self.override_set {&lt;br/&gt;            if !relays.is_empty() {&lt;br/&gt;                set.relays = HashSet::from_iter(relays);&lt;br/&gt;            }&lt;br/&gt;            if !naddrs.is_empty() {&lt;br/&gt;                set.naddrs = HashSet::from_iter(naddrs)&lt;br/&gt;            }&lt;br/&gt;        } else {&lt;br/&gt;            set.relays.extend(relays);&lt;br/&gt;            set.naddrs.extend(naddrs);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:22Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs9gkteya0d5mpp5nzvvaj363e2urlaj8dgkhhqqc557gq3mqjp4ngzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvyvxkrm</id>
    
      <title type="html">&amp;lt;?xml version=&amp;#39;1.0&amp;#39; ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs9gkteya0d5mpp5nzvvaj363e2urlaj8dgkhhqqc557gq3mqjp4ngzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvyvxkrm" />
    <content type="html">
      &amp;lt;?xml version=&amp;#39;1.0&amp;#39; encoding=&amp;#39;windows-1252&amp;#39;?&amp;gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  Copyright (C) 2017 Christopher R. Field.&lt;br/&gt;&lt;br/&gt;  Licensed under the Apache License, Version 2.0 (the &amp;#34;License&amp;#34;);&lt;br/&gt;  you may not use this file except in compliance with the License.&lt;br/&gt;  You may obtain a copy of the License at&lt;br/&gt;&lt;br/&gt;  &lt;a href=&#34;http://www.apache.org/licenses/LICENSE-2.0&#34;&gt;http://www.apache.org/licenses/LICENSE-2.0&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;  Unless required by applicable law or agreed to in writing, software&lt;br/&gt;  distributed under the License is distributed on an &amp;#34;AS IS&amp;#34; BASIS,&lt;br/&gt;  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&lt;br/&gt;  See the License for the specific language governing permissions and&lt;br/&gt;  limitations under the License.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  The &amp;#34;cargo wix&amp;#34; subcommand provides a variety of predefined variables available&lt;br/&gt;  for customization of this template. The values for each variable are set at&lt;br/&gt;  installer creation time. The following variables are available:&lt;br/&gt;&lt;br/&gt;  TargetTriple      = The rustc target triple name.&lt;br/&gt;  TargetEnv         = The rustc target environment. This is typically either&lt;br/&gt;                      &amp;#34;msvc&amp;#34; or &amp;#34;gnu&amp;#34; depending on the toolchain downloaded and&lt;br/&gt;                      installed.&lt;br/&gt;  TargetVendor      = The rustc target vendor. This is typically &amp;#34;pc&amp;#34;, but Rust&lt;br/&gt;                      does support other vendors, like &amp;#34;uwp&amp;#34;.&lt;br/&gt;  CargoTargetBinDir = The complete path to the directory containing the&lt;br/&gt;                      binaries (exes) to include. The default would be&lt;br/&gt;                      &amp;#34;target\release\&amp;#34;. If an explicit rustc target triple is&lt;br/&gt;                      used, i.e. cross-compiling, then the default path would&lt;br/&gt;                      be &amp;#34;target\&amp;lt;CARGO_TARGET&amp;gt;\&amp;lt;CARGO_PROFILE&amp;gt;&amp;#34;,&lt;br/&gt;                      where &amp;#34;&amp;lt;CARGO_TARGET&amp;gt;&amp;#34; is replaced with the &amp;#34;CargoTarget&amp;#34;&lt;br/&gt;                      variable value and &amp;#34;&amp;lt;CARGO_PROFILE&amp;gt;&amp;#34; is replaced with the&lt;br/&gt;                      value from the &amp;#34;CargoProfile&amp;#34; variable. This can also&lt;br/&gt;                      be overridden manually with the &amp;#34;target-bin-dir&amp;#34; flag.&lt;br/&gt;  CargoTargetDir    = The path to the directory for the build artifacts, i.e.&lt;br/&gt;                      &amp;#34;target&amp;#34;.&lt;br/&gt;  CargoProfile      = The cargo profile used to build the binaries&lt;br/&gt;                      (usually &amp;#34;debug&amp;#34; or &amp;#34;release&amp;#34;).&lt;br/&gt;  Version           = The version for the installer. The default is the&lt;br/&gt;                      &amp;#34;Major.Minor.Fix&amp;#34; semantic versioning number of the Rust&lt;br/&gt;                      package.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;!--&lt;br/&gt;  Please do not remove these pre-processor If-Else blocks. These are used with&lt;br/&gt;  the `cargo wix` subcommand to automatically determine the installation&lt;br/&gt;  destination for 32-bit versus 64-bit installers. Removal of these lines will&lt;br/&gt;  cause installation errors.&lt;br/&gt;--&amp;gt;&lt;br/&gt;&amp;lt;?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?&amp;gt;&lt;br/&gt;    &amp;lt;?define PlatformProgramFilesFolder = &amp;#34;ProgramFiles64Folder&amp;#34; ?&amp;gt;&lt;br/&gt;&amp;lt;?else ?&amp;gt;&lt;br/&gt;    &amp;lt;?define PlatformProgramFilesFolder = &amp;#34;ProgramFilesFolder&amp;#34; ?&amp;gt;&lt;br/&gt;&amp;lt;?endif ?&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;Wix xmlns=&amp;#39;&lt;a href=&#34;http://schemas.microsoft.com/wix/2006/wi&amp;#39;&amp;gt&#34;&gt;http://schemas.microsoft.com/wix/2006/wi&amp;#39;&amp;gt&lt;/a&gt;;&lt;br/&gt;&lt;br/&gt;    &amp;lt;Product&lt;br/&gt;        Id=&amp;#39;*&amp;#39;&lt;br/&gt;        Name=&amp;#39;n34&amp;#39;&lt;br/&gt;        UpgradeCode=&amp;#39;5E8926D0-4A18-48E9-A070-AFB0F698F04C&amp;#39;&lt;br/&gt;        Manufacturer=&amp;#39;Awiteb&amp;#39;&lt;br/&gt;        Language=&amp;#39;1033&amp;#39;&lt;br/&gt;        Codepage=&amp;#39;1252&amp;#39;&lt;br/&gt;        Version=&amp;#39;$(var.Version)&amp;#39;&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Package Id=&amp;#39;*&amp;#39;&lt;br/&gt;            Keywords=&amp;#39;Installer&amp;#39;&lt;br/&gt;            Description=&amp;#39;A CLI to interact with NIP-34 and other stuff related to code in Nostr&amp;#39;&lt;br/&gt;            Manufacturer=&amp;#39;Awiteb&amp;#39;&lt;br/&gt;            InstallerVersion=&amp;#39;450&amp;#39;&lt;br/&gt;            Languages=&amp;#39;1033&amp;#39;&lt;br/&gt;            Compressed=&amp;#39;yes&amp;#39;&lt;br/&gt;            InstallScope=&amp;#39;perMachine&amp;#39;&lt;br/&gt;            SummaryCodepage=&amp;#39;1252&amp;#39;&lt;br/&gt;            /&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;MajorUpgrade&lt;br/&gt;            Schedule=&amp;#39;afterInstallInitialize&amp;#39;&lt;br/&gt;            DowngradeErrorMessage=&amp;#39;A newer version of [ProductName] is already installed. Setup will now exit.&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Media Id=&amp;#39;1&amp;#39; Cabinet=&amp;#39;media1.cab&amp;#39; EmbedCab=&amp;#39;yes&amp;#39; DiskPrompt=&amp;#39;CD-ROM #1&amp;#39;/&amp;gt;&lt;br/&gt;        &amp;lt;Property Id=&amp;#39;DiskPrompt&amp;#39; Value=&amp;#39;n34 Installation&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Directory Id=&amp;#39;TARGETDIR&amp;#39; Name=&amp;#39;SourceDir&amp;#39;&amp;gt;&lt;br/&gt;            &amp;lt;Directory Id=&amp;#39;$(var.PlatformProgramFilesFolder)&amp;#39; Name=&amp;#39;PFiles&amp;#39;&amp;gt;&lt;br/&gt;                &amp;lt;Directory Id=&amp;#39;APPLICATIONFOLDER&amp;#39; Name=&amp;#39;n34&amp;#39;&amp;gt;&lt;br/&gt;                    &lt;br/&gt;                    &amp;lt;!--&lt;br/&gt;                      Enabling the license sidecar file in the installer is a four step process:&lt;br/&gt;&lt;br/&gt;                      1. Uncomment the `Component` tag and its contents.&lt;br/&gt;                      2. Change the value for the `Source` attribute in the `File` tag to a path&lt;br/&gt;                         to the file that should be included as the license sidecar file. The path&lt;br/&gt;                         can, and probably should be, relative to this file.&lt;br/&gt;                      3. Change the value for the `Name` attribute in the `File` tag to the&lt;br/&gt;                         desired name for the file when it is installed alongside the `bin` folder&lt;br/&gt;                         in the installation directory. This can be omitted if the desired name is&lt;br/&gt;                         the same as the file name.&lt;br/&gt;                      4. Uncomment the `ComponentRef` tag with the Id attribute value of &amp;#34;License&amp;#34;&lt;br/&gt;                         further down in this file.&lt;br/&gt;                    --&amp;gt;&lt;br/&gt;                    &amp;lt;!--&lt;br/&gt;                    &amp;lt;Component Id=&amp;#39;License&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                        &amp;lt;File Id=&amp;#39;LicenseFile&amp;#39; Name=&amp;#39;ChangeMe&amp;#39; DiskId=&amp;#39;1&amp;#39; Source=&amp;#39;C:\Path\To\File&amp;#39; KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                    &amp;lt;/Component&amp;gt;&lt;br/&gt;                    --&amp;gt;&lt;br/&gt;&lt;br/&gt;                    &amp;lt;Directory Id=&amp;#39;Bin&amp;#39; Name=&amp;#39;bin&amp;#39;&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;Path&amp;#39; Guid=&amp;#39;7A2E5F6A-FC23-498B-AEC1-5FA63678FBFD&amp;#39; KeyPath=&amp;#39;yes&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;Environment&lt;br/&gt;                                Id=&amp;#39;PATH&amp;#39;&lt;br/&gt;                                Name=&amp;#39;PATH&amp;#39;&lt;br/&gt;                                Value=&amp;#39;[Bin]&amp;#39;&lt;br/&gt;                                Permanent=&amp;#39;no&amp;#39;&lt;br/&gt;                                Part=&amp;#39;last&amp;#39;&lt;br/&gt;                                Action=&amp;#39;set&amp;#39;&lt;br/&gt;                                System=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                        &amp;lt;Component Id=&amp;#39;binary0&amp;#39; Guid=&amp;#39;*&amp;#39;&amp;gt;&lt;br/&gt;                            &amp;lt;File&lt;br/&gt;                                Id=&amp;#39;exe0&amp;#39;&lt;br/&gt;                                Name=&amp;#39;n34-cli.exe&amp;#39;&lt;br/&gt;                                DiskId=&amp;#39;1&amp;#39;&lt;br/&gt;                                Source=&amp;#39;$(var.CargoTargetBinDir)\n34-cli.exe&amp;#39;&lt;br/&gt;                                KeyPath=&amp;#39;yes&amp;#39;/&amp;gt;&lt;br/&gt;                        &amp;lt;/Component&amp;gt;&lt;br/&gt;                    &amp;lt;/Directory&amp;gt;&lt;br/&gt;                &amp;lt;/Directory&amp;gt;&lt;br/&gt;            &amp;lt;/Directory&amp;gt;&lt;br/&gt;        &amp;lt;/Directory&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Feature&lt;br/&gt;            Id=&amp;#39;Binaries&amp;#39;&lt;br/&gt;            Title=&amp;#39;Application&amp;#39;&lt;br/&gt;            Description=&amp;#39;Installs all binaries and the license.&amp;#39;&lt;br/&gt;            Level=&amp;#39;1&amp;#39;&lt;br/&gt;            ConfigurableDirectory=&amp;#39;APPLICATIONFOLDER&amp;#39;&lt;br/&gt;            AllowAdvertise=&amp;#39;no&amp;#39;&lt;br/&gt;            Display=&amp;#39;expand&amp;#39;&lt;br/&gt;            Absent=&amp;#39;disallow&amp;#39;&amp;gt;&lt;br/&gt;            &lt;br/&gt;            &amp;lt;!--&lt;br/&gt;              Uncomment the following `ComponentRef` tag to add the license&lt;br/&gt;              sidecar file to the installer.&lt;br/&gt;            --&amp;gt;&lt;br/&gt;            &amp;lt;!--&amp;lt;ComponentRef Id=&amp;#39;License&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;            &amp;lt;ComponentRef Id=&amp;#39;binary0&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;            &amp;lt;Feature&lt;br/&gt;                Id=&amp;#39;Environment&amp;#39;&lt;br/&gt;                Title=&amp;#39;PATH Environment Variable&amp;#39;&lt;br/&gt;                Description=&amp;#39;Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.&amp;#39;&lt;br/&gt;                Level=&amp;#39;1&amp;#39;&lt;br/&gt;                Absent=&amp;#39;allow&amp;#39;&amp;gt;&lt;br/&gt;                &amp;lt;ComponentRef Id=&amp;#39;Path&amp;#39;/&amp;gt;&lt;br/&gt;            &amp;lt;/Feature&amp;gt;&lt;br/&gt;        &amp;lt;/Feature&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;SetProperty Id=&amp;#39;ARPINSTALLLOCATION&amp;#39; Value=&amp;#39;[APPLICATIONFOLDER]&amp;#39; After=&amp;#39;CostFinalize&amp;#39;/&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the following `Icon` and `Property` tags to change the product icon.&lt;br/&gt;&lt;br/&gt;          The product icon is the graphic that appears in the Add/Remove&lt;br/&gt;          Programs control panel for the application.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;Icon Id=&amp;#39;ProductICO&amp;#39; SourceFile=&amp;#39;wix\Product.ico&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;Property Id=&amp;#39;ARPPRODUCTICON&amp;#39; Value=&amp;#39;ProductICO&amp;#39; /&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;Property Id=&amp;#39;ARPHELPLINK&amp;#39; Value=&amp;#39;&lt;a href=&#34;https://n34.dev/commands.html&amp;#39;/&amp;gt&#34;&gt;https://n34.dev/commands.html&amp;#39;/&amp;gt&lt;/a&gt;;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;UI&amp;gt;&lt;br/&gt;            &amp;lt;UIRef Id=&amp;#39;WixUI_FeatureTree&amp;#39;/&amp;gt;&lt;br/&gt;            &lt;br/&gt;            &amp;lt;!--&lt;br/&gt;              Enabling the EULA dialog in the installer is a three step process:&lt;br/&gt;&lt;br/&gt;                1. Comment out or remove the two `Publish` tags that follow the&lt;br/&gt;                   `WixVariable` tag.&lt;br/&gt;                2. Uncomment the `&amp;lt;WixVariable Id=&amp;#39;WixUILicenseRtf&amp;#39; Value=&amp;#39;Path\to\Eula.rft&amp;#39;&amp;gt;` tag further down&lt;br/&gt;                3. Replace the `Value` attribute of the `WixVariable` tag with&lt;br/&gt;                   the path to a RTF file that will be used as the EULA and&lt;br/&gt;                   displayed in the license agreement dialog.&lt;br/&gt;            --&amp;gt;&lt;br/&gt;            &amp;lt;Publish Dialog=&amp;#39;WelcomeDlg&amp;#39; Control=&amp;#39;Next&amp;#39; Event=&amp;#39;NewDialog&amp;#39; Value=&amp;#39;CustomizeDlg&amp;#39; Order=&amp;#39;99&amp;#39;&amp;gt;1&amp;lt;/Publish&amp;gt;&lt;br/&gt;            &amp;lt;Publish Dialog=&amp;#39;CustomizeDlg&amp;#39; Control=&amp;#39;Back&amp;#39; Event=&amp;#39;NewDialog&amp;#39; Value=&amp;#39;WelcomeDlg&amp;#39; Order=&amp;#39;99&amp;#39;&amp;gt;1&amp;lt;/Publish&amp;gt;&lt;br/&gt;&lt;br/&gt;        &amp;lt;/UI&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Enabling the EULA dialog in the installer requires uncommenting&lt;br/&gt;          the following `WixUILicenseRTF` tag and changing the `Value`&lt;br/&gt;          attribute.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!-- &amp;lt;WixVariable Id=&amp;#39;WixUILicenseRtf&amp;#39; Value=&amp;#39;Relative\Path\to\Eula.rtf&amp;#39;/&amp;gt; --&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the next `WixVariable` tag to customize the installer&amp;#39;s&lt;br/&gt;          Graphical User Interface (GUI) and add a custom banner image across&lt;br/&gt;          the top of each screen. See the WiX Toolset documentation for details&lt;br/&gt;          about customization.&lt;br/&gt;&lt;br/&gt;          The banner BMP dimensions are 493 x 58 pixels.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;WixVariable Id=&amp;#39;WixUIBannerBmp&amp;#39; Value=&amp;#39;wix\Banner.bmp&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;        &lt;br/&gt;        &amp;lt;!--&lt;br/&gt;          Uncomment the next `WixVariable` tag to customize the installer&amp;#39;s&lt;br/&gt;          Graphical User Interface (GUI) and add a custom image to the first&lt;br/&gt;          dialog, or screen. See the WiX Toolset documentation for details about&lt;br/&gt;          customization.&lt;br/&gt;&lt;br/&gt;          The dialog BMP dimensions are 493 x 312 pixels.&lt;br/&gt;        --&amp;gt;&lt;br/&gt;        &amp;lt;!--&amp;lt;WixVariable Id=&amp;#39;WixUIDialogBmp&amp;#39; Value=&amp;#39;wix\Dialog.bmp&amp;#39;/&amp;gt;--&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;/Product&amp;gt;&lt;br/&gt;&lt;br/&gt;&amp;lt;/Wix&amp;gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:21Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqstrcwpttghvq87n3kez40j4surplhcn2fft9vh9yemtszd3xn5usczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvq357fj</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqstrcwpttghvq87n3kez40j4surplhcn2fft9vh9yemtszd3xn5usczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvq357fj" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    fmt,&lt;br/&gt;    fs,&lt;br/&gt;    path::{Path, PathBuf},&lt;br/&gt;    str::FromStr,&lt;br/&gt;    sync::atomic::Ordering,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventId, Kind, Tag, TagKind, TagStandard},&lt;br/&gt;    filter::Alphabet,&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    nips::{&lt;br/&gt;        nip01::Coordinate,&lt;br/&gt;        nip10::Marker,&lt;br/&gt;        nip19::{Nip19Coordinate, Nip19Event, ToBech32},&lt;br/&gt;        nip34::GitRepositoryAnnouncement,&lt;br/&gt;        nip65::{self, RelayMetadata},&lt;br/&gt;    },&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use super::traits::TagsExt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{NOSTR_ADDRESS_FILE, parsers},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Returns the value of the given tag&lt;br/&gt;#[inline]&lt;br/&gt;fn tag_value(tag: &amp;amp;TagStandard) -&amp;gt; String {&lt;br/&gt;    tag.clone().to_vec().remove(1)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses the tag value into type `T` if possible.&lt;br/&gt;#[inline]&lt;br/&gt;fn parse_value&amp;lt;T: FromStr&amp;gt;(tag: &amp;amp;TagStandard) -&amp;gt; Option&amp;lt;T&amp;gt; {&lt;br/&gt;    tag_value(tag).parse().ok()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Gets all values from the tag. If any value fails to parse, returns an empty&lt;br/&gt;/// vector.&lt;br/&gt;#[inline]&lt;br/&gt;fn tag_values&amp;lt;T&amp;gt;(tag: &amp;amp;TagStandard) -&amp;gt; Vec&amp;lt;T&amp;gt;&lt;br/&gt;where&lt;br/&gt;    T: FromStr &#43; fmt::Debug,&lt;br/&gt;    &amp;lt;T as FromStr&amp;gt;::Err: fmt::Debug,&lt;br/&gt;{&lt;br/&gt;    tag.clone()&lt;br/&gt;        .to_vec()&lt;br/&gt;        .into_iter()&lt;br/&gt;        .skip(1)&lt;br/&gt;        .map(|t| {&lt;br/&gt;            let result = T::from_str(t.as_str());&lt;br/&gt;            tracing::trace!(&amp;#34;Parsing `{t}` result: `{result:?}`&amp;#34;);&lt;br/&gt;            result&lt;br/&gt;        })&lt;br/&gt;        .collect::&amp;lt;Result&amp;lt;_, _&amp;gt;&amp;gt;()&lt;br/&gt;        .unwrap_or_default()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Convert [`Event`] to [`GitRepositoryAnnouncement`]&lt;br/&gt;pub fn event_into_repo(event: Event, repo_id: impl Into&amp;lt;String&amp;gt;) -&amp;gt; GitRepositoryAnnouncement {&lt;br/&gt;    let tags = &amp;amp;event.tags;&lt;br/&gt;&lt;br/&gt;    GitRepositoryAnnouncement {&lt;br/&gt;        id:          repo_id.into(),&lt;br/&gt;        name:        tags.map_tag(TagKind::Name, tag_value),&lt;br/&gt;        description: tags.map_tag(TagKind::Description, tag_value),&lt;br/&gt;        euc:         tags&lt;br/&gt;            .map_marker(&lt;br/&gt;                TagKind::single_letter(Alphabet::R, false),&lt;br/&gt;                &amp;#34;euc&amp;#34;,&lt;br/&gt;                parse_value,&lt;br/&gt;            )&lt;br/&gt;            .flatten(),&lt;br/&gt;        web:         tags.dmap_tag(TagKind::Web, tag_values),&lt;br/&gt;        clone:       tags.dmap_tag(TagKind::Clone, tag_values),&lt;br/&gt;        relays:      tags.dmap_tag(TagKind::Relays, tag_values),&lt;br/&gt;        maintainers: tags.dmap_tag(TagKind::Maintainers, tag_values),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns a new string with leading and trailing whitespace removed.&lt;br/&gt;pub fn str_trim(s: String) -&amp;gt; String {&lt;br/&gt;    s.trim().to_owned()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns a vector with duplicate elements removed.&lt;br/&gt;pub fn dedup&amp;lt;I, T&amp;gt;(iter: I) -&amp;gt; Vec&amp;lt;T&amp;gt;&lt;br/&gt;where&lt;br/&gt;    T: std::cmp::Ord,&lt;br/&gt;    I: Iterator&amp;lt;Item = T&amp;gt;,&lt;br/&gt;{&lt;br/&gt;    let mut vector: Vec&amp;lt;T&amp;gt; = iter.collect();&lt;br/&gt;    vector.sort_unstable();&lt;br/&gt;    vector.dedup();&lt;br/&gt;    vector&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Sorts items from the iterator using the given key function.&lt;br/&gt;/// The sorting is unstable, but faster than stable sorting.&lt;br/&gt;pub fn sort_by_key&amp;lt;I, T, K&amp;gt;(iterator: I, key: impl FnMut(&amp;amp;T) -&amp;gt; K) -&amp;gt; impl Iterator&amp;lt;Item = T&amp;gt;&lt;br/&gt;where&lt;br/&gt;    I: IntoIterator&amp;lt;Item = T&amp;gt;,&lt;br/&gt;    K: Ord,&lt;br/&gt;{&lt;br/&gt;    let mut vector = Vec::&amp;lt;T&amp;gt;::from_iter(iterator);&lt;br/&gt;    vector.sort_unstable_by_key(key);&lt;br/&gt;    vector.into_iter()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates a new NIP-19 nevent string from an event ID and up to 3 unique relay&lt;br/&gt;/// URLs.&lt;br/&gt;#[inline]&lt;br/&gt;pub fn new_nevent(event_id: EventId, relays: &amp;amp;[RelayUrl]) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    Nip19Event::new(event_id)&lt;br/&gt;        .relays(&lt;br/&gt;            dedup(relays.iter().cloned())&lt;br/&gt;                .into_iter()&lt;br/&gt;                .take(3)&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;(),&lt;br/&gt;        )&lt;br/&gt;        .to_bech32()&lt;br/&gt;        .map_err(N34Error::from)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates a NIP-19 naddr string for a git repository announcement and up to 3&lt;br/&gt;/// unique relay URLs.&lt;br/&gt;#[inline]&lt;br/&gt;pub fn repo_naddr(&lt;br/&gt;    repo_id: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;    pubk: PublicKey,&lt;br/&gt;    relays: &amp;amp;[RelayUrl],&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    Nip19Coordinate::new(&lt;br/&gt;        Coordinate::new(Kind::GitRepoAnnouncement, pubk).identifier(repo_id),&lt;br/&gt;        dedup(relays.iter().cloned()).into_iter().take(3),&lt;br/&gt;    )&lt;br/&gt;    .to_bech32()&lt;br/&gt;    .map_err(N34Error::from)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts write relay URLs from an event if present, otherwise returns an&lt;br/&gt;/// empty vector.&lt;br/&gt;pub fn add_write_relays(event: Option&amp;lt;&amp;amp;Event&amp;gt;) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;    let mut vector = Vec::new();&lt;br/&gt;    if let Some(event) = event {&lt;br/&gt;        vector.extend(&lt;br/&gt;            nip65::extract_owned_relay_list(event.clone())&lt;br/&gt;                .filter_map(|(r, m)| m.is_none_or(|m| m == RelayMetadata::Write).then_some(r)),&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;    vector&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts read relay URLs from an event if present, otherwise returns an&lt;br/&gt;/// empty vector.&lt;br/&gt;pub fn add_read_relays(event: Option&amp;lt;&amp;amp;Event&amp;gt;) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;    let mut vector = Vec::new();&lt;br/&gt;    if let Some(event) = event {&lt;br/&gt;        vector.extend(&lt;br/&gt;            nip65::extract_owned_relay_list(event.clone())&lt;br/&gt;                .filter_map(|(r, m)| m.is_none_or(|m| m == RelayMetadata::Read).then_some(r)),&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;    vector&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Opens the user&amp;#39;s default editor ($EDITOR) to edit a temporary file with&lt;br/&gt;/// given suffix, then reads and returns the file contents. The temporary file&lt;br/&gt;/// is automatically deleted.&lt;br/&gt;pub fn read_editor(file_pre_content: Option&amp;lt;&amp;amp;str&amp;gt;, file_suffix: &amp;amp;str) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    let Ok(editor) = std::env::var(&amp;#34;EDITOR&amp;#34;) else {&lt;br/&gt;        return Err(N34Error::EditorNotFound);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let temp_path = tempfile::NamedTempFile::with_suffix(file_suffix)?.into_temp_path();&lt;br/&gt;&lt;br/&gt;    if let Some(pre_content) = file_pre_content {&lt;br/&gt;        fs::write(&amp;amp;temp_path, pre_content)?;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Disable the logs to not show up in a terminal text editor&lt;br/&gt;    crate::EDITOR_OPEN.store(true, Ordering::Relaxed);&lt;br/&gt;    let exit_status = std::process::Command::new(&amp;amp;editor)&lt;br/&gt;        .arg(temp_path.to_str().expect(&amp;#34;The path is valid utf8&amp;#34;))&lt;br/&gt;        .spawn()?&lt;br/&gt;        .wait()?;&lt;br/&gt;    crate::EDITOR_OPEN.store(false, Ordering::Relaxed);&lt;br/&gt;&lt;br/&gt;    if !exit_status.success()&lt;br/&gt;        &amp;amp;&amp;amp; let Some(code) = exit_status.code()&lt;br/&gt;    {&lt;br/&gt;        return Err(N34Error::EditorErr(editor, code));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let content = fs::read_to_string(&amp;amp;temp_path)&lt;br/&gt;        .map_err(N34Error::from)?&lt;br/&gt;        .trim()&lt;br/&gt;        .to_owned();&lt;br/&gt;&lt;br/&gt;    if content.is_empty() {&lt;br/&gt;        return Err(N34Error::EmptyEditorFile);&lt;br/&gt;    }&lt;br/&gt;    Ok(content)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns the given content if it&amp;#39;s `Option::Some` or call [`read_editor`]&lt;br/&gt;pub fn get_content(&lt;br/&gt;    content: Option&amp;lt;impl AsRef&amp;lt;str&amp;gt;&amp;gt;,&lt;br/&gt;    quoted_content: Option&amp;lt;impl AsRef&amp;lt;str&amp;gt;&amp;gt;,&lt;br/&gt;    file_suffix: &amp;amp;str,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    if let Some(content) = content {&lt;br/&gt;        return Ok(content.as_ref().trim().to_owned());&lt;br/&gt;    }&lt;br/&gt;    read_editor(&lt;br/&gt;        quoted_content.map(|s| s.as_ref().to_owned()).as_deref(),&lt;br/&gt;        file_suffix,&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Path to the `nostr-address` file in current directory.&lt;br/&gt;#[inline]&lt;br/&gt;pub fn nostr_address_path() -&amp;gt; std::io::Result&amp;lt;PathBuf&amp;gt; {&lt;br/&gt;    std::env::current_dir().map(|p| p.join(NOSTR_ADDRESS_FILE))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns the given coordinates if Some, otherwise attempts to read and parse&lt;br/&gt;/// coordinates from the specified file. Returns an empty vector if the file&lt;br/&gt;/// doesn&amp;#39;t exist.&lt;br/&gt;pub fn naddrs_or_file(&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt;,&lt;br/&gt;    address_file_path: &amp;amp;Path,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;    if let Some(naddrs) = naddrs {&lt;br/&gt;        return Ok(naddrs);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if address_file_path.exists() {&lt;br/&gt;        parsers::parse_nostr_address_file(address_file_path)&lt;br/&gt;    } else {&lt;br/&gt;        Ok(Vec::new())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Generate a reply tag for an event with the given ID, relay URL (if any), and&lt;br/&gt;/// marker.&lt;br/&gt;#[inline]&lt;br/&gt;pub fn event_reply_tag(reply_to: &amp;amp;EventId, relay: Option&amp;lt;&amp;amp;RelayUrl&amp;gt;, marker: Marker) -&amp;gt; Tag {&lt;br/&gt;    Tag::custom(&lt;br/&gt;        TagKind::e(),&lt;br/&gt;        [&lt;br/&gt;            reply_to.to_hex(),&lt;br/&gt;            relay.map(|r| r.to_string()).unwrap_or_default(),&lt;br/&gt;            marker.to_string(),&lt;br/&gt;        ],&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Wraps text into lines no longer than max_width, breaking only at whitespace.&lt;br/&gt;pub fn smart_wrap(text: &amp;amp;str, max_width: usize) -&amp;gt; String {&lt;br/&gt;    text.lines()&lt;br/&gt;        .map(|line| {&lt;br/&gt;            if !line.trim().is_empty() {&lt;br/&gt;                line.split(&amp;#34; &amp;#34;)&lt;br/&gt;                    .fold((String::new(), 0), |(result, last_newline), word| {&lt;br/&gt;                        let result_len = result.chars().count();&lt;br/&gt;                        if result_len == 0 {&lt;br/&gt;                            (word.to_owned(), 0)&lt;br/&gt;                        } else if (result_len - last_newline) &#43; word.chars().count() &amp;gt; max_width {&lt;br/&gt;                            (format!(&amp;#34;{result}\n{word}&amp;#34;), result_len &#43; 1)&lt;br/&gt;                        } else {&lt;br/&gt;                            (format!(&amp;#34;{result} {word}&amp;#34;), last_newline)&lt;br/&gt;                        }&lt;br/&gt;                    })&lt;br/&gt;                    .0&lt;br/&gt;            } else {&lt;br/&gt;                String::new()&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;        .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;        .join(&amp;#34;\n&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns an error if the given naddrs is empty otherwise returned it&lt;br/&gt;pub fn check_empty_naddrs(naddrs: Vec&amp;lt;Nip19Coordinate&amp;gt;) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;    if naddrs.is_empty() {&lt;br/&gt;        return Err(N34Error::EmptyNaddrs);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(naddrs)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:10Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsqevrr3kwljt5wlmkdf3jytazhfmev0j4qhjg44y2cs2swres7n0qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsve0rkhr</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsqevrr3kwljt5wlmkdf3jytazhfmev0j4qhjg44y2cs2swres7n0qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsve0rkhr" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::nips::nip19::ToBech32;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        RepoRelaySet,&lt;br/&gt;        traits::{CommandRunner, RepoRelaySetsExt},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct ShowArgs {&lt;br/&gt;    /// Name of the set to display. If not provided, lists all available sets.&lt;br/&gt;    name: Option&amp;lt;String&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ShowArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        if let Some(name) = self.name {&lt;br/&gt;            println!(&lt;br/&gt;                &amp;#34;{}&amp;#34;,&lt;br/&gt;                format_set(options.config.sets.as_slice().get_set(&amp;amp;name)?)&lt;br/&gt;            );&lt;br/&gt;        } else {&lt;br/&gt;            println!(&lt;br/&gt;                &amp;#34;{}&amp;#34;,&lt;br/&gt;                options&lt;br/&gt;                    .config&lt;br/&gt;                    .sets&lt;br/&gt;                    .iter()&lt;br/&gt;                    .map(format_set)&lt;br/&gt;                    .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;                    .join(&amp;#34;\n----------\n&amp;#34;)&lt;br/&gt;            );&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Format a set to view it to the user&lt;br/&gt;fn format_set(set: &amp;amp;RepoRelaySet) -&amp;gt; String {&lt;br/&gt;    let naddrs = if set.naddrs.is_empty() {&lt;br/&gt;        &amp;#34;Nothing&amp;#34;.to_owned()&lt;br/&gt;    } else {&lt;br/&gt;        format!(&lt;br/&gt;            &amp;#34;\n- {}&amp;#34;,&lt;br/&gt;            set.naddrs&lt;br/&gt;                .iter()&lt;br/&gt;                .map(|naddr| naddr.to_bech32().expect(&amp;#34;We did decoded before&amp;#34;))&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;                .join(&amp;#34;\n- &amp;#34;)&lt;br/&gt;        )&lt;br/&gt;    };&lt;br/&gt;    let relays = if set.relays.is_empty() {&lt;br/&gt;        &amp;#34;Nothing&amp;#34;.to_owned()&lt;br/&gt;    } else {&lt;br/&gt;        format!(&lt;br/&gt;            &amp;#34;\n- {}&amp;#34;,&lt;br/&gt;            set.relays&lt;br/&gt;                .iter()&lt;br/&gt;                .map(|relay| relay.to_string())&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;                .join(&amp;#34;\n- &amp;#34;)&lt;br/&gt;        )&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    format!(&lt;br/&gt;        &amp;#34;Name: {}\nّّRepositories: {naddrs}\nRelays: {relays}&amp;#34;,&lt;br/&gt;        set.name&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:49:10Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs00890jnd0h6a2089lleqchv6y3a9laapnys5gwl35knkwapna0rgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvs9cmmn</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs00890jnd0h6a2089lleqchv6y3a9laapnys5gwl35knkwapna0rgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvs9cmmn" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, RelayOrSet},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct RemoveArgs {&lt;br/&gt;    /// Set name to delete&lt;br/&gt;    name:   String,&lt;br/&gt;    /// Specific relay to remove it from the set, either as URL or set name to&lt;br/&gt;    /// extract its relays. [aliases: `--sr`]&lt;br/&gt;    #[arg(long = &amp;#34;set-relay&amp;#34;, alias(&amp;#34;sr&amp;#34;))]&lt;br/&gt;    relays: Vec&amp;lt;RelayOrSet&amp;gt;,&lt;br/&gt;&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs: Vec&amp;lt;NaddrOrSet&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for RemoveArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?;&lt;br/&gt;        let relays = self.relays.flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;&lt;br/&gt;        if relays.is_empty() &amp;amp;&amp;amp; naddrs.is_empty() {&lt;br/&gt;            options.config.sets.remove_set(self.name)?;&lt;br/&gt;        } else {&lt;br/&gt;            if !relays.is_empty() {&lt;br/&gt;                options&lt;br/&gt;                    .config&lt;br/&gt;                    .sets&lt;br/&gt;                    .remove_relays(&amp;amp;self.name, relays.into_iter())?;&lt;br/&gt;            }&lt;br/&gt;            if !naddrs.is_empty() {&lt;br/&gt;                options&lt;br/&gt;                    .config&lt;br/&gt;                    .sets&lt;br/&gt;                    .remove_naddrs(self.name, naddrs.into_iter())?;&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:59Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsz4j62gafg9alg5wkrtdqdzeegd4u8ak3dq7f7p83yfrdgzkcyz0szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsd20cv</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsz4j62gafg9alg5wkrtdqdzeegd4u8ak3dq7f7p83yfrdgzkcyz0szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsd20cv" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use convert_case::{Case, Casing};&lt;br/&gt;use nostr::hashes::sha1::Hash as Sha1Hash;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard, Tags},&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    nips::{&lt;br/&gt;        nip01::Coordinate,&lt;br/&gt;        nip19::Nip19Coordinate,&lt;br/&gt;        nip21::Nip21,&lt;br/&gt;        nip34::{GitIssue, GitRepositoryAnnouncement},&lt;br/&gt;    },&lt;br/&gt;    parser::Token,&lt;br/&gt;    types::{RelayUrl, Url},&lt;br/&gt;};&lt;br/&gt;use nostr_keyring::KeyringError;&lt;br/&gt;&lt;br/&gt;use crate::cli::issue::ISSUE_ALT_PREFIX;&lt;br/&gt;use crate::cli::patch::{&lt;br/&gt;    LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT,&lt;br/&gt;    REVISION_ROOT_HASHTAG_CONTENT,&lt;br/&gt;    ROOT_HASHTAG_CONTENT,&lt;br/&gt;};&lt;br/&gt;use crate::error::{N34Error, N34Result};&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// A trait to add helper instance function to [`Tags`] type&lt;br/&gt;#[easy_ext::ext(TagsExt)]&lt;br/&gt;impl Tags {&lt;br/&gt;    /// Search for the given tag and map it value to a function&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn map_tag&amp;lt;T&amp;gt;(&amp;amp;self, kind: TagKind, f: impl FnOnce(&amp;amp;TagStandard) -&amp;gt; T) -&amp;gt; Option&amp;lt;T&amp;gt; {&lt;br/&gt;        self.find_standardized(kind).map(f)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Search for the given tag and map it value to a function. If the tag not&lt;br/&gt;    /// found return the default `T`&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn dmap_tag&amp;lt;T&amp;gt;(&amp;amp;self, kind: TagKind, f: impl FnOnce(&amp;amp;TagStandard) -&amp;gt; T) -&amp;gt; T&lt;br/&gt;    where&lt;br/&gt;        T: Default,&lt;br/&gt;    {&lt;br/&gt;        self.map_tag(kind, f).unwrap_or_default()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finds the first standard tag of the given kind with the specified&lt;br/&gt;    /// marker, then applies the function to the tag and returns the result.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn map_marker&amp;lt;T&amp;gt;(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        kind: TagKind,&lt;br/&gt;        marker: &amp;amp;str,&lt;br/&gt;        f: impl FnOnce(&amp;amp;TagStandard) -&amp;gt; T,&lt;br/&gt;    ) -&amp;gt; Option&amp;lt;T&amp;gt; {&lt;br/&gt;        self.filter_standardized(kind)&lt;br/&gt;            .find(|t| (*t).clone().to_vec().last().is_some_and(|m| m == marker))&lt;br/&gt;            .map(f)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Trait for building [`GitRepositoryAnnouncement`] events&lt;br/&gt;#[easy_ext::ext(NewGitRepositoryAnnouncement)]&lt;br/&gt;impl EventBuilder {&lt;br/&gt;    /// Creates a new [`GitRepositoryAnnouncement`] event builder with the given&lt;br/&gt;    /// repository details.&lt;br/&gt;    #[allow(clippy::too_many_arguments)]&lt;br/&gt;    pub fn new_git_repo(&lt;br/&gt;        repo_id: String,&lt;br/&gt;        name: Option&amp;lt;String&amp;gt;,&lt;br/&gt;        description: Option&amp;lt;String&amp;gt;,&lt;br/&gt;        web: Vec&amp;lt;Url&amp;gt;,&lt;br/&gt;        clone: Vec&amp;lt;Url&amp;gt;,&lt;br/&gt;        relays: Vec&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;        maintainers: Vec&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;        labels: Vec&amp;lt;String&amp;gt;,&lt;br/&gt;        force_id: bool,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;EventBuilder&amp;gt; {&lt;br/&gt;        let repo_id = repo_id.trim();&lt;br/&gt;        let kebab_repo_id = repo_id.to_case(Case::Kebab);&lt;br/&gt;        if repo_id.is_empty() || (!force_id &amp;amp;&amp;amp; repo_id != kebab_repo_id) {&lt;br/&gt;            if repo_id != kebab_repo_id {&lt;br/&gt;                tracing::error!(&lt;br/&gt;                    &amp;#34;The repo id should be `{kebab_repo_id}` (kebab-case). Use `--force-id` to \&lt;br/&gt;                     override this check&amp;#34;&lt;br/&gt;                );&lt;br/&gt;            }&lt;br/&gt;            return Err(N34Error::InvalidRepoId);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(&lt;br/&gt;            EventBuilder::git_repository_announcement(GitRepositoryAnnouncement {&lt;br/&gt;                id: repo_id.to_owned(),&lt;br/&gt;                name,&lt;br/&gt;                description,&lt;br/&gt;                web,&lt;br/&gt;                clone,&lt;br/&gt;                relays,&lt;br/&gt;                euc: None,&lt;br/&gt;                maintainers,&lt;br/&gt;            })?&lt;br/&gt;            .dedup_tags()&lt;br/&gt;            .tags(labels.into_iter().map(Tag::hashtag)),&lt;br/&gt;        )&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Creates a new [`GitIssue`] event builder with the given&lt;br/&gt;    /// issue details.&lt;br/&gt;    pub fn new_git_issue(&lt;br/&gt;        coordinates: &amp;amp;[Coordinate],&lt;br/&gt;        content: String,&lt;br/&gt;        subject: Option&amp;lt;String&amp;gt;,&lt;br/&gt;        labels: Vec&amp;lt;String&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;EventBuilder&amp;gt; {&lt;br/&gt;        let mut coordinates = coordinates.iter();&lt;br/&gt;        let first_coordinate = coordinates.next().ok_or(N34Error::EmptyNaddrs)?;&lt;br/&gt;&lt;br/&gt;        let mut event_builder = EventBuilder::git_issue(GitIssue {&lt;br/&gt;            repository: first_coordinate.clone(),&lt;br/&gt;            content,&lt;br/&gt;            subject: subject.clone(),&lt;br/&gt;            labels: labels.into_iter().map(|l| l.trim().to_owned()).collect(),&lt;br/&gt;        })&lt;br/&gt;        .map_err(N34Error::from)?&lt;br/&gt;        .tags(&lt;br/&gt;            coordinates&lt;br/&gt;                .clone()&lt;br/&gt;                .map(|c| Tag::coordinate(c.clone(), None)),&lt;br/&gt;        )&lt;br/&gt;        .tags(coordinates.map(|c| Tag::public_key(c.public_key)));&lt;br/&gt;&lt;br/&gt;        if let Some(issue_subject) = subject {&lt;br/&gt;            event_builder =&lt;br/&gt;                event_builder.tag(Tag::alt(format!(&amp;#34;{ISSUE_ALT_PREFIX}{issue_subject}&amp;#34;)))&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(event_builder)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Helper functions for [`Token`] type&lt;br/&gt;#[easy_ext::ext(TokenUtils)]&lt;br/&gt;impl Token&amp;lt;&amp;#39;_&amp;gt; {&lt;br/&gt;    /// Returns `Some((public_key, relays))` from the givin token if it&amp;#39;s npub1&lt;br/&gt;    /// or nprofile1&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_public_key(&amp;amp;self) -&amp;gt; Option&amp;lt;(PublicKey, Vec&amp;lt;RelayUrl&amp;gt;)&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Token::Nostr(nip21) =&amp;gt; {&lt;br/&gt;                match nip21 {&lt;br/&gt;                    Nip21::Pubkey(pkey) =&amp;gt; Some((*pkey, Vec::new())),&lt;br/&gt;                    Nip21::Profile(profile) =&amp;gt; Some((profile.public_key, profile.relays.clone())),&lt;br/&gt;                    _ =&amp;gt; None,&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            _ =&amp;gt; None,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `Some((note_id, relays))` from the givin token if it&amp;#39;s note1 or&lt;br/&gt;    /// nevent1&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_event_id(&amp;amp;self) -&amp;gt; Option&amp;lt;(EventId, Vec&amp;lt;RelayUrl&amp;gt;)&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Token::Nostr(nip21) =&amp;gt; {&lt;br/&gt;                match nip21 {&lt;br/&gt;                    Nip21::EventId(event_id) =&amp;gt; Some((*event_id, Vec::new())),&lt;br/&gt;                    Nip21::Event(event) =&amp;gt; Some((event.event_id, event.relays.clone())),&lt;br/&gt;                    _ =&amp;gt; None,&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            _ =&amp;gt; None,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `Some(hashtag)` from the givin token if it&amp;#39;s a hashtag&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_hashtag(&amp;amp;self) -&amp;gt; Option&amp;lt;String&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Token::Hashtag(tag) =&amp;gt; Some(tag.trim().to_owned()),&lt;br/&gt;            _ =&amp;gt; None,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Utility functions for working with lists of NIP-19 coordinates&lt;br/&gt;#[easy_ext::ext(NaddrsUtils)]&lt;br/&gt;impl Vec&amp;lt;Nip19Coordinate&amp;gt; {&lt;br/&gt;    /// Converts these coordinate addresses to basic coordinates&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn into_coordinates(self) -&amp;gt; Vec&amp;lt;Coordinate&amp;gt; {&lt;br/&gt;        self.into_iter().map(|n| n.coordinate).collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns all repository owners&amp;#39; public keys from these coordinates.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_owners(&amp;amp;self) -&amp;gt; Vec&amp;lt;PublicKey&amp;gt; {&lt;br/&gt;        self.iter().map(|n| n.public_key).collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Extracts all relay URLs from these coordinates&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_relays(&amp;amp;self) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;        self.iter().flat_map(|n| n.relays.clone()).collect()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Utility functions for working with lists of repository announcement&lt;br/&gt;#[easy_ext::ext(ReposUtils)]&lt;br/&gt;impl Vec&amp;lt;GitRepositoryAnnouncement&amp;gt; {&lt;br/&gt;    /// Extracts all relay URLs from these repositories&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_relays(&amp;amp;self) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;        self.iter().flat_map(|n| n.relays.clone()).collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Extract all the maintainers from these repositories&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_maintainers(&amp;amp;self) -&amp;gt; Vec&amp;lt;PublicKey&amp;gt; {&lt;br/&gt;        self.iter().flat_map(|r| r.maintainers.clone()).collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the first EUC hash from the reposotoies if it exists.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_euc(&amp;amp;self) -&amp;gt; Option&amp;lt;&amp;amp;Sha1Hash&amp;gt; {&lt;br/&gt;        self.iter().find_map(|r| r.euc.as_ref())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Utility functions for working with patch events&lt;br/&gt;#[easy_ext::ext(GitPatchUtils)]&lt;br/&gt;impl Event {&lt;br/&gt;    /// Returns whether the patch is a root or not&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_root_patch(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.kind == Kind::GitPatch&lt;br/&gt;            &amp;amp;&amp;amp; self&lt;br/&gt;                .tags&lt;br/&gt;                .filter(TagKind::t())&lt;br/&gt;                .any(|t| t.content() == Some(ROOT_HASHTAG_CONTENT))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns whether the patch is patch-revision or not&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_revision_patch(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.kind == Kind::GitPatch&lt;br/&gt;            &amp;amp;&amp;amp; self.tags.filter(TagKind::t()).any(|t| {&lt;br/&gt;                [&lt;br/&gt;                    Some(REVISION_ROOT_HASHTAG_CONTENT),&lt;br/&gt;                    Some(LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT),&lt;br/&gt;                ]&lt;br/&gt;                .contains(&amp;amp;t.content())&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the root patch ID from a patch-revision event by finding the `e`&lt;br/&gt;    /// tag that replies to it. Fails if no such tag is found or if the tag&lt;br/&gt;    /// contains an invalid event ID.&lt;br/&gt;    pub fn root_patch_from_revision(&amp;amp;self) -&amp;gt; N34Result&amp;lt;EventId&amp;gt; {&lt;br/&gt;        self.tags&lt;br/&gt;            .iter()&lt;br/&gt;            .find(|tag| tag.is_reply())&lt;br/&gt;            .ok_or_else(|| {&lt;br/&gt;                N34Error::InvalidEvent(&lt;br/&gt;                    &amp;#34;A patch revision without `e`-reply to the root patch&amp;#34;.to_owned(),&lt;br/&gt;                )&lt;br/&gt;            })?&lt;br/&gt;            .content()&lt;br/&gt;            .ok_or_else(|| N34Error::InvalidEvent(&amp;#34;`e` tag without an event&amp;#34;.to_owned()))?&lt;br/&gt;            .parse()&lt;br/&gt;            .map_err(|err| N34Error::InvalidEvent(format!(&amp;#34;Invalid event ID in `e` tag: {err}&amp;#34;)))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Utility functions for working with issue events&lt;br/&gt;#[easy_ext::ext(GitIssueUtils)]&lt;br/&gt;impl Event {&lt;br/&gt;    /// Gets the subject line of the issue or &amp;#34;N/A&amp;#34; if none exists&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_issue_subject(&amp;amp;self) -&amp;gt; &amp;amp;str {&lt;br/&gt;        self.tags&lt;br/&gt;            .find(TagKind::Subject)&lt;br/&gt;            .and_then(|t| t.content())&lt;br/&gt;            .unwrap_or(&amp;#34;N/A&amp;#34;)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets all issue labels formatted as comma-separated hashtags (e.g. &amp;#34;#bug,&lt;br/&gt;    /// #feature&amp;#34;)&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn extract_issue_labels(&amp;amp;self) -&amp;gt; String {&lt;br/&gt;        self.tags&lt;br/&gt;            .filter(TagKind::t())&lt;br/&gt;            .filter_map(|t| t.content().map(|l| format!(&amp;#34;#{l}&amp;#34;)))&lt;br/&gt;            .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;            .join(&amp;#34;, &amp;#34;)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(NostrKeyringErrorUtils)]&lt;br/&gt;impl nostr_keyring::Error {&lt;br/&gt;    /// Checks if the error indicates a missing keyring entry.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_keyring_no_entry(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, nostr_keyring::Error::Keyring(KeyringError::NoEntry))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:58Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqstk5nvqpj3ttp5hfejzyw52gvfxvkuuxcuq2ym89a3g9d6qrfhydczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvj2j289</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqstk5nvqpj3ttp5hfejzyw52gvfxvkuuxcuq2ym89a3g9d6qrfhydczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvj2j289" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Extension traits for nostr types.&lt;br/&gt;pub mod traits;&lt;br/&gt;/// Utility functions for nostr.&lt;br/&gt;pub mod utils;&lt;br/&gt;&lt;br/&gt;use std::{collections::HashSet, time::Duration};&lt;br/&gt;&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventId, Kind, Tag, TagKind, TagStandard, Tags, UnsignedEvent},&lt;br/&gt;    filter::Filter,&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    nips::{&lt;br/&gt;        nip01::{Coordinate, Metadata},&lt;br/&gt;        nip19::ToBech32,&lt;br/&gt;        nip22,&lt;br/&gt;        nip34::GitRepositoryAnnouncement,&lt;br/&gt;    },&lt;br/&gt;    parser::NostrParser,&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;use nostr_sdk::{Client, ClientOptions};&lt;br/&gt;use traits::TokenUtils;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, issue::IssueStatus, patch::PatchStatus},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Timeout duration for the client.&lt;br/&gt;const CLIENT_TIMEOUT: Duration = Duration::from_millis(1500);&lt;br/&gt;/// Length of a Nostr npub (public key) in characters.&lt;br/&gt;const NPUB_LEN: usize = 63;&lt;br/&gt;&lt;br/&gt;/// Parsed content details&lt;br/&gt;#[derive(Clone)]&lt;br/&gt;pub struct ContentDetails {&lt;br/&gt;    /// Public keys of users mentioned in the content.&lt;br/&gt;    pub p_tagged:     HashSet&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;    /// Event IDs and optional relay URLs for quoted events.&lt;br/&gt;    pub quotes:       HashSet&amp;lt;(EventId, Option&amp;lt;RelayUrl&amp;gt;)&amp;gt;,&lt;br/&gt;    /// Hashtags found in the content.&lt;br/&gt;    pub hashtags:     HashSet&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Relays where mentioned users and quoted authors are read.&lt;br/&gt;    pub write_relays: HashSet&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// A client for interacting with the Nostr relays&lt;br/&gt;#[derive(Clone)]&lt;br/&gt;pub struct NostrClient {&lt;br/&gt;    /// The underlying Nostr client implementation&lt;br/&gt;    pub client: Client,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl ContentDetails {&lt;br/&gt;    /// Create a new [`ContentDetails`] instance&lt;br/&gt;    pub fn new(&lt;br/&gt;        users: impl IntoIterator&amp;lt;Item = PublicKey&amp;gt;,&lt;br/&gt;        quotes: impl IntoIterator&amp;lt;Item = (EventId, Option&amp;lt;RelayUrl&amp;gt;)&amp;gt;,&lt;br/&gt;        hashtags: impl IntoIterator&amp;lt;Item = String&amp;gt;,&lt;br/&gt;        write_relays: impl IntoIterator&amp;lt;Item = RelayUrl&amp;gt;,&lt;br/&gt;    ) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            p_tagged:     HashSet::from_iter(users),&lt;br/&gt;            quotes:       HashSet::from_iter(quotes),&lt;br/&gt;            hashtags:     HashSet::from_iter(hashtags),&lt;br/&gt;            write_relays: HashSet::from_iter(write_relays),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Converts the instance into a list of tags including hashtags, p-tagged&lt;br/&gt;    /// users, and quoted events.&lt;br/&gt;    pub fn into_tags(self) -&amp;gt; Tags {&lt;br/&gt;        let mut tags = Tags::new();&lt;br/&gt;        tags.extend(self.hashtags.into_iter().map(Tag::hashtag));&lt;br/&gt;        tags.extend(self.p_tagged.into_iter().map(Tag::public_key));&lt;br/&gt;        tags.extend(self.quotes.into_iter().map(|(event_id, relay_url)| {&lt;br/&gt;            // TODO: Add the author public key if we know it&lt;br/&gt;            Tag::from_standardized(TagStandard::Quote {&lt;br/&gt;                event_id,&lt;br/&gt;                relay_url,&lt;br/&gt;                public_key: None,&lt;br/&gt;            })&lt;br/&gt;        }));&lt;br/&gt;        tags&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl NostrClient {&lt;br/&gt;    /// Creates a new [`NostrClient`] with the given client and options.&lt;br/&gt;    const fn new(client: Client) -&amp;gt; Self {&lt;br/&gt;        Self { client }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Initializes a new [`NostrClient`] instance and connects to the specified&lt;br/&gt;    /// relays.&lt;br/&gt;    pub async fn init(options: &amp;amp;CliOptions, relays: &amp;amp;[RelayUrl]) -&amp;gt; Self {&lt;br/&gt;        let mut client_builder =&lt;br/&gt;            Client::builder().opts(ClientOptions::new().verify_subscriptions(true));&lt;br/&gt;&lt;br/&gt;        if let Ok(Some(signer)) = options.signer().await {&lt;br/&gt;            client_builder = client_builder.signer(signer);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let client = Self::new(client_builder.build());&lt;br/&gt;&lt;br/&gt;        client.add_relays(relays).await;&lt;br/&gt;        client&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    //// Returns the users public key&lt;br/&gt;    pub async fn pubkey(&amp;amp;self) -&amp;gt; N34Result&amp;lt;PublicKey&amp;gt; {&lt;br/&gt;        self.client&lt;br/&gt;            .signer()&lt;br/&gt;            .await?&lt;br/&gt;            .get_public_key()&lt;br/&gt;            .await&lt;br/&gt;            .map_err(N34Error::SignerError)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Add relays and connect to them&lt;br/&gt;    pub async fn add_relays(&amp;amp;self, relays: &amp;amp;[RelayUrl]) {&lt;br/&gt;        if relays.is_empty() {&lt;br/&gt;            return;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let mut tasks = Vec::new();&lt;br/&gt;        for relay in relays {&lt;br/&gt;            let relay = relay.clone();&lt;br/&gt;            let client = self.client.clone();&lt;br/&gt;            tasks.push(tokio::spawn(async move {&lt;br/&gt;                client&lt;br/&gt;                    .add_relay(&amp;amp;relay)&lt;br/&gt;                    .await&lt;br/&gt;                    .expect(&amp;#34;It&amp;#39;s a valid relay url&amp;#34;);&lt;br/&gt;                if let Err(err) = client.try_connect_relay(&amp;amp;relay, CLIENT_TIMEOUT).await {&lt;br/&gt;                    tracing::error!(&amp;#34;Failed to connect to relay &amp;#39;{relay}&amp;#39;: {err}&amp;#34;);&lt;br/&gt;                }&lt;br/&gt;            }));&lt;br/&gt;        }&lt;br/&gt;        future::join_all(tasks).await;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Add a relay hint and connect to it&lt;br/&gt;    pub async fn add_relay_hint(&amp;amp;self, hint: Option&amp;lt;RelayUrl&amp;gt;) {&lt;br/&gt;        if let Some(relay) = hint {&lt;br/&gt;            self.add_relays(&amp;amp;[relay]).await&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// broadcast an event to the given relays&lt;br/&gt;    pub async fn broadcast(&amp;amp;self, event: &amp;amp;Event, relays: &amp;amp;[RelayUrl]) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        self.client.send_event_to(relays, event).await?;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Broadcasts an unsigned event to given relays, optionally broadcast the&lt;br/&gt;    /// relays list event. Returns URLs of relays that successfully received&lt;br/&gt;    /// the event.&lt;br/&gt;    pub async fn send_event_to(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        mut event: UnsignedEvent,&lt;br/&gt;        relays_list: Option&amp;lt;&amp;amp;Event&amp;gt;,&lt;br/&gt;        relays: &amp;amp;[RelayUrl],&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;RelayUrl&amp;gt;&amp;gt; {&lt;br/&gt;        self.add_relays(relays).await;&lt;br/&gt;        let event_id = event.id();&lt;br/&gt;&lt;br/&gt;        let (result, ..) = futures::join!(&lt;br/&gt;            async {&lt;br/&gt;                N34Result::Ok(&lt;br/&gt;                    self.client&lt;br/&gt;                        .send_event_to(relays, &amp;amp;event.sign(&amp;amp;self.client.signer().await?).await?)&lt;br/&gt;                        .await?,&lt;br/&gt;                )&lt;br/&gt;            },&lt;br/&gt;            async {&lt;br/&gt;                if let Some(event) = relays_list {&lt;br/&gt;                    let _ = self.client.send_event_to(relays, event).await;&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;        );&lt;br/&gt;        let result = result?;&lt;br/&gt;&lt;br/&gt;        for relay in &amp;amp;result.success {&lt;br/&gt;            tracing::info!(event_id = %event_id, relay = %relay, &amp;#34;Event sent successfully&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;        for (relay, reason) in &amp;amp;result.failed {&lt;br/&gt;            tracing::warn!(event_id = %event_id, relay = %relay, reason = %reason, &amp;#34;Failed to send event&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(result.success.into_iter().collect())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Fetches the first event matching the given filter, or None if no event&lt;br/&gt;    /// is found.&lt;br/&gt;    pub async fn fetch_event(&amp;amp;self, filter: Filter) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        Ok(self&lt;br/&gt;            .client&lt;br/&gt;            .fetch_events(filter.limit(1), CLIENT_TIMEOUT)&lt;br/&gt;            .await?&lt;br/&gt;            .first_owned())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Fetches the events matching the given filter&lt;br/&gt;    pub async fn fetch_events(&amp;amp;self, filter: Filter) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        // Multiply timeout by 5 to account for multiple events being fetched&lt;br/&gt;        Ok(self&lt;br/&gt;            .client&lt;br/&gt;            .fetch_events(filter, CLIENT_TIMEOUT * 5)&lt;br/&gt;            .await?&lt;br/&gt;            .to_vec())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Try to fetch the repositories and returns them&lt;br/&gt;    pub async fn fetch_repos(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        repo_naddrs: &amp;amp;[Coordinate],&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;GitRepositoryAnnouncement&amp;gt;&amp;gt; {&lt;br/&gt;        future::join_all(repo_naddrs.iter().map(|c| {&lt;br/&gt;            async {&lt;br/&gt;                self.fetch_event(&lt;br/&gt;                    Filter::new()&lt;br/&gt;                        .author(c.public_key)&lt;br/&gt;                        .identifier(&amp;amp;c.identifier)&lt;br/&gt;                        .kind(Kind::GitRepoAnnouncement),&lt;br/&gt;                )&lt;br/&gt;                .await?&lt;br/&gt;                .map(|e| utils::event_into_repo(e, &amp;amp;c.identifier))&lt;br/&gt;                .ok_or(N34Error::NotFoundRepo)&lt;br/&gt;            }&lt;br/&gt;        }))&lt;br/&gt;        .await&lt;br/&gt;        .into_iter()&lt;br/&gt;        .collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Fetch the patch by the given id. None if not found&lt;br/&gt;    pub async fn fetch_patch(&amp;amp;self, patch_id: EventId) -&amp;gt; N34Result&amp;lt;Event&amp;gt; {&lt;br/&gt;        self.fetch_event(Filter::new().id(patch_id).kind(Kind::GitPatch))&lt;br/&gt;            .await?&lt;br/&gt;            .ok_or(N34Error::CanNotFoundPatch)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the username for a given public key. If no username is found,&lt;br/&gt;    /// falls back to a shortened version of the public key.&lt;br/&gt;    pub async fn get_username(&amp;amp;self, user: PublicKey) -&amp;gt; String {&lt;br/&gt;        self.fetch_event(Filter::new().kind(Kind::Metadata).author(user))&lt;br/&gt;            .await&lt;br/&gt;            .ok()&lt;br/&gt;            .flatten()&lt;br/&gt;            .and_then(|e| Metadata::try_from(&amp;amp;e).ok())&lt;br/&gt;            .and_then(|m| m.display_name.or(m.name))&lt;br/&gt;            .unwrap_or_else(|| {&lt;br/&gt;                let pubkey = user.to_bech32().expect(&amp;#34;The error is `Infallible`&amp;#34;);&lt;br/&gt;                format!(&amp;#34;{}...{}&amp;#34;, &amp;amp;pubkey[..8], &amp;amp;pubkey[NPUB_LEN - 8..])&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Get the latest status of an issue by its ID, only considering status&lt;br/&gt;    /// events from authorized_pubkeys. If no valid status event is found,&lt;br/&gt;    /// defaults to Open.&lt;br/&gt;    pub async fn fetch_issue_status(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        issue_id: EventId,&lt;br/&gt;        authorized_pubkeys: Vec&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;IssueStatus&amp;gt; {&lt;br/&gt;        self.fetch_events(&lt;br/&gt;            Filter::new()&lt;br/&gt;                .event(issue_id)&lt;br/&gt;                .kinds([&lt;br/&gt;                    Kind::GitStatusOpen,&lt;br/&gt;                    Kind::GitStatusApplied,&lt;br/&gt;                    Kind::GitStatusClosed,&lt;br/&gt;                ])&lt;br/&gt;                .authors(utils::dedup(authorized_pubkeys.into_iter())),&lt;br/&gt;        )&lt;br/&gt;        .await?&lt;br/&gt;        .into_iter()&lt;br/&gt;        .max_by_key(|e| e.created_at)&lt;br/&gt;        .map(|status| IssueStatus::try_from(status.kind))&lt;br/&gt;        .unwrap_or_else(|| Ok(IssueStatus::Open))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the status of a patch. If it&amp;#39;s a revision patch, checks if it&amp;#39;s&lt;br/&gt;    /// closed when the root patch is already merged/applied but doesn&amp;#39;t&lt;br/&gt;    /// reference this revision. Defaults to Open status if no status event&lt;br/&gt;    /// is found.&lt;br/&gt;    pub async fn fetch_patch_status(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        root_patch: EventId,&lt;br/&gt;        root_revision: Option&amp;lt;EventId&amp;gt;,&lt;br/&gt;        authorized_pubkeys: Vec&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;PatchStatus&amp;gt; {&lt;br/&gt;        let (root_status, event_tags) = self&lt;br/&gt;            .fetch_events(&lt;br/&gt;                Filter::new()&lt;br/&gt;                    .event(root_patch)&lt;br/&gt;                    .kinds([&lt;br/&gt;                        Kind::GitStatusOpen,&lt;br/&gt;                        Kind::GitStatusApplied,&lt;br/&gt;                        Kind::GitStatusClosed,&lt;br/&gt;                        Kind::GitStatusDraft,&lt;br/&gt;                    ])&lt;br/&gt;                    .authors(utils::dedup(authorized_pubkeys.into_iter())),&lt;br/&gt;            )&lt;br/&gt;            .await?&lt;br/&gt;            .into_iter()&lt;br/&gt;            .max_by_key(|e| e.created_at)&lt;br/&gt;            .map(|status| N34Result::Ok((PatchStatus::try_from(status.kind)?, status.tags)))&lt;br/&gt;            .unwrap_or_else(|| Ok((PatchStatus::Open, Tags::new())))?;&lt;br/&gt;&lt;br/&gt;        if let Some(revision_id) = root_revision&lt;br/&gt;            &amp;amp;&amp;amp; root_status.is_merged_or_applied()&lt;br/&gt;            &amp;amp;&amp;amp; !event_tags&lt;br/&gt;                .filter(TagKind::e())&lt;br/&gt;                .any(|t| t.is_reply() &amp;amp;&amp;amp; t.content().is_some_and(|c| c == revision_id.to_hex()))&lt;br/&gt;        {&lt;br/&gt;            return Ok(PatchStatus::Closed);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(root_status)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    pub async fn fetch_patch_series(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        root_patch_id: EventId,&lt;br/&gt;        root_patch_author: PublicKey,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        Ok(self&lt;br/&gt;            .fetch_events(&lt;br/&gt;                Filter::new()&lt;br/&gt;                    .kind(Kind::GitPatch)&lt;br/&gt;                    .author(root_patch_author)&lt;br/&gt;                    .event(root_patch_id),&lt;br/&gt;            )&lt;br/&gt;            .await?&lt;br/&gt;            .into_iter()&lt;br/&gt;            .filter(|e| {&lt;br/&gt;                e.tags.iter().any(|t| {&lt;br/&gt;                    t.is_root() &amp;amp;&amp;amp; t.content().is_some_and(|c| c == root_patch_id.to_hex())&lt;br/&gt;                })&lt;br/&gt;            })&lt;br/&gt;            .collect())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finds the root issue or patch for a given event. If the event is already&lt;br/&gt;    /// a root (issue/patch), returns it directly. For comments, follows&lt;br/&gt;    /// parent/root references until finding the root or failing. Returns&lt;br/&gt;    /// None if no root can be found.&lt;br/&gt;    pub async fn find_root(&amp;amp;self, mut event: Event) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        if !matches!(event.kind, Kind::GitIssue | Kind::GitPatch | Kind::Comment) {&lt;br/&gt;            return Err(N34Error::CanNotReplyToEvent);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        loop {&lt;br/&gt;            if matches!(event.kind, Kind::GitIssue | Kind::GitPatch) {&lt;br/&gt;                return Ok(Some(event));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            if let Some(nip22::CommentTarget::Event { id, relay_hint, .. }) =&lt;br/&gt;                nip22::extract_root(&amp;amp;event)&lt;br/&gt;            {&lt;br/&gt;                self.add_relay_hint(relay_hint.cloned()).await;&lt;br/&gt;                let root_event = self.fetch_event(Filter::new().id(*id)).await?;&lt;br/&gt;                if let Some(ref root_event) = root_event&lt;br/&gt;                    &amp;amp;&amp;amp; !matches!(root_event.kind, Kind::GitIssue | Kind::GitPatch)&lt;br/&gt;                {&lt;br/&gt;                    return Err(N34Error::CanNotReplyToEvent);&lt;br/&gt;                }&lt;br/&gt;                return Ok(root_event);&lt;br/&gt;            } else if let Some(nip22::CommentTarget::Event { id, relay_hint, .. }) =&lt;br/&gt;                nip22::extract_parent(&amp;amp;event)&lt;br/&gt;            {&lt;br/&gt;                self.add_relay_hint(relay_hint.cloned()).await;&lt;br/&gt;                if let Ok(Some(parent_event)) = self.fetch_event(Filter::new().id(*id)).await {&lt;br/&gt;                    event = parent_event;&lt;br/&gt;                    continue;&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            // Break if: no root/parent tags found, parent/root event fetch failed&lt;br/&gt;            break;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(None)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Fetches the relay list (kind 10002) for the given user. Returns None if&lt;br/&gt;    /// no relays are found.&lt;br/&gt;    pub async fn user_relays_list(&amp;amp;self, user: PublicKey) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Event&amp;gt;&amp;gt; {&lt;br/&gt;        self.fetch_event(Filter::new().author(user).kind(Kind::RelayList))&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the author of the specified event, if found.&lt;br/&gt;    pub async fn event_author(&amp;amp;self, event_id: EventId) -&amp;gt; N34Result&amp;lt;Option&amp;lt;PublicKey&amp;gt;&amp;gt; {&lt;br/&gt;        Ok(self&lt;br/&gt;            .fetch_event(Filter::new().id(event_id))&lt;br/&gt;            .await?&lt;br/&gt;            .map(|e| e.pubkey))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the read relays of the given user if found, otherwise empty&lt;br/&gt;    /// vector&lt;br/&gt;    pub async fn read_relays_from_user(&amp;amp;self, user: PublicKey) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;        utils::add_read_relays(self.user_relays_list(user).await.ok().flatten().as_ref())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the read relays of the given users if found, otherwise empty&lt;br/&gt;    /// vector&lt;br/&gt;    pub async fn read_relays_from_users(&amp;amp;self, users: &amp;amp;[PublicKey]) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;        self.fetch_events(&lt;br/&gt;            Filter::new()&lt;br/&gt;                .kind(nostr::event::Kind::RelayList)&lt;br/&gt;                .authors(utils::dedup(users.iter().copied())),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .unwrap_or_default()&lt;br/&gt;        .into_iter()&lt;br/&gt;        .flat_map(|e| utils::add_read_relays(Some(&amp;amp;e)))&lt;br/&gt;        .collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Parse the given content and returns the details that inside it&lt;br/&gt;    pub async fn parse_content(&amp;amp;self, content: &amp;amp;str) -&amp;gt; ContentDetails {&lt;br/&gt;        let mut write_relays = Vec::new();&lt;br/&gt;        let tokens = NostrParser::new().parse(content).collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;&lt;br/&gt;        let mut p_tagged_users = tokens&lt;br/&gt;            .iter()&lt;br/&gt;            .filter_map(TokenUtils::extract_public_key)&lt;br/&gt;            .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;        let quotes = tokens&lt;br/&gt;            .iter()&lt;br/&gt;            .filter_map(TokenUtils::extract_event_id)&lt;br/&gt;            .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;        let hashtags = tokens&lt;br/&gt;            .iter()&lt;br/&gt;            .filter_map(TokenUtils::extract_hashtag)&lt;br/&gt;            .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;&lt;br/&gt;        for (user, relays) in &amp;amp;p_tagged_users {&lt;br/&gt;            self.add_relays(relays).await;&lt;br/&gt;            write_relays.extend(self.read_relays_from_user(*user).await);&lt;br/&gt;        }&lt;br/&gt;        for (event_id, relays) in &amp;amp;quotes {&lt;br/&gt;            self.add_relays(relays).await;&lt;br/&gt;            // Add the event author to the p-tagged users&lt;br/&gt;            if let Ok(Some(author)) = self.event_author(*event_id).await {&lt;br/&gt;                p_tagged_users.push((author, Vec::new()));&lt;br/&gt;                write_relays.extend(self.read_relays_from_user(author).await);&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        ContentDetails::new(&lt;br/&gt;            p_tagged_users.into_iter().map(|(p, _)| p),&lt;br/&gt;            quotes.into_iter().map(|(e, r)| (e, r.first().cloned())),&lt;br/&gt;            hashtags,&lt;br/&gt;            write_relays,&lt;br/&gt;        )&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:47Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsvgytnjlz09twmnklaru4d6t9ug3ez075e79f2w94c3gp5wezya2czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9sfhfy</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsvgytnjlz09twmnklaru4d6t9ug3ez075e79f2w94c3gp5wezya2czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9sfhfy" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        ConfigError,&lt;br/&gt;        traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, RelayOrSet},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct NewArgs {&lt;br/&gt;    /// Unique name for the set&lt;br/&gt;    name:   String,&lt;br/&gt;    /// Optional relay to add it to the set, either as URL or set name to&lt;br/&gt;    /// extract its relays. [aliases: `--sr`]&lt;br/&gt;    #[arg(long = &amp;#34;set-relay&amp;#34;, alias(&amp;#34;sr&amp;#34;))]&lt;br/&gt;    relays: Vec&amp;lt;RelayOrSet&amp;gt;,&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs: Vec&amp;lt;NaddrOrSet&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for NewArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?;&lt;br/&gt;        let relays = self.relays.flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;&lt;br/&gt;        if relays.is_empty() &amp;amp;&amp;amp; naddrs.is_empty() {&lt;br/&gt;            return Err(ConfigError::NewEmptySet.into());&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.sets.push_set(self.name, naddrs, relays)?;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:40Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsv95fk95z6kmq987kj2yqpvc69x9t85c67paapqvz0wn2xq3lkjsczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv2cr5dy</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsv95fk95z6kmq987kj2yqpvc69x9t85c67paapqvz0wn2xq3lkjsczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv2cr5dy" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Command line interface module&lt;br/&gt;pub mod cli;&lt;br/&gt;/// N34 errors&lt;br/&gt;pub mod error;&lt;br/&gt;/// Nostr utils module&lt;br/&gt;pub mod nostr_utils;&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    process::ExitCode,&lt;br/&gt;    sync::atomic::{AtomicBool, Ordering},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Parser;&lt;br/&gt;use clap_verbosity_flag::Verbosity;&lt;br/&gt;use tracing::Level;&lt;br/&gt;use tracing_subscriber::{Layer, filter, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;use self::cli::Cli;&lt;br/&gt;&lt;br/&gt;/// Whether the editor is currently open. Prevents logging while the editor is&lt;br/&gt;/// open.&lt;br/&gt;static EDITOR_OPEN: AtomicBool = AtomicBool::new(false);&lt;br/&gt;&lt;br/&gt;/// Configures the logging level based on the provided verbosity.&lt;br/&gt;///&lt;br/&gt;/// When verbosity is set to TRACE, includes file and line numbers in logs.&lt;br/&gt;fn set_log_level(verbosity: Verbosity) {&lt;br/&gt;    let is_trace = verbosity&lt;br/&gt;        .tracing_level()&lt;br/&gt;        .is_some_and(|l| l == tracing::Level::TRACE);&lt;br/&gt;&lt;br/&gt;    let logs_filter = filter::dynamic_filter_fn(move |m, _| {&lt;br/&gt;        // Disable all logs while editor is open&lt;br/&gt;        verbosity.tracing_level().unwrap_or(Level::ERROR) &amp;gt;= *m.level()&lt;br/&gt;            &amp;amp;&amp;amp; !EDITOR_OPEN.load(Ordering::Relaxed)&lt;br/&gt;    });&lt;br/&gt;&lt;br/&gt;    let logs_layer = tracing_subscriber::fmt::layer()&lt;br/&gt;        .with_file(is_trace)&lt;br/&gt;        .with_line_number(is_trace)&lt;br/&gt;        .without_time();&lt;br/&gt;    let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter));&lt;br/&gt;    tracing::subscriber::set_global_default(subscriber).ok();&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() -&amp;gt; ExitCode {&lt;br/&gt;    let cli = match cli::post_cli(Cli::parse()) {&lt;br/&gt;        Ok(cli) =&amp;gt; cli,&lt;br/&gt;        Err(err) =&amp;gt; {&lt;br/&gt;            eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;            return ExitCode::FAILURE;&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    set_log_level(cli.verbosity);&lt;br/&gt;&lt;br/&gt;    if let Err(err) = cli.run().await {&lt;br/&gt;        tracing::error!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;        return err.exit_code();&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    ExitCode::SUCCESS&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:35Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsyhtxdsqatm265evcv0hrtut22gmy3dpet38rtr97hc00gxaz803gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsjam5z</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsyhtxdsqatm265evcv0hrtut22gmy3dpet38rtr97hc00gxaz803gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsjam5z" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `sets new` command&lt;br/&gt;mod new;&lt;br/&gt;/// `sets remove` command&lt;br/&gt;mod remove;&lt;br/&gt;/// `sets show` commands&lt;br/&gt;mod show;&lt;br/&gt;/// `sets update` command&lt;br/&gt;mod update;&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;&lt;br/&gt;use self::new::NewArgs;&lt;br/&gt;use self::remove::RemoveArgs;&lt;br/&gt;use self::show::ShowArgs;&lt;br/&gt;use self::update::UpdateArgs;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::error::N34Result;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum SetsSubcommands {&lt;br/&gt;    /// Remove a set, or specific repos and relays within it&lt;br/&gt;    Remove(RemoveArgs),&lt;br/&gt;    /// Create a new set&lt;br/&gt;    New(NewArgs),&lt;br/&gt;    /// Modify an existing set&lt;br/&gt;    Update(UpdateArgs),&lt;br/&gt;    /// Show a single set or all the stored sets&lt;br/&gt;    Show(ShowArgs),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl CommandRunner for SetsSubcommands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::run_command!(self, options, &amp;amp; Remove New Update Show)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:29Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsq7vkrgjryp33pnayz4t93suwxaf72e2jyrft0e46t9m4eeswxfdqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvns0afj</id>
    
      <title type="html">#![allow(unused)] // n34 - A CLI to interact with NIP-34 and ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsq7vkrgjryp33pnayz4t93suwxaf72e2jyrft0e46t9m4eeswxfdqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvns0afj" />
    <content type="html">
      #![allow(unused)]&lt;br/&gt;// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Command line interface module&lt;br/&gt;pub mod cli;&lt;br/&gt;/// N34 errors&lt;br/&gt;pub mod error;&lt;br/&gt;/// Nostr utils module&lt;br/&gt;pub mod nostr_utils;&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    process::ExitCode,&lt;br/&gt;    sync::atomic::{AtomicBool, Ordering},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Parser;&lt;br/&gt;use clap_verbosity_flag::Verbosity;&lt;br/&gt;use tracing::Level;&lt;br/&gt;use tracing_subscriber::{Layer, filter, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;use self::cli::Cli;&lt;br/&gt;&lt;br/&gt;/// Whether the editor is currently open. Prevents logging while the editor is&lt;br/&gt;/// open.&lt;br/&gt;static EDITOR_OPEN: AtomicBool = AtomicBool::new(false);&lt;br/&gt;&lt;br/&gt;/// Configures the logging level based on the provided verbosity.&lt;br/&gt;///&lt;br/&gt;/// When verbosity is set to TRACE, includes file and line numbers in logs.&lt;br/&gt;fn set_log_level(verbosity: Verbosity) {&lt;br/&gt;    let is_trace = verbosity&lt;br/&gt;        .tracing_level()&lt;br/&gt;        .is_some_and(|l| l == tracing::Level::TRACE);&lt;br/&gt;&lt;br/&gt;    let logs_filter = filter::dynamic_filter_fn(move |m, _| {&lt;br/&gt;        // Disable all logs while editor is open&lt;br/&gt;        verbosity.tracing_level().unwrap_or(Level::ERROR) &amp;gt;= *m.level()&lt;br/&gt;            &amp;amp;&amp;amp; !EDITOR_OPEN.load(Ordering::Relaxed)&lt;br/&gt;    });&lt;br/&gt;&lt;br/&gt;    let logs_layer = tracing_subscriber::fmt::layer()&lt;br/&gt;        .with_file(is_trace)&lt;br/&gt;        .with_line_number(is_trace)&lt;br/&gt;        .without_time();&lt;br/&gt;    let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter));&lt;br/&gt;    tracing::subscriber::set_global_default(subscriber).ok();&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// ```rust&lt;br/&gt;// 	#[tokio::main]&lt;br/&gt;// 	async fn main() -&amp;gt; ExitCode {&lt;br/&gt;// 	    let cli = match cli::post_cli(Cli::parse()) {&lt;br/&gt;// 	        Ok(cli) =&amp;gt; cli,&lt;br/&gt;// 	        Err(err) =&amp;gt; {&lt;br/&gt;// 	            eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;// 	            return ExitCode::FAILURE;&lt;br/&gt;// 	        }&lt;br/&gt;// 	    };&lt;br/&gt;// 	&lt;br/&gt;// 	    set_log_level(cli.verbosity);&lt;br/&gt;// 	&lt;br/&gt;// 	    if let Err(err) = cli.run().await {&lt;br/&gt;// 	        tracing::error!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;// 	        return err.exit_code();&lt;br/&gt;// 	    }&lt;br/&gt;// 	&lt;br/&gt;// 	    ExitCode::SUCCESS&lt;br/&gt;// 	}&lt;br/&gt;// ```&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:24Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs876hdvc6m5mvaj2xqp664v79r8tralgzwgmfzp34ha7jpqzst04czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvckyvy</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs876hdvc6m5mvaj2xqp664v79r8tralgzwgmfzp34ha7jpqzst04czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvckyvy" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::fmt;&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::nips::nip19::ToBech32;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        CommandRunner,&lt;br/&gt;        traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::NaddrOrSet,&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;    nostr_utils::{NostrClient, traits::NaddrsUtils, utils},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Arguments for the `repo view` command&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct ViewArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;)]&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ViewArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(&lt;br/&gt;            self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;            &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;        )?)?;&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;        let repos = client.fetch_repos(&amp;amp;naddrs.into_coordinates()).await?;&lt;br/&gt;        let mut repos_details: Vec&amp;lt;String&amp;gt; = Vec::new();&lt;br/&gt;&lt;br/&gt;        for repo in repos {&lt;br/&gt;            let mut repo_details = format!(&amp;#34;ID: {}&amp;#34;, repo.id);&lt;br/&gt;&lt;br/&gt;            if let Some(name) = repo.name {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nName: {name}&amp;#34;));&lt;br/&gt;            }&lt;br/&gt;            if let Some(desc) = repo.description {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nDescription: {desc}&amp;#34;));&lt;br/&gt;            }&lt;br/&gt;            if !repo.web.is_empty() {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nWebpages:\n{}&amp;#34;, format_list(repo.web)));&lt;br/&gt;            }&lt;br/&gt;            if !repo.clone.is_empty() {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nClone urls:\n{}&amp;#34;, format_list(repo.clone)));&lt;br/&gt;            }&lt;br/&gt;            if !repo.relays.is_empty() {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nRelays:\n{}&amp;#34;, format_list(repo.relays)));&lt;br/&gt;            }&lt;br/&gt;            if let Some(euc) = repo.euc {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nEarliest unique commit: {euc}&amp;#34;));&lt;br/&gt;            }&lt;br/&gt;            if !repo.maintainers.is_empty() {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&lt;br/&gt;                    &amp;#34;\nMaintainers:\n{}&amp;#34;,&lt;br/&gt;                    format_list(&lt;br/&gt;                        repo.maintainers&lt;br/&gt;                            .iter()&lt;br/&gt;                            .map(|p| p.to_bech32().expect(&amp;#34;Infallible&amp;#34;))&lt;br/&gt;                    )&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;            repos_details.push(repo_details);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;{}&amp;#34;, repos_details.join(&amp;#34;\n----------\n&amp;#34;));&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Format a vector to print it&lt;br/&gt;fn format_list&amp;lt;I, T&amp;gt;(iterator: I) -&amp;gt; String&lt;br/&gt;where&lt;br/&gt;    I: IntoIterator&amp;lt;Item = T&amp;gt;,&lt;br/&gt;    T: fmt::Display,&lt;br/&gt;{&lt;br/&gt;    iterator&lt;br/&gt;        .into_iter()&lt;br/&gt;        .map(|t| format!(&amp;#34; - {t}&amp;#34;))&lt;br/&gt;        .collect::&amp;lt;Vec&amp;lt;String&amp;gt;&amp;gt;()&lt;br/&gt;        .join(&amp;#34;\n&amp;#34;)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:17Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsfws8htgl8axyhu4qmzlxrs2jjwcrg5cjssalxk8gl4epyg22p70gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0z2dkr</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsfws8htgl8axyhu4qmzlxrs2jjwcrg5cjssalxk8gl4epyg22p70gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0z2dkr" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{net::AddrParseError, process::ExitCode};&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Kind, builder::Error as EventBuilderError},&lt;br/&gt;    signer::SignerError,&lt;br/&gt;};&lt;br/&gt;use nostr_sdk::client::Error as ClientError;&lt;br/&gt;&lt;br/&gt;use crate::cli::ConfigError;&lt;br/&gt;&lt;br/&gt;/// The input data was incorrect in some way. This should only be used for&lt;br/&gt;/// user’s data and not system file.&lt;br/&gt;const DATA_ERROR: u8 = 65;&lt;br/&gt;&lt;br/&gt;/// An internal software error has been detected. This should be limited to&lt;br/&gt;/// non-operating system related errors.&lt;br/&gt;const SOFTWARE_ERROR: u8 = 70;&lt;br/&gt;&lt;br/&gt;/// An error occurred while doing I/O on some file.&lt;br/&gt;const IO_ERROR: u8 = 74;&lt;br/&gt;&lt;br/&gt;/// Something was found in an unconfigured or misconfigured state.&lt;br/&gt;const CONFIG_ERROR: u8 = 78;&lt;br/&gt;&lt;br/&gt;pub type N34Result&amp;lt;T&amp;gt; = Result&amp;lt;T, N34Error&amp;gt;;&lt;br/&gt;&lt;br/&gt;/// N34 errors&lt;br/&gt;#[derive(Debug, thiserror::Error)]&lt;br/&gt;pub enum N34Error {&lt;br/&gt;    #[error(&amp;#34;IO: {0}&amp;#34;)]&lt;br/&gt;    Io(#[from] std::io::Error),&lt;br/&gt;    #[error(&amp;#34;Signer Error: {0}&amp;#34;)]&lt;br/&gt;    SignerError(#[from] SignerError),&lt;br/&gt;    #[error(&amp;#34;Invalid Browser Signer Proxy Address: {0}&amp;#34;)]&lt;br/&gt;    Addr(#[from] AddrParseError),&lt;br/&gt;    #[error(&amp;#34;Browser Signer Proxy Error: {0}&amp;#34;)]&lt;br/&gt;    BrowserSignerProxy(#[from] nostr_browser_signer_proxy::Error),&lt;br/&gt;    #[error(&amp;#34;Keyring error: {0}&amp;#34;)]&lt;br/&gt;    Keyring(#[from] nostr_keyring::Error),&lt;br/&gt;    #[error(&amp;#34;{0}&amp;#34;)]&lt;br/&gt;    Config(#[from] ConfigError),&lt;br/&gt;    #[error(&amp;#34;No editor specified in the `EDITOR` environment variable&amp;#34;)]&lt;br/&gt;    EditorNotFound,&lt;br/&gt;    #[error(&amp;#34;The file you edited is empty. Please save your changes before exiting the editor.&amp;#34;)]&lt;br/&gt;    EmptyEditorFile,&lt;br/&gt;    #[error(&amp;#34;The editor `{0}` exit with unsuccessful exit code `{1}`&amp;#34;)]&lt;br/&gt;    EditorErr(String, i32),&lt;br/&gt;    #[error(&amp;#34;Client Error: {0}&amp;#34;)]&lt;br/&gt;    Client(#[from] ClientError),&lt;br/&gt;    #[error(&amp;#34;Unable to locate the repository. The repository may not exists in the given relays&amp;#34;)]&lt;br/&gt;    NotFoundRepo,&lt;br/&gt;    #[error(&amp;#34;Failed building an event: {0}&amp;#34;)]&lt;br/&gt;    EventBuilder(#[from] EventBuilderError),&lt;br/&gt;    #[error(&amp;#34;Invalid repository id, it can&amp;#39;t be empty and must be kebab-case&amp;#34;)]&lt;br/&gt;    InvalidRepoId,&lt;br/&gt;    #[error(&amp;#34;Invalid event: {0}&amp;#34;)]&lt;br/&gt;    InvalidEvent(String),&lt;br/&gt;    #[error(&amp;#34;Bech32 error: {0}&amp;#34;)]&lt;br/&gt;    Bech32(#[from] nostr::nips::nip19::Error),&lt;br/&gt;    #[error(&amp;#34;Event error: {0}&amp;#34;)]&lt;br/&gt;    Event(#[from] nostr::event::Error),&lt;br/&gt;    #[error(&amp;#34;Event not found in the specified relays&amp;#34;)]&lt;br/&gt;    EventNotFound,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Can&amp;#39;t reply to this event. Only Git issues, patches, and their comments can be replied \&lt;br/&gt;         to.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    CanNotReplyToEvent,&lt;br/&gt;    #[error(&amp;#34;No repository address given and couldn&amp;#39;t read `nostr-address` file: {0}&amp;#34;)]&lt;br/&gt;    CanNotReadNostrAddressFile(std::io::Error),&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;The `nostr-address` file is empty.  Please add a valid Nostr repository address (naddr) \&lt;br/&gt;         to the file or provide it manually as a flag.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    EmptyNostrAddressFile,&lt;br/&gt;    #[error(&amp;#34;Invalid `nostr-address` file content: {0}&amp;#34;)]&lt;br/&gt;    InvalidNostrAddressFileContent(String),&lt;br/&gt;    #[error(&amp;#34;This command requires at least one relay, but none were provided&amp;#34;)]&lt;br/&gt;    EmptyRelays,&lt;br/&gt;    #[error(&amp;#34;One naddr is required for this command&amp;#34;)]&lt;br/&gt;    EmptyNaddrs,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;This command requires a signer to sign events. Use `--secret-key`, `--nip07` or \&lt;br/&gt;         `--bunker-url` to provide a signer&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    SignerRequired,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Invalid repository address. Expected one of these formats:\n- NIP-05 identifier with \&lt;br/&gt;         repository ID: `&amp;lt;user@domain.com&amp;gt;/&amp;lt;repo_id&amp;gt;`\n- Valid NIP-19 naddr string (starts with \&lt;br/&gt;         &amp;#39;naddr1...&amp;#39;)\n- Existing set name (merges all repositories in set)\nError: No set named \&lt;br/&gt;         &amp;#39;{0}&amp;#39; exists.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    InvalidNaddrArg(String),&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Invalid relays. Expected a relay url or a set name that contains some relays\nError: No \&lt;br/&gt;         set named &amp;#39;{0}&amp;#39; exists.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    InvalidRelaysArg(String),&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;The set &amp;#39;{0}&amp;#39; doesn&amp;#39;t contain any addresses. Use &amp;#39;sets update&amp;#39; to add addresses to it.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    EmptySetNaddrs(String),&lt;br/&gt;    #[error(&amp;#34;The set &amp;#39;{0}&amp;#39; doesn&amp;#39;t contain any relays. Use &amp;#39;sets update&amp;#39; to add relays to it.&amp;#34;)]&lt;br/&gt;    EmptySetRelays(String),&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Issue not found, make sure it is in the relays and make sure that the ID is an issue ID&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    CanNotFoundIssue,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Patch not found, make sure it is in the relays and make sure that the ID is an patch ID&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    CanNotFoundPatch,&lt;br/&gt;    #[error(r#&amp;#34;The given patch id is not a root patch. It must contains `[&amp;#34;t&amp;#34;, &amp;#34;root&amp;#34;]` tag&amp;#34;#)]&lt;br/&gt;    NotRootPatch,&lt;br/&gt;    #[error(&amp;#34;This status kind can&amp;#39;t be set for an issue: {0}&amp;#34;)]&lt;br/&gt;    InvalidIssueStatus(Kind),&lt;br/&gt;    #[error(&amp;#34;This status kind can&amp;#39;t be set for a patch: {0}&amp;#34;)]&lt;br/&gt;    InvalidPatchStatus(Kind),&lt;br/&gt;    #[error(&amp;#34;Can&amp;#39;t find the root patch of the given patch-revision&amp;#34;)]&lt;br/&gt;    RevisionRootNotFound,&lt;br/&gt;    #[error(&amp;#34;Invalid status for the issue/patch: {0}&amp;#34;)]&lt;br/&gt;    InvalidStatus(String),&lt;br/&gt;    #[error(&amp;#34;Not valid bunker URL&amp;#34;)]&lt;br/&gt;    NotBunkerUrl,&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;No secret key found in the keyring. Please use the secret key at least once while \&lt;br/&gt;         keyring is enabled to store it&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    SecretKeyKeyringWithoutEntry,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl N34Error {&lt;br/&gt;    /// Returns the exit code associated with this error&lt;br/&gt;    pub fn exit_code(&amp;amp;self) -&amp;gt; ExitCode {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Io(_) | Self::CanNotReadNostrAddressFile(_) =&amp;gt; ExitCode::from(IO_ERROR),&lt;br/&gt;            Self::Config(_) =&amp;gt; ExitCode::from(CONFIG_ERROR),&lt;br/&gt;            Self::EditorErr(..) =&amp;gt; ExitCode::from(SOFTWARE_ERROR),&lt;br/&gt;            Self::InvalidRepoId&lt;br/&gt;            | Self::EmptyNostrAddressFile&lt;br/&gt;            | Self::InvalidNostrAddressFileContent(_)&lt;br/&gt;            | Self::EmptyRelays&lt;br/&gt;            | Self::EmptyNaddrs&lt;br/&gt;            | Self::SignerRequired&lt;br/&gt;            | Self::InvalidNaddrArg(_)&lt;br/&gt;            | Self::InvalidRelaysArg(_)&lt;br/&gt;            | Self::EmptySetNaddrs(_)&lt;br/&gt;            | Self::EmptySetRelays(_)&lt;br/&gt;            | Self::NotRootPatch =&amp;gt; ExitCode::from(DATA_ERROR),&lt;br/&gt;            _ =&amp;gt; ExitCode::FAILURE,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:12Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqst9jjf06hm68ez0ly3f06u842c68q63ny8fgx7hcg7aycgannyv7qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhjrtlq</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqst9jjf06hm68ez0ly3f06u842c68q63ny8fgx7hcg7aycgannyv7qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhjrtlq" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `repo announce` subcommand&lt;br/&gt;mod announce;&lt;br/&gt;/// `repo view` subcommand&lt;br/&gt;mod view;&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;&lt;br/&gt;use self::announce::AnnounceArgs;&lt;br/&gt;use self::view::ViewArgs;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::error::N34Result;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum RepoSubcommands {&lt;br/&gt;    /// View details of a nostr git repository&lt;br/&gt;    View(ViewArgs),&lt;br/&gt;    /// Broadcast and update a git repository&lt;br/&gt;    Announce(AnnounceArgs),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for RepoSubcommands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::run_command!(self, options, &amp;amp; View Announce)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:06Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsfg6q29rvz9dqfw283nyw35uxhxds84uj29sakddy2gmvr3j664sgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfh26al</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsfg6q29rvz9dqfw283nyw35uxhxds84uj29sakddy2gmvr3j664sgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfh26al" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::str::FromStr;&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{EventId, Kind},&lt;br/&gt;    nips::{&lt;br/&gt;        nip01::Coordinate,&lt;br/&gt;        nip05::{Nip05Address, Nip05Profile},&lt;br/&gt;        nip19::{self, FromBech32, Nip19Coordinate},&lt;br/&gt;    },&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;    util::BoxedFuture,&lt;br/&gt;};&lt;br/&gt;use nostr_connect::client::AuthUrlHandler;&lt;br/&gt;use tokio::runtime::Handle;&lt;br/&gt;&lt;br/&gt;use super::parsers;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{RepoRelaySet, traits::RepoRelaySetsExt},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Either a NIP-19 coordinate (naddr) or a named set.&lt;br/&gt;#[derive(Debug, Clone)]&lt;br/&gt;pub enum NaddrOrSet {&lt;br/&gt;    /// NIP-19 coordinate.&lt;br/&gt;    Naddr(Nip19Coordinate),&lt;br/&gt;    /// Name of a set (may not exist).&lt;br/&gt;    Set(String),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Either relay URL or a named set.&lt;br/&gt;#[derive(Debug, Clone)]&lt;br/&gt;pub enum RelayOrSet {&lt;br/&gt;    /// Relay URL.&lt;br/&gt;    Relay(RelayUrl),&lt;br/&gt;    /// Name of a set (may not exist).&lt;br/&gt;    Set(String),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses and represents a Nostr `nevent1` or `note1`.&lt;br/&gt;#[derive(Debug, Clone)]&lt;br/&gt;pub struct NostrEvent {&lt;br/&gt;    /// Unique identifier for the event.&lt;br/&gt;    pub event_id: EventId,&lt;br/&gt;    /// List of relay URLs associated with the event. Empty if parsing a&lt;br/&gt;    /// `note1`.&lt;br/&gt;    pub relays:   Vec&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub struct EchoAuthUrl;&lt;br/&gt;&lt;br/&gt;impl AuthUrlHandler for EchoAuthUrl {&lt;br/&gt;    fn on_auth_url(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        auth_url: nostr::Url,&lt;br/&gt;    ) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;_, Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt;&amp;gt; {&lt;br/&gt;        Box::pin(async move {&lt;br/&gt;            println!(&amp;#34;The bunker requires authentication. Please open this URL: {auth_url}&amp;#34;);&lt;br/&gt;            Ok(())&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl NaddrOrSet {&lt;br/&gt;    /// Returns the naddr if `Naddr` or try to get the relays from the set.&lt;br/&gt;    /// Returns error if the set naddrs are empty or the set not found.&lt;br/&gt;    pub fn get_naddrs(self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Naddr(nip19_coordinate) =&amp;gt; Ok(vec![nip19_coordinate]),&lt;br/&gt;            Self::Set(name) =&amp;gt; {&lt;br/&gt;                let set = sets&lt;br/&gt;                    .get_set(&amp;amp;name)&lt;br/&gt;                    .map_err(|_| N34Error::InvalidNaddrArg(name.clone()))?;&lt;br/&gt;                if set.naddrs.is_empty() {&lt;br/&gt;                    Err(N34Error::EmptySetNaddrs(name))&lt;br/&gt;                } else {&lt;br/&gt;                    Ok(Vec::from_iter(set.naddrs.clone()))&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl RelayOrSet {&lt;br/&gt;    /// Returns the relay if `Relay` or try to get the relays from the set.&lt;br/&gt;    /// Returns error if the set relays are empty or the set not found&lt;br/&gt;    pub fn get_relays(self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;RelayUrl&amp;gt;&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Relay(relay) =&amp;gt; Ok(vec![relay]),&lt;br/&gt;            Self::Set(name) =&amp;gt; {&lt;br/&gt;                let set = sets&lt;br/&gt;                    .get_set(&amp;amp;name)&lt;br/&gt;                    .map_err(|_| N34Error::InvalidRelaysArg(name.clone()))?;&lt;br/&gt;                if set.relays.is_empty() {&lt;br/&gt;                    Err(N34Error::EmptySetRelays(name))&lt;br/&gt;                } else {&lt;br/&gt;                    Ok(Vec::from_iter(set.relays.clone()))&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl NostrEvent {&lt;br/&gt;    /// Create a new [`NostrEvent`] instance&lt;br/&gt;    fn new(event_id: EventId, relays: Vec&amp;lt;RelayUrl&amp;gt;) -&amp;gt; Self {&lt;br/&gt;        Self { event_id, relays }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl FromStr for NaddrOrSet {&lt;br/&gt;    type Err = String;&lt;br/&gt;&lt;br/&gt;    /// Parses a Git repository address which can be either:&lt;br/&gt;    /// - A bech32-encoded naddr (e.g. &amp;#34;naddr1...&amp;#34;) for Git repository&lt;br/&gt;    ///   announcements (kind 30617)&lt;br/&gt;    /// - A NIP-05 identifier with repository ID (e.g. &amp;#34;4rs.nl/n34&amp;#34; or&lt;br/&gt;    ///   &amp;#34;_@4rs.nl/n34&amp;#34;)&lt;br/&gt;    /// - A set name.&lt;br/&gt;    ///&lt;br/&gt;    /// Returns an error for invalid formats, failed bech32 decoding, wrong&lt;br/&gt;    /// event kind.&lt;br/&gt;    fn from_str(naddr_or_set: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, Self::Err&amp;gt; {&lt;br/&gt;        let naddr_or_set = naddr_or_set.trim();&lt;br/&gt;&lt;br/&gt;        if naddr_or_set.contains(&amp;#34;/&amp;#34;) {&lt;br/&gt;            let (nip5, repo_id) = naddr_or_set.split_once(&amp;#34;/&amp;#34;).expect(&amp;#34;There is a `/`&amp;#34;);&lt;br/&gt;            parse_nip5_repo(nip5, repo_id)&lt;br/&gt;        } else if naddr_or_set.starts_with(&amp;#34;naddr1&amp;#34;) || naddr_or_set.starts_with(&amp;#34;nostr:naddr1&amp;#34;) {&lt;br/&gt;            parsers::parse_repo_naddr(naddr_or_set.trim_start_matches(&amp;#34;nostr:&amp;#34;)).map(Self::Naddr)&lt;br/&gt;        } else {&lt;br/&gt;            Ok(Self::Set(naddr_or_set.to_owned()))&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl FromStr for RelayOrSet {&lt;br/&gt;    type Err = String;&lt;br/&gt;&lt;br/&gt;    /// Parse a string into a relay URL or a set name.&lt;br/&gt;    /// If the string is a valid URL (e.g., &amp;#34;wss://example.com&amp;#34;), it&amp;#39;s treated&lt;br/&gt;    /// as a relay URL. Otherwise, it&amp;#39;s treated as a set name, and its&lt;br/&gt;    /// associated relays will be merged.&lt;br/&gt;    fn from_str(relay_or_set: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, Self::Err&amp;gt; {&lt;br/&gt;        let relay_or_set = relay_or_set.trim();&lt;br/&gt;&lt;br/&gt;        if relay_or_set.starts_with(&amp;#34;wss://&amp;#34;) {&lt;br/&gt;            RelayUrl::from_str(relay_or_set)&lt;br/&gt;                .map_err(|err| err.to_string())&lt;br/&gt;                .map(Self::Relay)&lt;br/&gt;        } else {&lt;br/&gt;            Ok(Self::Set(relay_or_set.to_owned()))&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl FromStr for NostrEvent {&lt;br/&gt;    type Err = String;&lt;br/&gt;&lt;br/&gt;    fn from_str(s: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, Self::Err&amp;gt; {&lt;br/&gt;        let str_event = s.trim().trim_start_matches(&amp;#34;nostr:&amp;#34;);&lt;br/&gt;        if str_event.starts_with(&amp;#34;nevent1&amp;#34;) {&lt;br/&gt;            let event = nip19::Nip19Event::from_bech32(str_event).map_err(|e| e.to_string())?;&lt;br/&gt;            Ok(Self::new(event.event_id, event.relays))&lt;br/&gt;        } else if str_event.starts_with(&amp;#34;note1&amp;#34;) {&lt;br/&gt;            Ok(Self::new(&lt;br/&gt;                EventId::from_bech32(str_event).map_err(|e| e.to_string())?,&lt;br/&gt;                Vec::new(),&lt;br/&gt;            ))&lt;br/&gt;        } else {&lt;br/&gt;            Err(&amp;#34;Invalid event id, must starts with `note1` or `nevent1`&amp;#34;.to_owned())&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn parse_nip5_repo(nip5: &amp;amp;str, repo_id: &amp;amp;str) -&amp;gt; Result&amp;lt;NaddrOrSet, String&amp;gt; {&lt;br/&gt;    let (username, domain) = nip5.split_once(&amp;#34;@&amp;#34;).unwrap_or((&amp;#34;_&amp;#34;, nip5));&lt;br/&gt;&lt;br/&gt;    let nip5_address =&lt;br/&gt;        Nip05Address::parse(&amp;amp;format!(&amp;#34;{username}@{domain}&amp;#34;)).map_err(|err| err.to_string())?;&lt;br/&gt;&lt;br/&gt;    let nip5_json = tokio::task::block_in_place(|| {&lt;br/&gt;        Handle::current().block_on(async {&lt;br/&gt;            reqwest::get(nip5_address.url().as_str())&lt;br/&gt;                .await&lt;br/&gt;                .map_err(|err| err.to_string())?&lt;br/&gt;                .text()&lt;br/&gt;                .await&lt;br/&gt;                .map_err(|err| err.to_string())&lt;br/&gt;        })&lt;br/&gt;    })?;&lt;br/&gt;&lt;br/&gt;    let nip5_profile =&lt;br/&gt;        Nip05Profile::from_raw_json(&amp;amp;nip5_address, &amp;amp;nip5_json).map_err(|err| err.to_string())?;&lt;br/&gt;&lt;br/&gt;    Ok(NaddrOrSet::Naddr(Nip19Coordinate::new(&lt;br/&gt;        Coordinate::new(Kind::GitRepoAnnouncement, nip5_profile.public_key).identifier(repo_id),&lt;br/&gt;        nip5_profile.relays,&lt;br/&gt;    )))&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:48:01Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsrq9jqj3tzhpvwc8hk8nraax0m4ajmkumpkzdcj2y3s8vwawcn5ugzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhy47eu</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsrq9jqj3tzhpvwc8hk8nraax0m4ajmkumpkzdcj2y3s8vwawcn5ugzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhy47eu" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{fs, io::Write};&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{event::EventBuilder, key::PublicKey, types::Url};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, traits::RelayOrSetVecExt},&lt;br/&gt;    error::N34Result,&lt;br/&gt;    nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Header written to new `nostr-address` files. Contains two trailing newline&lt;br/&gt;/// for formatting.&lt;br/&gt;const NOSTR_ADDRESS_FILE_HEADER: &amp;amp;str = r##&amp;#34;# This file contains NIP-19 `naddr` entities for repositories that accept this&lt;br/&gt;# project&amp;#39;s issues and patches.&lt;br/&gt;#&lt;br/&gt;# The file acts as a **read-only reference** for retrieving repository relays&lt;br/&gt;# when embedded in an `naddr` and mentions those repositories when opening&lt;br/&gt;# patches or issues. Modifications here will not affect in the relays, as the&lt;br/&gt;# file is **explicitly untracked**. Its goal is to simplify contributions by&lt;br/&gt;# removing the need for manual address entry.&lt;br/&gt;#&lt;br/&gt;# Each entry must start with &amp;#34;naddr&amp;#34;. Embedded relays are **strongly recommended**&lt;br/&gt;# to assist client-side discovery.&lt;br/&gt;#&lt;br/&gt;# Empty lines are ignored. Lines starting with &amp;#34;#&amp;#34; are treated as comments.&lt;br/&gt;&lt;br/&gt;&amp;#34;##;&lt;br/&gt;&lt;br/&gt;/// Arguments for the `repo announce` command&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct AnnounceArgs {&lt;br/&gt;    /// Unique identifier for the repository in kebab-case.&lt;br/&gt;    #[arg(long = &amp;#34;id&amp;#34;)]&lt;br/&gt;    repo_id:      String,&lt;br/&gt;    /// A name for the repository.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    name:         Option&amp;lt;String&amp;gt;,&lt;br/&gt;    /// A description for the repository.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    description:  Option&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Webpage URLs for the repository (if provided by the git server).&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    web:          Vec&amp;lt;Url&amp;gt;,&lt;br/&gt;    /// URLs for cloning the repository.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    clone:        Vec&amp;lt;Url&amp;gt;,&lt;br/&gt;    /// Additional maintainers of the repository (besides yourself).&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    maintainers:  Vec&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;    /// Labels to categorize the repository. Can be specified multiple times.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    label:        Vec&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Skip kebab-case validation for the repository ID&lt;br/&gt;    #[arg(long)]&lt;br/&gt;    force_id:     bool,&lt;br/&gt;    /// If set, creates a `nostr-address` file to enable automatic address&lt;br/&gt;    /// discovery by n34&lt;br/&gt;    #[arg(long)]&lt;br/&gt;    address_file: bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for AnnounceArgs {&lt;br/&gt;    const NEED_RELAYS: bool = true;&lt;br/&gt;&lt;br/&gt;    async fn run(mut self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        let user_pubk = client.pubkey().await?;&lt;br/&gt;        let relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;        client&lt;br/&gt;            .add_relays(&amp;amp;utils::add_read_relays(relays_list.as_ref()))&lt;br/&gt;            .await;&lt;br/&gt;&lt;br/&gt;        if !self.maintainers.contains(&amp;amp;user_pubk) {&lt;br/&gt;            self.maintainers.insert(0, user_pubk);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let naddr = utils::repo_naddr(&amp;amp;self.repo_id, user_pubk, &amp;amp;relays)?;&lt;br/&gt;        let event = EventBuilder::new_git_repo(&lt;br/&gt;            self.repo_id,&lt;br/&gt;            self.name.map(utils::str_trim),&lt;br/&gt;            self.description.map(utils::str_trim),&lt;br/&gt;            self.web,&lt;br/&gt;            self.clone,&lt;br/&gt;            relays.clone(),&lt;br/&gt;            self.maintainers.clone(),&lt;br/&gt;            self.label.into_iter().map(utils::str_trim).collect(),&lt;br/&gt;            self.force_id,&lt;br/&gt;        )?&lt;br/&gt;        .dedup_tags()&lt;br/&gt;        .pow(options.pow.unwrap_or_default())&lt;br/&gt;        .build(user_pubk);&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;        if self.address_file {&lt;br/&gt;            let address_path = std::env::current_dir()?.join(NOSTR_ADDRESS_FILE);&lt;br/&gt;            if !address_path.exists() {&lt;br/&gt;                tracing::info!(&lt;br/&gt;                    &amp;#34;Creating new address file: &amp;#39;{NOSTR_ADDRESS_FILE}&amp;#39; at path &amp;#39;{}&amp;#39; with default \&lt;br/&gt;                     header&amp;#34;,&lt;br/&gt;                    address_path.display()&lt;br/&gt;                );&lt;br/&gt;                fs::write(&amp;amp;address_path, NOSTR_ADDRESS_FILE_HEADER)?;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let mut file = fs::OpenOptions::new().append(true).open(&amp;amp;address_path)?;&lt;br/&gt;&lt;br/&gt;            tracing::info!(&amp;#34;Appending naddr &amp;#39;{naddr}&amp;#39; to address file: &amp;#39;{NOSTR_ADDRESS_FILE}&amp;#39;&amp;#34;);&lt;br/&gt;            file.write_all(format!(&amp;#34;{naddr}\n&amp;#34;).as_bytes())?;&lt;br/&gt;            tracing::info!(&amp;#34;Successfully wrote naddr to address file&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let write_relays = [&lt;br/&gt;            relays,&lt;br/&gt;            utils::add_write_relays(relays_list.as_ref()),&lt;br/&gt;            // Include read relays for each maintainer (if found)&lt;br/&gt;            future::join_all(&lt;br/&gt;                self.maintainers&lt;br/&gt;                    .iter()&lt;br/&gt;                    .map(|pkey| client.read_relays_from_user(*pkey)),&lt;br/&gt;            )&lt;br/&gt;            .await&lt;br/&gt;            .into_iter()&lt;br/&gt;            .flatten()&lt;br/&gt;            .collect(),&lt;br/&gt;        ]&lt;br/&gt;        .concat();&lt;br/&gt;        let nevent = utils::new_nevent(event.id.expect(&amp;#34;There is an id&amp;#34;), &amp;amp;write_relays)?;&lt;br/&gt;&lt;br/&gt;        client&lt;br/&gt;            .send_event_to(event, relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;            .await?;&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;Event: {nevent}&amp;#34;,);&lt;br/&gt;        println!(&amp;#34;Repo Address: {naddr}&amp;#34;,);&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:47:55Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsdy7dza0v2qrja2j6ddmkutjvkypep4avvdkzcue4snzrhkqzvetszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvkf2k3w</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsdy7dza0v2qrja2j6ddmkutjvkypep4avvdkzcue4snzrhkqzvetszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvkf2k3w" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use nostr::{event::EventId, nips::nip19::Nip19Coordinate, types::RelayUrl};&lt;br/&gt;&lt;br/&gt;use super::CliOptions;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        ConfigError,&lt;br/&gt;        RepoRelaySet,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent, RelayOrSet},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// A trait defining the interface for command runners in the CLI.&lt;br/&gt;pub trait CommandRunner {&lt;br/&gt;    /// Whether this command needs the relays option (false by default).&lt;br/&gt;    /// Only applies to commands, not subcommands.&lt;br/&gt;    const NEED_RELAYS: bool = false;&lt;br/&gt;    /// Indicates if this command requires the signer. Defaults to true.&lt;br/&gt;    /// Only applies to commands, not subcommands.&lt;br/&gt;    const NEED_SIGNER: bool = true;&lt;br/&gt;&lt;br/&gt;    /// Executes the command and returns a Result indicating success or failure.&lt;br/&gt;    fn run(self, options: CliOptions) -&amp;gt; impl Future&amp;lt;Output = N34Result&amp;lt;()&amp;gt;&amp;gt; &#43; Send;&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(VecNostrEventExt)]&lt;br/&gt;impl Vec&amp;lt;NostrEvent&amp;gt; {&lt;br/&gt;    /// Extracts `EventId` from each `NostrEvent` and collects them into a&lt;br/&gt;    /// `Vec&amp;lt;EventId&amp;gt;`.&lt;br/&gt;    pub fn into_event_ids(self) -&amp;gt; Vec&amp;lt;EventId&amp;gt; {&lt;br/&gt;        self.into_iter().map(|e| e.event_id).collect()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(NaddrOrSetVecExt)]&lt;br/&gt;impl Vec&amp;lt;NaddrOrSet&amp;gt; {&lt;br/&gt;    /// Converts this vector of [`NaddrOrSet`] into a flat vector of&lt;br/&gt;    /// [`Nip19Coordinate`] using the given sets.&lt;br/&gt;    pub fn flat_naddrs(self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;        self.into_iter()&lt;br/&gt;            .map(|n| n.get_naddrs(sets))&lt;br/&gt;            .try_fold(Vec::new(), |mut acc, item| {&lt;br/&gt;                acc.extend(item?);&lt;br/&gt;                Ok(acc)&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(RelayOrSetVecExt)]&lt;br/&gt;impl Vec&amp;lt;RelayOrSet&amp;gt; {&lt;br/&gt;    /// Converts this vector of [`RelayOrSet`] into a flat vector of&lt;br/&gt;    /// [`RelayUrl`] using the given sets.&lt;br/&gt;    pub fn flat_relays(self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;RelayUrl&amp;gt;&amp;gt; {&lt;br/&gt;        self.into_iter()&lt;br/&gt;            .map(|n| n.get_relays(sets))&lt;br/&gt;            .try_fold(Vec::new(), |mut acc, item| {&lt;br/&gt;                acc.extend(item?);&lt;br/&gt;                Ok(acc)&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(OptionNaddrOrSetVecExt)]&lt;br/&gt;impl Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt; {&lt;br/&gt;    /// Converts this vector of [`NaddrOrSet`] into a flat vector of&lt;br/&gt;    /// [`Nip19Coordinate`] using the given sets.&lt;br/&gt;    pub fn flat_naddrs(&amp;amp;self, sets: &amp;amp;[RepoRelaySet]) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt;&amp;gt; {&lt;br/&gt;        // Clones self here to simplify command code&lt;br/&gt;        self.clone()&lt;br/&gt;            .map(|naddrs| naddrs.flat_naddrs(sets))&lt;br/&gt;            .transpose()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(MutRepoRelaySetsExt)]&lt;br/&gt;impl Vec&amp;lt;RepoRelaySet&amp;gt; {&lt;br/&gt;    /// Removes duplicate repository addresses from each set.&lt;br/&gt;    ///&lt;br/&gt;    /// Relays are automatically deduplicated by the HashSet, but&lt;br/&gt;    /// repository addresses may appear duplicated if relays are sorted&lt;br/&gt;    /// differently or when relay counts vary. This compares addresses by&lt;br/&gt;    /// their coordinates, ignoring any embedded relay details.&lt;br/&gt;    pub fn dedup_naddrs(&amp;amp;mut self) {&lt;br/&gt;        self.iter_mut().for_each(RepoRelaySet::dedup_naddrs);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finds and returns a mutable reference a set with the given name. Returns&lt;br/&gt;    /// an error if no set with this name exists.&lt;br/&gt;    pub fn get_mut_set(&amp;amp;mut self, name: impl AsRef&amp;lt;str&amp;gt;) -&amp;gt; N34Result&amp;lt;&amp;amp;mut RepoRelaySet&amp;gt; {&lt;br/&gt;        let name = name.as_ref();&lt;br/&gt;        let set = self&lt;br/&gt;            .iter_mut()&lt;br/&gt;            .find(|set| set.name == name)&lt;br/&gt;            .ok_or_else(|| N34Error::from(ConfigError::SetNotFound(name.to_owned())))?;&lt;br/&gt;&lt;br/&gt;        tracing::trace!(&lt;br/&gt;            name = %name, set = ?set,&lt;br/&gt;            &amp;#34;Successfully located a set with the giving name&amp;#34;&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        Ok(set)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Creates and pushes a new set with the given name.&lt;br/&gt;    ///&lt;br/&gt;    /// Returns an error if a set with the same name already exists.&lt;br/&gt;    pub fn push_set(&lt;br/&gt;        &amp;amp;mut self,&lt;br/&gt;        name: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;        repos: impl IntoIterator&amp;lt;Item = Nip19Coordinate&amp;gt;,&lt;br/&gt;        relays: impl IntoIterator&amp;lt;Item = RelayUrl&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let set_name: String = name.into();&lt;br/&gt;        tracing::trace!(sets = ?self, &amp;#34;Pushing set &amp;#39;{set_name}&amp;#39; to sets collection&amp;#34;);&lt;br/&gt;&lt;br/&gt;        if self.as_slice().exists(&amp;amp;set_name) {&lt;br/&gt;            return Err(ConfigError::SetDuplicateName(set_name).into());&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        self.push(RepoRelaySet::new(set_name, repos, relays));&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Removes the set with the given name if it exists. Returns an error if&lt;br/&gt;    /// the set is not found.&lt;br/&gt;    pub fn remove_set(&amp;amp;mut self, name: impl Into&amp;lt;String&amp;gt;) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let set_name: String = name.into();&lt;br/&gt;        tracing::trace!(set_name, sets = ?self, &amp;#34;Removing set &amp;#39;{set_name}&amp;#39; from sets collection&amp;#34;);&lt;br/&gt;&lt;br/&gt;        if !self.as_slice().exists(&amp;amp;set_name) {&lt;br/&gt;            return Err(ConfigError::SetNotFound(set_name).into());&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        self.retain(|s| s.name != set_name);&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Removes the given relays from the specified set.&lt;br/&gt;    pub fn remove_relays(&lt;br/&gt;        &amp;amp;mut self,&lt;br/&gt;        name: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;        relays: impl Iterator&amp;lt;Item = RelayUrl&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let relays = Vec::from_iter(relays);&lt;br/&gt;        let set = self.get_mut_set(name.into())?;&lt;br/&gt;&lt;br/&gt;        set.relays.retain(|r| !relays.contains(r));&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Removes the given naddrs from the specified set.&lt;br/&gt;    pub fn remove_naddrs(&lt;br/&gt;        &amp;amp;mut self,&lt;br/&gt;        name: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;        naddrs: impl Iterator&amp;lt;Item = Nip19Coordinate&amp;gt;,&lt;br/&gt;    ) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let coordinates = Vec::from_iter(naddrs.map(|n| n.coordinate));&lt;br/&gt;        let set = self.get_mut_set(name.into())?;&lt;br/&gt;&lt;br/&gt;        set.naddrs.retain(|n| !coordinates.contains(&amp;amp;n.coordinate));&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[easy_ext::ext(RepoRelaySetsExt)]&lt;br/&gt;impl &amp;amp;[RepoRelaySet] {&lt;br/&gt;    /// Checks for duplicate set names. Returns an error if any duplicates are&lt;br/&gt;    /// found.&lt;br/&gt;    pub fn ensure_names(&amp;amp;self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let mut names = Vec::with_capacity(self.len());&lt;br/&gt;        names.extend(self.iter().map(|s| s.name.to_owned()));&lt;br/&gt;&lt;br/&gt;        names.sort_unstable();&lt;br/&gt;&lt;br/&gt;        if let Some(duplicate) = duplicate_in_sorted(&amp;amp;names) {&lt;br/&gt;            return Err(ConfigError::SetDuplicateName(duplicate.clone()).into());&lt;br/&gt;        }&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if a set with the given name exists.&lt;br/&gt;    pub fn exists(&amp;amp;self, set_name: &amp;amp;str) -&amp;gt; bool {&lt;br/&gt;        self.iter().any(|set| set.name == set_name)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finds and returns a reference a set with the given name. Returns an&lt;br/&gt;    /// error if no set with this name exists.&lt;br/&gt;    pub fn get_set(&amp;amp;self, name: impl AsRef&amp;lt;str&amp;gt;) -&amp;gt; N34Result&amp;lt;&amp;amp;RepoRelaySet&amp;gt; {&lt;br/&gt;        let name = name.as_ref();&lt;br/&gt;        let set = self&lt;br/&gt;            .iter()&lt;br/&gt;            .find(|set| set.name == name)&lt;br/&gt;            .ok_or_else(|| N34Error::from(ConfigError::SetNotFound(name.to_owned())))?;&lt;br/&gt;        tracing::trace!(&lt;br/&gt;            name = %name, set = ?set,&lt;br/&gt;            &amp;#34;Successfully located a set with the giving name&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        Ok(set)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Helper function that checks for duplicates in a sorted slice&lt;br/&gt;fn duplicate_in_sorted&amp;lt;T: PartialEq &#43; Clone&amp;gt;(items: &amp;amp;[T]) -&amp;gt; Option&amp;lt;&amp;amp;T&amp;gt; {&lt;br/&gt;    items.windows(2).find(|w| w[0] == w[1]).map(|w| &amp;amp;w[0])&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:47:45Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsyym2fhl00fyfmg6chyrt6r7ghdgzf5ra3zcymz6jk3660g4xna8czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhqwgv6</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsyym2fhl00fyfmg6chyrt6r7ghdgzf5ra3zcymz6jk3660g4xna8czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhqwgv6" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::fs;&lt;br/&gt;&lt;br/&gt;use clap::{ArgGroup, Args};&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventBuilder, Kind},&lt;br/&gt;    filter::Filter,&lt;br/&gt;    nips::nip01::Coordinate,&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;    nostr_utils::{&lt;br/&gt;        NostrClient,&lt;br/&gt;        traits::{NaddrsUtils, ReposUtils},&lt;br/&gt;        utils,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// The max date &amp;#34;9999-01-01 at 00:00 UTC&amp;#34;&lt;br/&gt;const MAX_DATE: i64 = 253370764800;&lt;br/&gt;&lt;br/&gt;/// Arguments for the `reply` command&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;#[clap(&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;comment-content&amp;#34;)&lt;br/&gt;            .args([&amp;#34;comment&amp;#34;, &amp;#34;editor&amp;#34;])&lt;br/&gt;            .required(true)&lt;br/&gt;    ),&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;quote-reply-to&amp;#34;)&lt;br/&gt;            .args([&amp;#34;comment&amp;#34;, &amp;#34;quote_to&amp;#34;])&lt;br/&gt;    )&lt;br/&gt;)]&lt;br/&gt;pub struct ReplyArgs {&lt;br/&gt;    /// The issue, patch, or comment to reply to&lt;br/&gt;    #[arg(value_name = &amp;#34;nevent1-or-note1&amp;#34;)]&lt;br/&gt;    to:       NostrEvent,&lt;br/&gt;    /// Quote the replied-to event in the editor&lt;br/&gt;    #[arg(long)]&lt;br/&gt;    quote_to: bool,&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The comment (cannot be used with --editor)&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    comment:  Option&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Open editor to write comment (cannot be used with --content)&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    editor:   bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ReplyArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let nostr_address_path = utils::nostr_address_path()?;&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        let user_pubk = client.pubkey().await?;&lt;br/&gt;        let repo_naddrs = if let Some(naddrs) = self.naddrs.flat_naddrs(&amp;amp;options.config.sets)? {&lt;br/&gt;            client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;            Some(naddrs)&lt;br/&gt;        } else if fs::exists(&amp;amp;nostr_address_path).is_ok() {&lt;br/&gt;            let naddrs = utils::naddrs_or_file(None, &amp;amp;nostr_address_path)?;&lt;br/&gt;            client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;            Some(naddrs)&lt;br/&gt;        } else {&lt;br/&gt;            None&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        client.add_relays(&amp;amp;self.to.relays).await;&lt;br/&gt;        let relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;        let author_read_relays =&lt;br/&gt;            utils::add_read_relays(client.user_relays_list(user_pubk).await?.as_ref());&lt;br/&gt;        client.add_relays(&amp;amp;author_read_relays).await;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;        let reply_to = client&lt;br/&gt;            .fetch_event(Filter::new().id(self.to.event_id))&lt;br/&gt;            .await?&lt;br/&gt;            .ok_or(N34Error::EventNotFound)?;&lt;br/&gt;        let root = client.find_root(reply_to.clone()).await?;&lt;br/&gt;&lt;br/&gt;        let repos_coordinate = if let Some(naddrs) = repo_naddrs {&lt;br/&gt;            naddrs.into_coordinates()&lt;br/&gt;        } else if let Some(ref root_event) = root {&lt;br/&gt;            coordinates_from_root(root_event)?&lt;br/&gt;        } else {&lt;br/&gt;            return Err(N34Error::NotFoundRepo);&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let repos = client.fetch_repos(&amp;amp;repos_coordinate).await?;&lt;br/&gt;        let maintainers = repos.extract_maintainers();&lt;br/&gt;&lt;br/&gt;        let quoted_content = if self.quote_to {&lt;br/&gt;            Some(quote_reply_to_content(&amp;amp;client, &amp;amp;reply_to).await)&lt;br/&gt;        } else {&lt;br/&gt;            None&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let content = utils::get_content(self.comment.as_ref(), quoted_content.as_ref(), &amp;#34;.txt&amp;#34;)?;&lt;br/&gt;        let content_details = client.parse_content(&amp;amp;content).await;&lt;br/&gt;&lt;br/&gt;        let event = EventBuilder::comment(&lt;br/&gt;            content,&lt;br/&gt;            &amp;amp;reply_to,&lt;br/&gt;            root.as_ref(),&lt;br/&gt;            repos.first().and_then(|r| r.relays.first()).cloned(),&lt;br/&gt;        )&lt;br/&gt;        .dedup_tags()&lt;br/&gt;        .pow(options.pow.unwrap_or_default())&lt;br/&gt;        .tags(content_details.clone().into_tags())&lt;br/&gt;        .build(user_pubk);&lt;br/&gt;&lt;br/&gt;        let event_id = event.id.expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;        let write_relays = [&lt;br/&gt;            relays,&lt;br/&gt;            utils::add_write_relays(relays_list.as_ref()),&lt;br/&gt;            // Merge repository announcement relays into write relays&lt;br/&gt;            repos.extract_relays(),&lt;br/&gt;            // Include read relays for each repository maintainer (if found)&lt;br/&gt;            client.read_relays_from_users(&amp;amp;maintainers).await,&lt;br/&gt;            // read relays of the root event and the reply to event&lt;br/&gt;            {&lt;br/&gt;                let (r1, r2) = future::join(&lt;br/&gt;                    client.read_relays_from_user(reply_to.pubkey),&lt;br/&gt;                    event_author_read_relays(&amp;amp;client, root.as_ref()),&lt;br/&gt;                )&lt;br/&gt;                .await;&lt;br/&gt;                [r1, r2].concat()&lt;br/&gt;            },&lt;br/&gt;            content_details.write_relays.into_iter().collect(),&lt;br/&gt;        ]&lt;br/&gt;        .concat();&lt;br/&gt;&lt;br/&gt;        tracing::trace!(relays = ?write_relays, &amp;#34;Write relays list&amp;#34;);&lt;br/&gt;        let (success, ..) = futures::join!(&lt;br/&gt;            client.send_event_to(event, relays_list.as_ref(), &amp;amp;write_relays),&lt;br/&gt;            client.broadcast(&amp;amp;reply_to, &amp;amp;author_read_relays),&lt;br/&gt;            async {&lt;br/&gt;                if let Some(root_event) = root {&lt;br/&gt;                    let _ = client.broadcast(&amp;amp;root_event, &amp;amp;author_read_relays).await;&lt;br/&gt;                }&lt;br/&gt;            },&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;        let nevent = utils::new_nevent(event_id, &amp;amp;success?)?;&lt;br/&gt;        println!(&amp;#34;Comment created: {nevent}&amp;#34;);&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates a quoted reply string in the format &amp;#34;On yyyy-mm-dd at hh:mm UTC,&lt;br/&gt;/// {author} wrote:&amp;#34; followed by the event content. Uses display name if&lt;br/&gt;/// available, otherwise falls back to a shortened npub string. Dates are&lt;br/&gt;/// formatted in UTC.&lt;br/&gt;async fn quote_reply_to_content(client: &amp;amp;NostrClient, quoted_event: &amp;amp;Event) -&amp;gt; String {&lt;br/&gt;    let author_name = client.get_username(quoted_event.pubkey).await;&lt;br/&gt;&lt;br/&gt;    let fdate = chrono::DateTime::from_timestamp(&lt;br/&gt;        quoted_event&lt;br/&gt;            .created_at&lt;br/&gt;            .as_u64()&lt;br/&gt;            .try_into()&lt;br/&gt;            .unwrap_or(MAX_DATE),&lt;br/&gt;        0,&lt;br/&gt;    )&lt;br/&gt;    .map(|datetime| datetime.format(&amp;#34;On %F at %R UTC, &amp;#34;).to_string())&lt;br/&gt;    .unwrap_or_default();&lt;br/&gt;&lt;br/&gt;    format!(&lt;br/&gt;        &amp;#34;{fdate}{author_name} wrote:\n&amp;gt; {}&amp;#34;,&lt;br/&gt;        quoted_event.content.trim().replace(&amp;#34;\n&amp;#34;, &amp;#34;\n&amp;gt; &amp;#34;)&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Gets the repository coordinate from a root Nostr event&amp;#39;s tags.&lt;br/&gt;/// The event must contain a coordinate tag with GitRepoAnnouncement kind.&lt;br/&gt;fn coordinates_from_root(root: &amp;amp;Event) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;    let coordinates: Vec&amp;lt;Coordinate&amp;gt; = root&lt;br/&gt;        .tags&lt;br/&gt;        .coordinates()&lt;br/&gt;        .filter(|c| c.kind == Kind::GitRepoAnnouncement)&lt;br/&gt;        .cloned()&lt;br/&gt;        .collect();&lt;br/&gt;&lt;br/&gt;    if coordinates.is_empty() {&lt;br/&gt;        return Err(N34Error::InvalidEvent(&lt;br/&gt;            &amp;#34;The Git issue/patch does not specify a target repository&amp;#34;.to_owned(),&lt;br/&gt;        ));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(coordinates)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns the event author read relays if found, otherwise an empty vector&lt;br/&gt;async fn event_author_read_relays(client: &amp;amp;NostrClient, event: Option&amp;lt;&amp;amp;Event&amp;gt;) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;    if let Some(root_event) = event {&lt;br/&gt;        client.read_relays_from_user(root_event.pubkey).await&lt;br/&gt;    } else {&lt;br/&gt;        Vec::new()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:47:37Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsp58f6cpkp7v05yr4dt97pl7vsux04gm7alznhm83ws88cmjc9g0gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvga0emj</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsp58f6cpkp7v05yr4dt97pl7vsux04gm7alznhm83ws88cmjc9g0gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvga0emj" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    collections::HashSet,&lt;br/&gt;    fs,&lt;br/&gt;    path::{Path, PathBuf},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    Kind,&lt;br/&gt;    nips::{&lt;br/&gt;        nip19::{FromBech32, Nip19Coordinate, ToBech32},&lt;br/&gt;        nip46::NostrConnectURI,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;use serde::{Deserialize, Serialize, Serializer};&lt;br/&gt;&lt;br/&gt;use super::CliConfig;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::DEFAULT_FALLBACK_PATH,&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;pub fn parse_repo_naddr(repo_naddr: &amp;amp;str) -&amp;gt; Result&amp;lt;Nip19Coordinate, String&amp;gt; {&lt;br/&gt;    let naddr = Nip19Coordinate::from_bech32(repo_naddr).map_err(|err| err.to_string())?;&lt;br/&gt;    if naddr.relays.is_empty() {&lt;br/&gt;        tracing::warn!(&amp;#34;The repository naddr does not contain any relay hints&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    (naddr.kind == Kind::GitRepoAnnouncement)&lt;br/&gt;        .then_some(naddr)&lt;br/&gt;        .ok_or_else(|| &amp;#34;Invalid naddr: must be of kind 30617 (GitRepoAnnouncement)&amp;#34;.to_owned())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses a nostr-address file into a NIP-19 coordinates. Expects the file to&lt;br/&gt;/// contain a repository announcements.&lt;br/&gt;pub fn parse_nostr_address_file(file_path: &amp;amp;Path) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;    let addresses = fs::read_to_string(file_path)&lt;br/&gt;        .map_err(N34Error::CanNotReadNostrAddressFile)?&lt;br/&gt;        .split(&amp;#34;\n&amp;#34;)&lt;br/&gt;        .filter_map(|line| {&lt;br/&gt;            (!line.starts_with(&amp;#34;#&amp;#34;) &amp;amp;&amp;amp; !line.trim().is_empty())&lt;br/&gt;                .then_some(parse_repo_naddr(line).map_err(N34Error::InvalidNostrAddressFileContent))&lt;br/&gt;        })&lt;br/&gt;        .collect::&amp;lt;N34Result&amp;lt;Vec&amp;lt;Nip19Coordinate&amp;gt;&amp;gt;&amp;gt;()?;&lt;br/&gt;    if addresses.is_empty() {&lt;br/&gt;        return Err(N34Error::EmptyNostrAddressFile);&lt;br/&gt;    }&lt;br/&gt;    Ok(addresses)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Loads CLI configuration from given path. Uses default config path if input&lt;br/&gt;/// matches fallback path.&lt;br/&gt;pub fn parse_config_path(config_path: &amp;amp;str) -&amp;gt; N34Result&amp;lt;CliConfig&amp;gt; {&lt;br/&gt;    let mut path = PathBuf::from(config_path.trim());&lt;br/&gt;&lt;br/&gt;    if config_path == DEFAULT_FALLBACK_PATH {&lt;br/&gt;        path = super::defaults::config_path()?;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    CliConfig::load(path)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses a bunker URL and checks if it&amp;#39;s a valid Nostr Connect URI.&lt;br/&gt;/// Returns an error if the URL is not a valid bunker URL.&lt;br/&gt;pub fn parse_bunker_url(bunker_url: &amp;amp;str) -&amp;gt; N34Result&amp;lt;NostrConnectURI&amp;gt; {&lt;br/&gt;    match NostrConnectURI::parse(bunker_url) {&lt;br/&gt;        Ok(url) if url.is_bunker() =&amp;gt; Ok(url),&lt;br/&gt;        _ =&amp;gt; Err(N34Error::NotBunkerUrl),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Serializes a set of NIP-19 coordinates as a list of bech32 strings.&lt;br/&gt;pub fn ser_naddrs&amp;lt;S&amp;gt;(naddr: &amp;amp;HashSet&amp;lt;Nip19Coordinate&amp;gt;, serializer: S) -&amp;gt; Result&amp;lt;S::Ok, S::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    S: Serializer,&lt;br/&gt;{&lt;br/&gt;    let str_naddrs = naddr&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|n| n.to_bech32().map_err(|err| err.to_string()))&lt;br/&gt;        .collect::&amp;lt;Result&amp;lt;Vec&amp;lt;_&amp;gt;, _&amp;gt;&amp;gt;()&lt;br/&gt;        .map_err(serde::ser::Error::custom)?;&lt;br/&gt;&lt;br/&gt;    str_naddrs.serialize(serializer)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Deserializes a list of bech32 strings into a set of NIP-19 coordinates.&lt;br/&gt;pub fn de_naddrs&amp;lt;&amp;#39;de, D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;HashSet&amp;lt;Nip19Coordinate&amp;gt;, D::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    D: serde::Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;{&lt;br/&gt;    Vec::&amp;lt;String&amp;gt;::deserialize(deserializer)?&lt;br/&gt;        .into_iter()&lt;br/&gt;        .map(|naddr| Nip19Coordinate::from_bech32(&amp;amp;naddr))&lt;br/&gt;        .collect::&amp;lt;Result&amp;lt;HashSet&amp;lt;_&amp;gt;, _&amp;gt;&amp;gt;()&lt;br/&gt;        .map_err(serde::de::Error::custom)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn ser_bunker_url&amp;lt;S&amp;gt;(&lt;br/&gt;    bunker_url: &amp;amp;Option&amp;lt;NostrConnectURI&amp;gt;,&lt;br/&gt;    serializer: S,&lt;br/&gt;) -&amp;gt; Result&amp;lt;S::Ok, S::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    S: Serializer,&lt;br/&gt;{&lt;br/&gt;    bunker_url&lt;br/&gt;        .as_ref()&lt;br/&gt;        .map(|u| u.to_string())&lt;br/&gt;        .serialize(serializer)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn de_bunker_url&amp;lt;&amp;#39;de, D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;Option&amp;lt;NostrConnectURI&amp;gt;, D::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    D: serde::Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;{&lt;br/&gt;    Option::&amp;lt;String&amp;gt;::deserialize(deserializer)?&lt;br/&gt;        .map(|u| parse_bunker_url(&amp;amp;u))&lt;br/&gt;        .transpose()&lt;br/&gt;        .map_err(serde::de::Error::custom)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:47:34Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqszhnmua4tc58ua5la4sy6w4lqk7eusj5rc8mqduekyjxwge2e796gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0yahu3</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqszhnmua4tc58ua5la4sy6w4lqk7eusj5rc8mqduekyjxwge2e796gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0yahu3" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    net::{Ipv4Addr, SocketAddr, SocketAddrV4},&lt;br/&gt;    time::Duration,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use nostr_browser_signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};&lt;br/&gt;&lt;br/&gt;/// The default socket address used for the NIP-07 signer proxy, set to&lt;br/&gt;/// localhost on port 51034.&lt;br/&gt;pub const DEFAULT_NIP07_PROXY_ADDR: SocketAddr =&lt;br/&gt;    SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 51034));&lt;br/&gt;&lt;br/&gt;/// How long to wait for the proxy response (3 minutes).&lt;br/&gt;pub const BROWSER_SIGNER_PROXY_TIMEOUT: Duration = Duration::from_secs(60 * 3);&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Represents the state used for CLI options.&lt;br/&gt;pub struct OptionsState {&lt;br/&gt;    /// The browser signer proxy, will be used if `--nip07` is enabled&lt;br/&gt;    pub browser_signer_proxy: BrowserSignerProxy,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl Default for OptionsState {&lt;br/&gt;    fn default() -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            browser_signer_proxy: default_browser_signer_proxy(),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Build the default browser signer proxy&lt;br/&gt;#[inline]&lt;br/&gt;fn default_browser_signer_proxy() -&amp;gt; BrowserSignerProxy {&lt;br/&gt;    BrowserSignerProxy::new(&lt;br/&gt;        BrowserSignerProxyOptions::default()&lt;br/&gt;            .timeout(BROWSER_SIGNER_PROXY_TIMEOUT)&lt;br/&gt;            .ip_addr(DEFAULT_NIP07_PROXY_ADDR.ip())&lt;br/&gt;            .port(DEFAULT_NIP07_PROXY_ADDR.port()),&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:47:21Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsdzmag9wx8rn2mn9fq3mmf3ge83tvk3y7waqq5rhks08eumayhu0czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv979r4d</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsdzmag9wx8rn2mn9fq3mmf3ge83tvk3y7waqq5rhks08eumayhu0czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv979r4d" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use super::*;&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_normal() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: a to abc&lt;br/&gt;&lt;br/&gt;Abc patch&lt;br/&gt;---&lt;br/&gt; src/nostr_utils/mod.rs            |  1 &#43;&lt;br/&gt; 1files changed, 3 insertions(&#43;), 1 deletions(-)&lt;br/&gt;&lt;br/&gt;diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs&lt;br/&gt;index 4120f5a..e68783c 100644&lt;br/&gt;--- a/src/nostr_utils/mod.rs&lt;br/&gt;&#43;&#43;&#43; b/src/nostr_utils/mod.rs&lt;br/&gt;@@ -103,31 &#43;103,9 @@ impl CommandRunner for NewArgs {&lt;br/&gt;&lt;br/&gt;- a&lt;br/&gt;&#43; abc&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(patch.subject, &amp;#34;[PATCH] chore: a to abc&amp;#34;);&lt;br/&gt;    assert_eq!(patch.body, &amp;#34;Abc patch&amp;#34;);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_normal_with_patch_in_content() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: Subject in subject&lt;br/&gt;&lt;br/&gt;A good test patch&lt;br/&gt;---&lt;br/&gt; src/nostr_utils/mod.rs            |  1 &#43;&lt;br/&gt; 1files changed, 3 insertions(&#43;), 1 deletions(-)&lt;br/&gt;&lt;br/&gt;diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs&lt;br/&gt;index 4120f5a..e68783c 100644&lt;br/&gt;--- a/src/nostr_utils/mod.rs&lt;br/&gt;&#43;&#43;&#43; b/src/nostr_utils/mod.rs&lt;br/&gt;@@ -103,31 &#43;103,9 @@ impl CommandRunner for NewArgs {&lt;br/&gt;&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: What a subject&lt;br/&gt;&lt;br/&gt;hi&lt;br/&gt;---&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(patch.subject, &amp;#34;[PATCH] chore: Subject in subject&amp;#34;);&lt;br/&gt;    assert_eq!(patch.body, &amp;#34;A good test patch&amp;#34;);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_multiline_subject() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: Some long subject yes so long one Some long subject yes&lt;br/&gt; so long one&lt;br/&gt;&lt;br/&gt;Abc patch&lt;br/&gt;---&lt;br/&gt; src/nostr_utils/mod.rs            |  1 &#43;&lt;br/&gt; 1files changed, 3 insertions(&#43;), 1 deletions(-)&lt;br/&gt;&lt;br/&gt;diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs&lt;br/&gt;index 4120f5a..e68783c 100644&lt;br/&gt;--- a/src/nostr_utils/mod.rs&lt;br/&gt;&#43;&#43;&#43; b/src/nostr_utils/mod.rs&lt;br/&gt;@@ -103,31 &#43;103,9 @@ impl CommandRunner for NewArgs {&lt;br/&gt;&lt;br/&gt;- a&lt;br/&gt;&#43; abc&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.subject,&lt;br/&gt;        &amp;#34;[PATCH] chore: Some long subject yes so long one Some long subject yes so long one&amp;#34;&lt;br/&gt;    );&lt;br/&gt;    assert_eq!(patch.body, &amp;#34;Abc patch&amp;#34;);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_multiline_body() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: a to abc&lt;br/&gt;&lt;br/&gt;Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus&lt;br/&gt;aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.&lt;br/&gt;Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione&lt;br/&gt;atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci&lt;br/&gt;aspernatur.&lt;br/&gt;---&lt;br/&gt; src/nostr_utils/mod.rs            |  1 &#43;&lt;br/&gt; 1files changed, 3 insertions(&#43;), 1 deletions(-)&lt;br/&gt;&lt;br/&gt;diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs&lt;br/&gt;index 4120f5a..e68783c 100644&lt;br/&gt;--- a/src/nostr_utils/mod.rs&lt;br/&gt;&#43;&#43;&#43; b/src/nostr_utils/mod.rs&lt;br/&gt;@@ -103,31 &#43;103,9 @@ impl CommandRunner for NewArgs {&lt;br/&gt;&lt;br/&gt;- a&lt;br/&gt;&#43; abc&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(patch.subject, &amp;#34;[PATCH] chore: a to abc&amp;#34;);&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.body,&lt;br/&gt;        &amp;#34;Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus&lt;br/&gt;aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.&lt;br/&gt;Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione&lt;br/&gt;atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci&lt;br/&gt;aspernatur.&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_cover_letter() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 3 Jun 2025 08:41:12 &#43;0000&lt;br/&gt;Subject: [PATCH v2 0/7] feat: Some test just a test&lt;br/&gt;&lt;br/&gt;Cover body&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(patch.subject, &amp;#34;[PATCH v2 0/7] feat: Some test just a test&amp;#34;);&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.body,&lt;br/&gt;        &amp;#34;Cover body&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_multiline_cover_subject() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 3 Jun 2025 08:41:12 &#43;0000&lt;br/&gt;Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some&lt;br/&gt; test just a test&lt;br/&gt;&lt;br/&gt;Cover body&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.subject,&lt;br/&gt;        &amp;#34;[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a test&amp;#34;&lt;br/&gt;    );&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.body,&lt;br/&gt;        &amp;#34;Cover body&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_multiline_cover_body() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 3 Jun 2025 08:41:12 &#43;0000&lt;br/&gt;Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some&lt;br/&gt; test just a test&lt;br/&gt;&lt;br/&gt;Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus&lt;br/&gt;aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.&lt;br/&gt;Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione&lt;br/&gt;atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci&lt;br/&gt;aspernatur.&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.subject,&lt;br/&gt;        &amp;#34;[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a test&amp;#34;&lt;br/&gt;    );&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.body,&lt;br/&gt;        &amp;#34;Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus&lt;br/&gt;aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.&lt;br/&gt;Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione&lt;br/&gt;atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci&lt;br/&gt;aspernatur.&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn normal_patch_filename() {&lt;br/&gt;    let mut patch = GitPatch {&lt;br/&gt;        inner:   String::new(),&lt;br/&gt;        subject: String::new(),&lt;br/&gt;        body:    String::new(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v2 0/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v2-0000-cover-letter.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH 0/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;0000-cover-letter.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v2 1/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v2-0001-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v42 1/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v42-0001-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v42 23/30] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v42-0023-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH 1/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;0001-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH 32/50] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;0032-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v100 32/50] feat: some long subject some long subject some long \&lt;br/&gt;                     subject some long subject&amp;#34;&lt;br/&gt;        .to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v100-0032-feat-some-long-subject-some-long-subject-some-long-subject.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_filename_without_patch() {&lt;br/&gt;    let mut patch = GitPatch {&lt;br/&gt;        inner:   String::new(),&lt;br/&gt;        subject: &amp;#34;[RFC v5 1/2] Something&amp;#34;.to_owned(),&lt;br/&gt;        body:    String::new(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_err());&lt;br/&gt;    patch.subject = &amp;#34;Something&amp;#34;.to_owned();&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_err());&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_filename_without_number() {&lt;br/&gt;    let mut patch = GitPatch {&lt;br/&gt;        inner:   String::new(),&lt;br/&gt;        subject: &amp;#34;[PATCH v5 /2] Something&amp;#34;.to_owned(),&lt;br/&gt;        body:    String::new(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_err());&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v5 2/] Something&amp;#34;.to_owned();&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_err());&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_filename_without_version() {&lt;br/&gt;    let patch = GitPatch {&lt;br/&gt;        inner:   String::new(),&lt;br/&gt;        subject: &amp;#34;[PATCH 1/2] Something&amp;#34;.to_owned(),&lt;br/&gt;        body:    String::new(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_ok());&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:47:18Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs22p474qsdgyu86sjsk8avh6e5r78uge4mjxh2ttmmks2342cjyfszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvg79khw</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs22p474qsdgyu86sjsk8avh6e5r78uge4mjxh2ttmmks2342cjyfszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvg79khw" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// Commands module&lt;br/&gt;pub mod commands;&lt;br/&gt;/// Common commands used by multiply commands&lt;br/&gt;pub mod common_commands;&lt;br/&gt;/// The CLI config&lt;br/&gt;pub mod config;&lt;br/&gt;/// Default lazy values for CLI arguments&lt;br/&gt;pub mod defaults;&lt;br/&gt;/// Macros for CLI application.&lt;br/&gt;pub mod macros;&lt;br/&gt;/// Represents the state used for CLI options.&lt;br/&gt;pub mod options_state;&lt;br/&gt;/// CLI arguments parsers&lt;br/&gt;pub mod parsers;&lt;br/&gt;/// CLI traits&lt;br/&gt;pub mod traits;&lt;br/&gt;/// Common helper types used throughout the CLI.&lt;br/&gt;pub mod types;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;use clap::Parser;&lt;br/&gt;use clap_verbosity_flag::Verbosity;&lt;br/&gt;use nostr::key::Keys;&lt;br/&gt;use nostr::key::SecretKey;&lt;br/&gt;use nostr_browser_signer_proxy::BrowserSignerProxy;&lt;br/&gt;use nostr_browser_signer_proxy::BrowserSignerProxyOptions;&lt;br/&gt;use nostr_keyring::KeyringError;&lt;br/&gt;use nostr_keyring::NostrKeyring;&lt;br/&gt;use types::RelayOrSet;&lt;br/&gt;&lt;br/&gt;pub use self::commands::*;&lt;br/&gt;pub use self::config::*;&lt;br/&gt;use self::traits::CommandRunner;&lt;br/&gt;use crate::cli::options_state::BROWSER_SIGNER_PROXY_TIMEOUT;&lt;br/&gt;use crate::error::N34Error;&lt;br/&gt;use crate::error::N34Result;&lt;br/&gt;use crate::nostr_utils::traits::NostrKeyringErrorUtils;&lt;br/&gt;&lt;br/&gt;/// Header message, used in the help message&lt;br/&gt;const HEADER: &amp;amp;str = r#&amp;#34;Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;License GNU GPL-3.0-or-later &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;&lt;br/&gt;This is free software: you are free to change and redistribute it.&lt;br/&gt;There is NO WARRANTY, to the extent permitted by law.&lt;br/&gt;&lt;br/&gt;Git repository: &lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git&amp;#34;#&#34;&gt;https://git.4rs.nl/awiteb/n34.git&amp;#34;#&lt;/a&gt;;&lt;br/&gt;&lt;br/&gt;/// Footer message, used in the help message&lt;br/&gt;const FOOTER: &amp;amp;str = r#&amp;#34;Please report bugs to &amp;lt;naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqauesksc39&amp;gt;.&amp;#34;#;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Name of the file storing the repository address&lt;br/&gt;pub const NOSTR_ADDRESS_FILE: &amp;amp;str = &amp;#34;nostr-address&amp;#34;;&lt;br/&gt;&lt;br/&gt;#[derive(Parser, Debug)]&lt;br/&gt;#[command(about, version, before_long_help = HEADER, after_long_help = FOOTER)]&lt;br/&gt;/// A command-line interface for interacting with NIP-34 and other Nostr&lt;br/&gt;/// code-related stuff.&lt;br/&gt;pub struct Cli {&lt;br/&gt;    #[command(flatten)]&lt;br/&gt;    pub options:   commands::CliOptions,&lt;br/&gt;    /// Controls the verbosity level of output&lt;br/&gt;    #[command(flatten)]&lt;br/&gt;    pub verbosity: Verbosity,&lt;br/&gt;    /// The subcommand to execute&lt;br/&gt;    #[command(subcommand)]&lt;br/&gt;    pub command:   commands::Commands,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl Cli {&lt;br/&gt;    /// Keyring service name of n34&lt;br/&gt;    pub const N34_KEYRING_SERVICE_NAME: &amp;amp;str = &amp;#34;n34&amp;#34;;&lt;br/&gt;    /// Keyring entry name of the n34 keypair&lt;br/&gt;    pub const N34_KEY_PAIR_ENTRY: &amp;amp;str = &amp;#34;n34_keypair&amp;#34;;&lt;br/&gt;    /// Keyring entry name of the user secret key&lt;br/&gt;    pub const USER_KEY_PAIR_ENTRY: &amp;amp;str = &amp;#34;user_keypair&amp;#34;;&lt;br/&gt;&lt;br/&gt;    /// Executes the command&lt;br/&gt;    pub async fn run(self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        self.command.run(self.options).await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the n34 keypair from the keyring or generates and stores a new one&lt;br/&gt;    /// if none exists.&lt;br/&gt;    pub fn n34_keypair() -&amp;gt; N34Result&amp;lt;Keys&amp;gt; {&lt;br/&gt;        let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);&lt;br/&gt;&lt;br/&gt;        match keyring.get(Self::N34_KEY_PAIR_ENTRY) {&lt;br/&gt;            Ok(keys) =&amp;gt; Ok(keys),&lt;br/&gt;            Err(nostr_keyring::Error::Keyring(KeyringError::NoEntry)) =&amp;gt; {&lt;br/&gt;                let new_keys = Keys::generate();&lt;br/&gt;                keyring.set(Self::N34_KEY_PAIR_ENTRY, &amp;amp;new_keys)?;&lt;br/&gt;                Ok(new_keys)&lt;br/&gt;            }&lt;br/&gt;            Err(err) =&amp;gt; Err(N34Error::Keyring(err)),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Retrieves the user&amp;#39;s keypair from the keyring. If no key exists and one&lt;br/&gt;    /// is provided, stores and returns it. If no key exists and none is&lt;br/&gt;    /// provided, returns an error.&lt;br/&gt;    pub fn user_keypair(secret_key: Option&amp;lt;SecretKey&amp;gt;) -&amp;gt; N34Result&amp;lt;Keys&amp;gt; {&lt;br/&gt;        let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);&lt;br/&gt;        let keyring_key = keyring.get(Self::USER_KEY_PAIR_ENTRY);&lt;br/&gt;&lt;br/&gt;        if let Err(ref err) = keyring_key&lt;br/&gt;            &amp;amp;&amp;amp; err.is_keyring_no_entry()&lt;br/&gt;            &amp;amp;&amp;amp; let Some(secret_key) = secret_key&lt;br/&gt;        {&lt;br/&gt;            let keypair = Keys::new(secret_key);&lt;br/&gt;            keyring.set(Self::USER_KEY_PAIR_ENTRY, &amp;amp;keypair)?;&lt;br/&gt;            return Ok(keypair);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        keyring_key.map_err(|err| {&lt;br/&gt;            if err.is_keyring_no_entry() {&lt;br/&gt;                N34Error::SecretKeyKeyringWithoutEntry&lt;br/&gt;            } else {&lt;br/&gt;                N34Error::Keyring(err)&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Processes the CLI configuration by applying fallback values from config if&lt;br/&gt;/// needed. Returns the processed Cli configuration if successful.&lt;br/&gt;pub fn post_cli(mut cli: Cli) -&amp;gt; N34Result&amp;lt;Cli&amp;gt; {&lt;br/&gt;    cli.options.pow = cli.options.pow.or(cli.options.config.pow);&lt;br/&gt;&lt;br/&gt;    if cli.options.relays.is_empty()&lt;br/&gt;        &amp;amp;&amp;amp; let Some(relays) = &amp;amp;cli.options.config.fallback_relays&lt;br/&gt;    {&lt;br/&gt;        cli.options.relays = relays.iter().cloned().map(RelayOrSet::Relay).collect();&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Automatically sets the signer based on the configuration if no signer&lt;br/&gt;    // is provided.&lt;br/&gt;    if !cli.options.nip07&lt;br/&gt;        &amp;amp;&amp;amp; cli.options.bunker_url.is_none()&lt;br/&gt;        &amp;amp;&amp;amp; (cli.options.secret_key.is_none() || cli.options.config.keyring_secret_key)&lt;br/&gt;    {&lt;br/&gt;        if let Some(addr) = cli.options.config.nip07 {&lt;br/&gt;            cli.options.nip07 = true;&lt;br/&gt;            cli.options.state.browser_signer_proxy = BrowserSignerProxy::new(&lt;br/&gt;                BrowserSignerProxyOptions::default()&lt;br/&gt;                    .timeout(BROWSER_SIGNER_PROXY_TIMEOUT)&lt;br/&gt;                    .ip_addr(addr.ip())&lt;br/&gt;                    .port(addr.port()),&lt;br/&gt;            );&lt;br/&gt;        } else if let Some(bunker_url) = &amp;amp;cli.options.config.bunker_url {&lt;br/&gt;            cli.options.bunker_url = Some(bunker_url.clone());&lt;br/&gt;        } else if cli.options.config.keyring_secret_key {&lt;br/&gt;            cli.options.secret_key = Some(&lt;br/&gt;                Cli::user_keypair(cli.options.secret_key)?&lt;br/&gt;                    .secret_key()&lt;br/&gt;                    .clone(),&lt;br/&gt;            );&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(cli)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:47:10Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsyhvpfkdg838c92pj45jsla0eefrrd4t747h78nwezldqr2d0z3dgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvqtz9av</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsyhvpfkdg838c92pj45jsla0eefrrd4t747h78nwezldqr2d0z3dgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvqtz9av" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{fs, str::FromStr};&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{EventBuilder, EventId, Kind, Tag, TagKind, Tags, UnsignedEvent},&lt;br/&gt;    hashes::sha1::Hash as Sha1Hash,&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    nips::{nip01::Coordinate, nip10::Marker},&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use super::GitPatch;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        patch::{REVISION_ROOT_HASHTAG_CONTENT, ROOT_HASHTAG_CONTENT},&lt;br/&gt;        traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;    nostr_utils::{&lt;br/&gt;        NostrClient,&lt;br/&gt;        traits::{NaddrsUtils, ReposUtils},&lt;br/&gt;        utils,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Prefix used for git patch alt.&lt;br/&gt;const PATCH_ALT_PREFIX: &amp;amp;str = &amp;#34;git patch: &amp;#34;;&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct SendArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:         Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// List of patch files to send (space separated).&lt;br/&gt;    ///&lt;br/&gt;    /// For p-tagging users, include them in the cover letter with&lt;br/&gt;    /// `nostr:npub1...`.&lt;br/&gt;    #[arg(value_name = &amp;#34;PATCH-PATH&amp;#34;, required = true, value_parser = parse_patch_path)]&lt;br/&gt;    patches:        Vec&amp;lt;GitPatch&amp;gt;,&lt;br/&gt;    /// Original patch ID if this is a revision of it&lt;br/&gt;    #[arg(long, value_name = &amp;#34;EVENT-ID&amp;#34;)]&lt;br/&gt;    original_patch: Option&amp;lt;NostrEvent&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for SendArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(&lt;br/&gt;            self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;            &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;        )?)?;&lt;br/&gt;&lt;br/&gt;        let repo_coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        let user_pubk = client.pubkey().await?;&lt;br/&gt;&lt;br/&gt;        client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;        if let Some(original_patch) = &amp;amp;self.original_patch {&lt;br/&gt;            client.add_relays(&amp;amp;original_patch.relays).await;&lt;br/&gt;        }&lt;br/&gt;        let relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;        client&lt;br/&gt;            .add_relays(&amp;amp;utils::add_read_relays(relays_list.as_ref()))&lt;br/&gt;            .await;&lt;br/&gt;        let repos = client.fetch_repos(&amp;amp;repo_coordinates).await?;&lt;br/&gt;        let euc = repos.extract_euc();&lt;br/&gt;        let maintainers = repos.extract_maintainers();&lt;br/&gt;        client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;        let (events, events_write_relays) = make_patch_series(&lt;br/&gt;            &amp;amp;client,&lt;br/&gt;            self.patches,&lt;br/&gt;            self.original_patch.as_ref().map(|e| e.event_id),&lt;br/&gt;            repos.extract_relays().first().cloned(),&lt;br/&gt;            repo_coordinates,&lt;br/&gt;            euc,&lt;br/&gt;            user_pubk,&lt;br/&gt;        )&lt;br/&gt;        .await?;&lt;br/&gt;&lt;br/&gt;        let write_relays = [&lt;br/&gt;            relays,&lt;br/&gt;            repos.extract_relays(),&lt;br/&gt;            events_write_relays,&lt;br/&gt;            naddrs.extract_relays(),&lt;br/&gt;            self.original_patch.map(|e| e.relays).unwrap_or_default(),&lt;br/&gt;            utils::add_write_relays(relays_list.as_ref()),&lt;br/&gt;            client.read_relays_from_users(&amp;amp;maintainers).await,&lt;br/&gt;        ]&lt;br/&gt;        .concat();&lt;br/&gt;&lt;br/&gt;        tracing::trace!(write_relays = ?write_relays, &amp;#34;Write relays of the patches&amp;#34;);&lt;br/&gt;        let nevents = future::join_all(events.into_iter().map(|mut event| {&lt;br/&gt;            async {&lt;br/&gt;                let event_id = event.id();&lt;br/&gt;                let subject = event&lt;br/&gt;                    .tags&lt;br/&gt;                    .find(TagKind::Alt)&lt;br/&gt;                    .and_then(Tag::content)&lt;br/&gt;                    .expect(&amp;#34;There is an alt&amp;#34;)&lt;br/&gt;                    .replace(PATCH_ALT_PREFIX, &amp;#34;&amp;#34;);&lt;br/&gt;                client&lt;br/&gt;                    .send_event_to(event, relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;                    .await&lt;br/&gt;                    .map(|r| Ok((subject, utils::new_nevent(event_id, &amp;amp;r)?)))?&lt;br/&gt;            }&lt;br/&gt;        }))&lt;br/&gt;        .await&lt;br/&gt;        .into_iter()&lt;br/&gt;        .collect::&amp;lt;N34Result&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;&amp;gt;()?;&lt;br/&gt;&lt;br/&gt;        for (subject, nevent) in nevents {&lt;br/&gt;            println!(&amp;#34;Created &amp;#39;{subject}&amp;#39;: {nevent}&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn parse_patch_path(patch_path: &amp;amp;str) -&amp;gt; Result&amp;lt;GitPatch, String&amp;gt; {&lt;br/&gt;    tracing::debug!(&amp;#34;Parsing patch file `{patch_path}`&amp;#34;);&lt;br/&gt;    let patch_content = fs::read_to_string(patch_path)&lt;br/&gt;        .map_err(|err| format!(&amp;#34;Failed to read patch file `{patch_path}`: {err}&amp;#34;))?;&lt;br/&gt;    GitPatch::from_str(&amp;amp;patch_content)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn make_patch_series(&lt;br/&gt;    client: &amp;amp;NostrClient,&lt;br/&gt;    patches: Vec&amp;lt;GitPatch&amp;gt;,&lt;br/&gt;    original_patch: Option&amp;lt;EventId&amp;gt;,&lt;br/&gt;    relay_hint: Option&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;    repo_coordinates: Vec&amp;lt;Coordinate&amp;gt;,&lt;br/&gt;    euc: Option&amp;lt;&amp;amp;Sha1Hash&amp;gt;,&lt;br/&gt;    author_pkey: PublicKey,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;(Vec&amp;lt;UnsignedEvent&amp;gt;, Vec&amp;lt;RelayUrl&amp;gt;)&amp;gt; {&lt;br/&gt;    let mut write_relays = Vec::new();&lt;br/&gt;    let mut patch_series = Vec::new();&lt;br/&gt;    let mut patches = patches.into_iter();&lt;br/&gt;    let root_patch = patches.next().expect(&amp;#34;Patches can&amp;#39;t be empty&amp;#34;);&lt;br/&gt;    let (root_event, root_relays) = make_patch(&lt;br/&gt;        client,&lt;br/&gt;        root_patch,&lt;br/&gt;        None,&lt;br/&gt;        original_patch,&lt;br/&gt;        relay_hint.as_ref(),&lt;br/&gt;        &amp;amp;repo_coordinates,&lt;br/&gt;        euc,&lt;br/&gt;        author_pkey,&lt;br/&gt;    )&lt;br/&gt;    .await;&lt;br/&gt;    write_relays.extend(root_relays);&lt;br/&gt;    let root_id = *root_event.id.as_ref().expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;    let mut previous_patch = root_id;&lt;br/&gt;    patch_series.push(root_event);&lt;br/&gt;&lt;br/&gt;    for patch in patches {&lt;br/&gt;        let (patch_event, patch_relays) = make_patch(&lt;br/&gt;            client,&lt;br/&gt;            patch,&lt;br/&gt;            Some(root_id),&lt;br/&gt;            Some(previous_patch),&lt;br/&gt;            relay_hint.as_ref(),&lt;br/&gt;            &amp;amp;repo_coordinates,&lt;br/&gt;            euc,&lt;br/&gt;            author_pkey,&lt;br/&gt;        )&lt;br/&gt;        .await;&lt;br/&gt;        previous_patch = patch_event.id.expect(&amp;#34;there is an id&amp;#34;);&lt;br/&gt;        write_relays.extend(patch_relays);&lt;br/&gt;        patch_series.push(patch_event);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok((patch_series, write_relays))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[allow(clippy::too_many_arguments)]&lt;br/&gt;async fn make_patch(&lt;br/&gt;    client: &amp;amp;NostrClient,&lt;br/&gt;    patch: GitPatch,&lt;br/&gt;    root: Option&amp;lt;EventId&amp;gt;,&lt;br/&gt;    reply_to: Option&amp;lt;EventId&amp;gt;,&lt;br/&gt;    write_relay: Option&amp;lt;&amp;amp;RelayUrl&amp;gt;,&lt;br/&gt;    repo_coordinates: &amp;amp;[Coordinate],&lt;br/&gt;    euc: Option&amp;lt;&amp;amp;Sha1Hash&amp;gt;,&lt;br/&gt;    author_pkey: PublicKey,&lt;br/&gt;) -&amp;gt; (UnsignedEvent, Vec&amp;lt;RelayUrl&amp;gt;) {&lt;br/&gt;    let content_details = client.parse_content(&amp;amp;patch.body).await;&lt;br/&gt;    let content_relays = content_details.write_relays.clone();&lt;br/&gt;    // NIP-34 compliance requires referencing the previous patch using `NIP-10 e&lt;br/&gt;    // reply`. However, this fails for the second patch when&lt;br/&gt;    // `EventBuilder::dedup_tags` is enabled because:&lt;br/&gt;    // 1. The tag is treated as a duplicate based on its content (the root ID).&lt;br/&gt;    // 2. The second patch would reply to the root twice:&lt;br/&gt;    //    - First with the &amp;#39;root&amp;#39; marker&lt;br/&gt;    //    - Then with the &amp;#39;reply&amp;#39; marker&lt;br/&gt;    // The `EventBuilder::dedup_tags` function then removes the &amp;#39;reply&amp;#39; marker as a&lt;br/&gt;    // duplicate.&lt;br/&gt;    let mut safe_dedup_tags = Tags::new();&lt;br/&gt;    safe_dedup_tags.push(Tag::alt(format!(&amp;#34;{PATCH_ALT_PREFIX}{}&amp;#34;, patch.subject)));&lt;br/&gt;    safe_dedup_tags.push(Tag::description(patch.subject));&lt;br/&gt;    safe_dedup_tags.extend(content_details.into_tags());&lt;br/&gt;    safe_dedup_tags.extend(&lt;br/&gt;        repo_coordinates&lt;br/&gt;            .iter()&lt;br/&gt;            .map(|c| Tag::coordinate(c.clone(), None)),&lt;br/&gt;    );&lt;br/&gt;    safe_dedup_tags.extend(&lt;br/&gt;        repo_coordinates&lt;br/&gt;            .iter()&lt;br/&gt;            .map(|c| Tag::public_key(c.public_key)),&lt;br/&gt;    );&lt;br/&gt;    if let Some(euc) = euc {&lt;br/&gt;        safe_dedup_tags.push(Tag::reference(euc.to_string()));&lt;br/&gt;    }&lt;br/&gt;    safe_dedup_tags.dedup();&lt;br/&gt;    let mut event_builder = EventBuilder::new(Kind::GitPatch, patch.inner).tags(safe_dedup_tags);&lt;br/&gt;&lt;br/&gt;    // If the root is None, this indicates we&amp;#39;re handling the root event&lt;br/&gt;    if let Some(root_id) = root {&lt;br/&gt;        event_builder =&lt;br/&gt;            event_builder.tag(utils::event_reply_tag(&amp;amp;root_id, write_relay, Marker::Root));&lt;br/&gt;    } else {&lt;br/&gt;        event_builder = event_builder.tag(Tag::hashtag(ROOT_HASHTAG_CONTENT));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Handles the case where there is a patch to reply to but no root. This&lt;br/&gt;    // indicates we are processing a revision, as the root revision should reply&lt;br/&gt;    // directly to the original patch.&lt;br/&gt;    if let Some(reply_to_id) = reply_to {&lt;br/&gt;        if root.is_none() {&lt;br/&gt;            event_builder = event_builder.tags([&lt;br/&gt;                utils::event_reply_tag(&amp;amp;reply_to_id, write_relay, Marker::Reply),&lt;br/&gt;                Tag::hashtag(REVISION_ROOT_HASHTAG_CONTENT),&lt;br/&gt;            ]);&lt;br/&gt;        } else {&lt;br/&gt;            event_builder = event_builder.tag(utils::event_reply_tag(&lt;br/&gt;                &amp;amp;reply_to_id,&lt;br/&gt;                write_relay,&lt;br/&gt;                Marker::Reply,&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;    (&lt;br/&gt;        event_builder.build(author_pkey),&lt;br/&gt;        content_relays.into_iter().collect(),&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:47:04Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqstk3ngvupz5hyk3xdgnh9t8mpl3056fj0q78f8uch66a5mwdhlkpqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7hkysa</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqstk3ngvupz5hyk3xdgnh9t8mpl3056fj0q78f8uch66a5mwdhlkpqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7hkysa" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use super::traits::CommandRunner;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Returns whether the command runner type `T` requires relays.&lt;br/&gt;pub fn get_relays_state&amp;lt;T: CommandRunner&amp;gt;(_v: &amp;amp;T) -&amp;gt; bool {&lt;br/&gt;    T::NEED_RELAYS&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns whether the command runner type `T` requires a signer.&lt;br/&gt;pub fn get_signer_state&amp;lt;T: CommandRunner&amp;gt;(_v: &amp;amp;T) -&amp;gt; bool {&lt;br/&gt;    T::NEED_SIGNER&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Executes a command with required setup checks. The first parameter is the&lt;br/&gt;/// command to match on (often `self`), followed by options. Optional&lt;br/&gt;/// subcommands come next, and commands with arguments (after `&amp;amp;`) are listed&lt;br/&gt;/// last.&lt;br/&gt;#[macro_export]&lt;br/&gt;macro_rules! run_command {&lt;br/&gt;    ($command:ident, $options:ident, $($subcommands:ident)* &amp;amp; $($commands:ident)*) =&amp;gt; {&lt;br/&gt;        match $command {&lt;br/&gt;            $(&lt;br/&gt;                Self::$subcommands { subcommands } =&amp;gt; subcommands.run($options).await,&lt;br/&gt;            )*&lt;br/&gt;            $(&lt;br/&gt;                Self::$commands ( args ) =&amp;gt; {&lt;br/&gt;                    if $crate::cli::macros::get_relays_state(&amp;amp;args) {&lt;br/&gt;                        $options.ensure_relays()?;&lt;br/&gt;                    }&lt;br/&gt;                    if $crate::cli::macros::get_signer_state(&amp;amp;args) {&lt;br/&gt;                        $options.ensure_signer()?;&lt;br/&gt;                    }&lt;br/&gt;                    args.run($options).await&lt;br/&gt;                },&lt;br/&gt;            )*&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:58Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs8dent30yd7wt9t6xfjk7pyx9kf8uxayfc2qc3uafaygz0edsdwagzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7hnn74</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs8dent30yd7wt9t6xfjk7pyx9kf8uxayfc2qc3uafaygz0edsdwagzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv7hnn74" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use super::PatchStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::CommandRunner,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ReopenArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The closed/drafted patch id to reopen it. Must be orignal root patch&lt;br/&gt;    patch_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ReopenArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::patch_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.patch_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            PatchStatus::Open,&lt;br/&gt;            None,&lt;br/&gt;            Vec::new(),&lt;br/&gt;            |patch_status| {&lt;br/&gt;                if patch_status.is_open() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t open an already open patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_merged_or_applied() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t open a merged/applied patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:52Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsds0n474lzq6dnumwcmvw2x2wdy2zmd9tmtyvr3afxxw4kwssj45gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrrkhnz</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsds0n474lzq6dnumwcmvw2x2wdy2zmd9tmtyvr3afxxw4kwssj45gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrrkhnz" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::path::PathBuf;&lt;br/&gt;&lt;br/&gt;use crate::{cli::ConfigError, error::N34Result};&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Default config path&lt;br/&gt;pub fn config_path() -&amp;gt; N34Result&amp;lt;PathBuf&amp;gt; {&lt;br/&gt;    Ok(dirs::config_dir()&lt;br/&gt;        .ok_or(ConfigError::CanNotFindConfigPath)?&lt;br/&gt;        .join(&amp;#34;n34&amp;#34;)&lt;br/&gt;        .join(&amp;#34;config.toml&amp;#34;))&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:47Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsfwsraq27jkvnq4fdgkhgap3kz34ta3ymgsfkpv2s7h2vdcnqn56szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv929hnw</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsfwsraq27jkvnq4fdgkhgap3kz34ta3ymgsfkpv2s7h2vdcnqn56szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv929hnw" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `patch apply` suubcommand&lt;br/&gt;mod apply;&lt;br/&gt;/// `patch close` subcommand&lt;br/&gt;mod close;&lt;br/&gt;/// `patch draft` subcommand&lt;br/&gt;mod draft;&lt;br/&gt;/// `patch fetch` subcommand&lt;br/&gt;mod fetch;&lt;br/&gt;/// `patch list` subcommand&lt;br/&gt;mod list;&lt;br/&gt;/// `patch merge` subcommand&lt;br/&gt;mod merge;&lt;br/&gt;/// `patch reopen` subcommand&lt;br/&gt;mod reopen;&lt;br/&gt;/// `patch send` subcommand&lt;br/&gt;mod send;&lt;br/&gt;#[cfg(test)]&lt;br/&gt;mod tests;&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    fmt,&lt;br/&gt;    path::{Path, PathBuf},&lt;br/&gt;    str::FromStr,&lt;br/&gt;    sync::LazyLock,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;use nostr::event::Kind;&lt;br/&gt;use regex::Regex;&lt;br/&gt;&lt;br/&gt;use self::apply::ApplyArgs;&lt;br/&gt;use self::close::CloseArgs;&lt;br/&gt;use self::draft::DraftArgs;&lt;br/&gt;use self::fetch::FetchArgs;&lt;br/&gt;use self::list::ListArgs;&lt;br/&gt;use self::merge::MergeArgs;&lt;br/&gt;use self::reopen::ReopenArgs;&lt;br/&gt;use self::send::SendArgs;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::error::{N34Error, N34Result};&lt;br/&gt;&lt;br/&gt;/// Regular expression for extracting the patch subject.&lt;br/&gt;static SUBJECT_RE: LazyLock&amp;lt;Regex&amp;gt; =&lt;br/&gt;    LazyLock::new(|| Regex::new(r&amp;#34;(?m)^Subject: (.*(?:\n .*)*)&amp;#34;).unwrap());&lt;br/&gt;&lt;br/&gt;/// Regular expression for extracting the patch body.&lt;br/&gt;static BODY_RE: LazyLock&amp;lt;Regex&amp;gt; =&lt;br/&gt;    LazyLock::new(|| Regex::new(r&amp;#34;\n\n((?:.|\n)*?)(?:\n--[ -]|\z)&amp;#34;).unwrap());&lt;br/&gt;&lt;br/&gt;/// Regular expiration for extracting the patch version and number&lt;br/&gt;static PATCH_VERSION_NUMBER_RE: LazyLock&amp;lt;Regex&amp;gt; = LazyLock::new(|| {&lt;br/&gt;    Regex::new(r&amp;#34;\[PATCH\s&#43;(?:v(?&amp;lt;version&amp;gt;\d&#43;)\s*)?(?&amp;lt;number&amp;gt;\d&#43;)/(?:\d&#43;)&amp;#34;).unwrap()&lt;br/&gt;});&lt;br/&gt;&lt;br/&gt;/// Content of the hashtag representing the root patch.&lt;br/&gt;pub const ROOT_HASHTAG_CONTENT: &amp;amp;str = &amp;#34;root&amp;#34;;&lt;br/&gt;/// Content of the hashtag representing the root revision patch.&lt;br/&gt;pub const REVISION_ROOT_HASHTAG_CONTENT: &amp;amp;str = &amp;#34;root-revision&amp;#34;;&lt;br/&gt;/// The content of the hashtag used by `ngit-cli` to represent a root revision&lt;br/&gt;/// patch before the commit 6ae42e67d9da36f6c2e1356acba30a3a62112bc7. This was a&lt;br/&gt;/// typo.&lt;br/&gt;pub const LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT: &amp;amp;str = &amp;#34;revision-root&amp;#34;;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum PatchSubcommands {&lt;br/&gt;    /// Send one or more patches to a repository.&lt;br/&gt;    Send(SendArgs),&lt;br/&gt;    /// Fetches a patch by its id.&lt;br/&gt;    Fetch(FetchArgs),&lt;br/&gt;    /// Closes an open or drafted patch.&lt;br/&gt;    Close(CloseArgs),&lt;br/&gt;    /// Converts an open patch to draft state.&lt;br/&gt;    Draft(DraftArgs),&lt;br/&gt;    /// Reopens a closed or drafted patch.&lt;br/&gt;    Reopen(ReopenArgs),&lt;br/&gt;    /// Set an open patch status to applied.&lt;br/&gt;    Apply(ApplyArgs),&lt;br/&gt;    /// Set an open patch status to merged.&lt;br/&gt;    Merge(MergeArgs),&lt;br/&gt;    /// List the repositories patches.&lt;br/&gt;    List(ListArgs),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Represents a git patch&lt;br/&gt;#[derive(Clone, Debug)]&lt;br/&gt;pub struct GitPatch {&lt;br/&gt;    /// Full content of the patch file&lt;br/&gt;    pub inner:   String,&lt;br/&gt;    /// Short description of the patch changes&lt;br/&gt;    pub subject: String,&lt;br/&gt;    /// Detailed explanation of the patch changes&lt;br/&gt;    pub body:    String,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub enum PatchStatus {&lt;br/&gt;    /// The patch is currently open&lt;br/&gt;    Open,&lt;br/&gt;    /// The patch has been merged/applied&lt;br/&gt;    MergedApplied,&lt;br/&gt;    /// The patch has been closed&lt;br/&gt;    Closed,&lt;br/&gt;    /// A patch that has been drafted but not yet applied.&lt;br/&gt;    Draft,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl PatchStatus {&lt;br/&gt;    /// Maps the issue status to its corresponding Nostr kind.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn kind(&amp;amp;self) -&amp;gt; Kind {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Open =&amp;gt; Kind::GitStatusOpen,&lt;br/&gt;            Self::MergedApplied =&amp;gt; Kind::GitStatusApplied,&lt;br/&gt;            Self::Closed =&amp;gt; Kind::GitStatusClosed,&lt;br/&gt;            Self::Draft =&amp;gt; Kind::GitStatusDraft,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the string representation of the patch status.&lt;br/&gt;    pub const fn as_str(&amp;amp;self) -&amp;gt; &amp;amp;&amp;#39;static str {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Open =&amp;gt; &amp;#34;Open&amp;#34;,&lt;br/&gt;            Self::MergedApplied =&amp;gt; &amp;#34;Merged/Applied&amp;#34;,&lt;br/&gt;            Self::Closed =&amp;gt; &amp;#34;Closed&amp;#34;,&lt;br/&gt;            Self::Draft =&amp;gt; &amp;#34;Draft&amp;#34;,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the patch is open.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_open(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Open)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the patch is merged/applied.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_merged_or_applied(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::MergedApplied)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the patch is closed.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_closed(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Closed)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the patch is drafted&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_drafted(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Draft)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl From&amp;lt;&amp;amp;PatchStatus&amp;gt; for Kind {&lt;br/&gt;    fn from(status: &amp;amp;PatchStatus) -&amp;gt; Self {&lt;br/&gt;        status.kind()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl fmt::Display for PatchStatus {&lt;br/&gt;    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter&amp;lt;&amp;#39;_&amp;gt;) -&amp;gt; fmt::Result {&lt;br/&gt;        write!(f, &amp;#34;{}&amp;#34;, self.as_str())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl TryFrom&amp;lt;Kind&amp;gt; for PatchStatus {&lt;br/&gt;    type Error = N34Error;&lt;br/&gt;&lt;br/&gt;    fn try_from(kind: Kind) -&amp;gt; Result&amp;lt;Self, Self::Error&amp;gt; {&lt;br/&gt;        match kind {&lt;br/&gt;            Kind::GitStatusOpen =&amp;gt; Ok(Self::Open),&lt;br/&gt;            Kind::GitStatusApplied =&amp;gt; Ok(Self::MergedApplied),&lt;br/&gt;            Kind::GitStatusClosed =&amp;gt; Ok(Self::Closed),&lt;br/&gt;            Kind::GitStatusDraft =&amp;gt; Ok(Self::Draft),&lt;br/&gt;            _ =&amp;gt; Err(N34Error::InvalidPatchStatus(kind)),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl GitPatch {&lt;br/&gt;    /// Returns the patch file name from the subject&lt;br/&gt;    pub fn filename(&amp;amp;self, parent: impl AsRef&amp;lt;Path&amp;gt;) -&amp;gt; N34Result&amp;lt;PathBuf&amp;gt; {&lt;br/&gt;        let (patch_version, patch_number) = if self.subject.contains(&amp;#34;[PATCH]&amp;#34;) {&lt;br/&gt;            (String::new(), &amp;#34;1&amp;#34;)&lt;br/&gt;        } else {&lt;br/&gt;            patch_version_and_subject(&amp;amp;self.subject)?&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let patch_name = if patch_number == &amp;#34;0&amp;#34; {&lt;br/&gt;            &amp;#34;cover-letter&amp;#34;.to_owned()&lt;br/&gt;        } else {&lt;br/&gt;            patch_file_name(&amp;amp;self.subject)?&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        Ok(parent&lt;br/&gt;            .as_ref()&lt;br/&gt;            .join(format!(&amp;#34;{patch_version}{patch_number:0&amp;gt;4}-{patch_name}&amp;#34;).replace(&amp;#34;--&amp;#34;, &amp;#34;-&amp;#34;))&lt;br/&gt;            .with_extension(&amp;#34;patch&amp;#34;))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for PatchSubcommands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::run_command!(self, options, &amp;amp; Send Fetch Close Reopen Draft Apply Merge List)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl FromStr for GitPatch {&lt;br/&gt;    type Err = String;&lt;br/&gt;&lt;br/&gt;    fn from_str(patch_content: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, Self::Err&amp;gt; {&lt;br/&gt;        // Regex for subject (handles multi-line subjects)&lt;br/&gt;        let subject = SUBJECT_RE&lt;br/&gt;            .captures(patch_content)&lt;br/&gt;            .and_then(|cap| cap.get(1))&lt;br/&gt;            .ok_or(&amp;#34;No subject found&amp;#34;)?&lt;br/&gt;            .as_str()&lt;br/&gt;            .trim()&lt;br/&gt;            .replace(&amp;#39;\n&amp;#39;, &amp;#34;&amp;#34;)&lt;br/&gt;            .to_string();&lt;br/&gt;&lt;br/&gt;        // Regex for body&lt;br/&gt;        let body = BODY_RE&lt;br/&gt;            .captures(patch_content)&lt;br/&gt;            .and_then(|cap| cap.get(1))&lt;br/&gt;            .ok_or(&amp;#34;No body found&amp;#34;)?&lt;br/&gt;            .as_str()&lt;br/&gt;            .trim()&lt;br/&gt;            .to_string();&lt;br/&gt;        Ok(Self {&lt;br/&gt;            inner: patch_content.to_owned(),&lt;br/&gt;            subject,&lt;br/&gt;            body,&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts the version prefix and patch number from a patch subject string.&lt;br/&gt;///&lt;br/&gt;/// The version prefix is formatted as &amp;#34;v{version}-&amp;#34; if present, or an empty&lt;br/&gt;/// string. The patch number is mandatory and will cause an error if not found.&lt;br/&gt;fn patch_version_and_subject(subject: &amp;amp;str) -&amp;gt; N34Result&amp;lt;(String, &amp;amp;str)&amp;gt; {&lt;br/&gt;    let captures = PATCH_VERSION_NUMBER_RE.captures(subject).ok_or_else(|| {&lt;br/&gt;        N34Error::InvalidEvent(format!(&amp;#34;Can not parse the patch subject `{subject}`&amp;#34;))&lt;br/&gt;    })?;&lt;br/&gt;    Ok((&lt;br/&gt;        captures&lt;br/&gt;            .name(&amp;#34;version&amp;#34;)&lt;br/&gt;            .map(|m| format!(&amp;#34;v{}-&amp;#34;, m.as_str()))&lt;br/&gt;            .unwrap_or_default(),&lt;br/&gt;        captures&lt;br/&gt;            .name(&amp;#34;number&amp;#34;)&lt;br/&gt;            .map(|m| m.as_str())&lt;br/&gt;            .expect(&amp;#34;It&amp;#39;s not optional, regex will fail if it&amp;#39;s not found&amp;#34;),&lt;br/&gt;    ))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts a clean file name from the patch subject by removing version info&lt;br/&gt;/// and special characters. Converts to lowercase and ensures the name only&lt;br/&gt;/// contains alphanumeric, &amp;#39;.&amp;#39;, &amp;#39;-&amp;#39;, or &amp;#39;_&amp;#39; characters.&lt;br/&gt;fn patch_file_name(subject: &amp;amp;str) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    Ok(subject&lt;br/&gt;        .split_once(&amp;#34;]&amp;#34;)&lt;br/&gt;        .ok_or_else(|| {&lt;br/&gt;            N34Error::InvalidEvent(format!(&lt;br/&gt;                &amp;#34;Invalid patch subject. No `[PATCH ...]`: `{subject}`&amp;#34;,&lt;br/&gt;            ))&lt;br/&gt;        })?&lt;br/&gt;        .1&lt;br/&gt;        .trim()&lt;br/&gt;        .to_lowercase()&lt;br/&gt;        .replace(&lt;br/&gt;            |c: char| !c.is_ascii_alphanumeric() &amp;amp;&amp;amp; ![&amp;#39;.&amp;#39;, &amp;#39;-&amp;#39;, &amp;#39;_&amp;#39;].contains(&amp;amp;c),&lt;br/&gt;            &amp;#34;-&amp;#34;,&lt;br/&gt;        )&lt;br/&gt;        .chars()&lt;br/&gt;        .take(60)&lt;br/&gt;        .collect::&amp;lt;String&amp;gt;()&lt;br/&gt;        .trim_matches(&amp;#39;-&amp;#39;)&lt;br/&gt;        .trim()&lt;br/&gt;        .replace(&amp;#34;--&amp;#34;, &amp;#34;-&amp;#34;))&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:41Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs9ynyw5w2zzq5k83vd6kfdyhmx6dz0t89ctxs74pcxvjlw9cp9taszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvjngur2</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs9ynyw5w2zzq5k83vd6kfdyhmx6dz0t89ctxs74pcxvjlw9cp9taszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvjngur2" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{collections::HashSet, fs, net::SocketAddr, path::PathBuf};&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    nips::{nip19::Nip19Coordinate, nip46::NostrConnectURI},&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::traits::{MutRepoRelaySetsExt, RepoRelaySetsExt},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Errors that can occur when working with configuration files.&lt;br/&gt;#[derive(thiserror::Error, Debug)]&lt;br/&gt;pub enum ConfigError {&lt;br/&gt;    #[error(&lt;br/&gt;        &amp;#34;Could not determine the default config path: both `$XDG_CONFIG_HOME` and `$HOME` \&lt;br/&gt;         environment variables are missing or unset.&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    CanNotFindConfigPath,&lt;br/&gt;    #[error(&amp;#34;Couldn&amp;#39;t read the config file: {0}&amp;#34;)]&lt;br/&gt;    ReadFile(std::io::Error),&lt;br/&gt;    #[error(&amp;#34;Couldn&amp;#39;t write in the config file: {0}&amp;#34;)]&lt;br/&gt;    WriteFile(std::io::Error),&lt;br/&gt;    #[error(&amp;#34;Couldn&amp;#39;t serialize the config. This is a bug, please report it: {0}&amp;#34;)]&lt;br/&gt;    Serialize(toml::ser::Error),&lt;br/&gt;    #[error(&amp;#34;Failed to parse the config file: {0}&amp;#34;)]&lt;br/&gt;    ParseFile(toml::de::Error),&lt;br/&gt;    #[error(&amp;#34;Duplicate configuration set name detected: &amp;#39;{0}&amp;#39;. Each set  must have a unique name.&amp;#34;)]&lt;br/&gt;    SetDuplicateName(String),&lt;br/&gt;    #[error(&amp;#34;No set with the given name `{0}`&amp;#34;)]&lt;br/&gt;    SetNotFound(String),&lt;br/&gt;    #[error(&amp;#34;You can&amp;#39;t create an new empty set.&amp;#34;)]&lt;br/&gt;    NewEmptySet,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Configuration for the command-line interface.&lt;br/&gt;#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)]&lt;br/&gt;pub struct CliConfig {&lt;br/&gt;    /// Path to the configuration file (not serialized)&lt;br/&gt;    #[serde(skip)]&lt;br/&gt;    path:                   PathBuf,&lt;br/&gt;    /// Groups of repositories and relays.&lt;br/&gt;    #[serde(default, skip_serializing_if = &amp;#34;Vec::is_empty&amp;#34;)]&lt;br/&gt;    pub sets:               Vec&amp;lt;RepoRelaySet&amp;gt;,&lt;br/&gt;    /// The default PoW difficulty&lt;br/&gt;    #[serde(skip_serializing_if = &amp;#34;Option::is_none&amp;#34;)]&lt;br/&gt;    pub pow:                Option&amp;lt;u8&amp;gt;,&lt;br/&gt;    /// List of fallback relays used if no fallback relays was provided.&lt;br/&gt;    #[serde(skip_serializing_if = &amp;#34;Option::is_none&amp;#34;)]&lt;br/&gt;    pub fallback_relays:    Option&amp;lt;Vec&amp;lt;RelayUrl&amp;gt;&amp;gt;,&lt;br/&gt;    /// Default Nostr bunker URL used for signing events.&lt;br/&gt;    #[serde(&lt;br/&gt;        default,&lt;br/&gt;        skip_serializing_if = &amp;#34;Option::is_none&amp;#34;,&lt;br/&gt;        deserialize_with = &amp;#34;super::parsers::de_bunker_url&amp;#34;,&lt;br/&gt;        serialize_with = &amp;#34;super::parsers::ser_bunker_url&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    pub bunker_url:         Option&amp;lt;NostrConnectURI&amp;gt;,&lt;br/&gt;    /// Whether to use the system keyring to store the secret key.&lt;br/&gt;    #[serde(default)]&lt;br/&gt;    pub keyring_secret_key: bool,&lt;br/&gt;    /// Signs events using the browser&amp;#39;s NIP-07 extension.&lt;br/&gt;    #[serde(default)]&lt;br/&gt;    pub nip07:              Option&amp;lt;SocketAddr&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// A named group of repositories and relays.&lt;br/&gt;#[derive(serde::Serialize, serde::Deserialize, Default, Clone, Debug)]&lt;br/&gt;pub struct RepoRelaySet {&lt;br/&gt;    /// Unique identifier for this group.&lt;br/&gt;    pub name:   String,&lt;br/&gt;    /// Repository addresses in this group.&lt;br/&gt;    #[serde(&lt;br/&gt;        default,&lt;br/&gt;        skip_serializing_if = &amp;#34;HashSet::is_empty&amp;#34;,&lt;br/&gt;        serialize_with = &amp;#34;super::parsers::ser_naddrs&amp;#34;,&lt;br/&gt;        deserialize_with = &amp;#34;super::parsers::de_naddrs&amp;#34;&lt;br/&gt;    )]&lt;br/&gt;    pub naddrs: HashSet&amp;lt;Nip19Coordinate&amp;gt;,&lt;br/&gt;    /// Relay URLs in this group.&lt;br/&gt;    #[serde(default, skip_serializing_if = &amp;#34;HashSet::is_empty&amp;#34;)]&lt;br/&gt;    pub relays: HashSet&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CliConfig {&lt;br/&gt;    /// Reads and parse a TOML config file from the given path, creating it if&lt;br/&gt;    /// missing.&lt;br/&gt;    pub fn load(file_path: PathBuf) -&amp;gt; N34Result&amp;lt;Self&amp;gt; {&lt;br/&gt;        tracing::info!(path = %file_path.display(), &amp;#34;Loading configuration from file&amp;#34;);&lt;br/&gt;        // Make sure the file is exist&lt;br/&gt;        if let Some(parent) = file_path.parent()&lt;br/&gt;            &amp;amp;&amp;amp; !parent.exists()&lt;br/&gt;        {&lt;br/&gt;            fs::create_dir_all(parent)?;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let _ = fs::File::create_new(&amp;amp;file_path);&lt;br/&gt;&lt;br/&gt;        let mut config: Self =&lt;br/&gt;            toml::from_str(&amp;amp;fs::read_to_string(&amp;amp;file_path).map_err(ConfigError::ReadFile)?)&lt;br/&gt;                .map_err(ConfigError::ParseFile)?;&lt;br/&gt;        config.path = file_path;&lt;br/&gt;&lt;br/&gt;        config.post_sets()?;&lt;br/&gt;&lt;br/&gt;        Ok(config)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Dump the config as toml in a file&lt;br/&gt;    pub fn dump(mut self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        tracing::debug!(config = ?self, &amp;#34;Writing configuration to {}&amp;#34;, self.path.display());&lt;br/&gt;        self.post_sets()?;&lt;br/&gt;&lt;br/&gt;        fs::write(&lt;br/&gt;            &amp;amp;self.path,&lt;br/&gt;            toml::to_string_pretty(&amp;amp;self).map_err(ConfigError::Serialize)?,&lt;br/&gt;        )&lt;br/&gt;        .map_err(ConfigError::WriteFile)?;&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Performs post-processing validation on the sets after loading or before&lt;br/&gt;    /// dumping.&lt;br/&gt;    fn post_sets(&amp;amp;mut self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        self.sets.as_slice().ensure_names()?;&lt;br/&gt;        self.sets.dedup_naddrs();&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl RepoRelaySet {&lt;br/&gt;    /// Create a new [`RepoRelaySet`]&lt;br/&gt;    pub fn new(&lt;br/&gt;        name: impl Into&amp;lt;String&amp;gt;,&lt;br/&gt;        naddrs: impl IntoIterator&amp;lt;Item = Nip19Coordinate&amp;gt;,&lt;br/&gt;        relays: impl IntoIterator&amp;lt;Item = RelayUrl&amp;gt;,&lt;br/&gt;    ) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            name:   name.into(),&lt;br/&gt;            naddrs: HashSet::from_iter(naddrs),&lt;br/&gt;            relays: HashSet::from_iter(relays),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Removes duplicate repository addresses by comparing their coordinates,&lt;br/&gt;    /// ignoring embedded relays.&lt;br/&gt;    pub fn dedup_naddrs(&amp;amp;mut self) {&lt;br/&gt;        let mut seen = HashSet::new();&lt;br/&gt;        self.naddrs.retain(|n| seen.insert(n.coordinate.clone()));&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:36Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsd8xh45u37lnl8phvss4rnakfu80avns942nq3u3qnpgz364t7ggszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdf2mde</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsd8xh45u37lnl8phvss4rnakfu80avns942nq3u3qnpgz364t7ggszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdf2mde" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::hashes::sha1::Hash as Sha1Hash;&lt;br/&gt;&lt;br/&gt;use super::PatchStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, VecNostrEventExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct MergeArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:         Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The open patch id to merge it. Must be orignal root patch or&lt;br/&gt;    /// revision root&lt;br/&gt;    patch_id:       NostrEvent,&lt;br/&gt;    /// Patches that have been merged. Use this when only some patches have been&lt;br/&gt;    /// merged, not all.&lt;br/&gt;    #[arg(long = &amp;#34;patches&amp;#34;, value_name = &amp;#34;PATCH-EVENT-ID&amp;#34;)]&lt;br/&gt;    merged_patches: Vec&amp;lt;NostrEvent&amp;gt;,&lt;br/&gt;    /// The merge commit id&lt;br/&gt;    merge_commit:   Sha1Hash,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for MergeArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::patch_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.patch_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            PatchStatus::MergedApplied,&lt;br/&gt;            Some(either::Either::Left(self.merge_commit)),&lt;br/&gt;            self.merged_patches.into_event_ids(),&lt;br/&gt;            |patch_status| {&lt;br/&gt;                if patch_status.is_merged_or_applied() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t merge an already merged patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_closed() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t merge a closed patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_drafted() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t merge a drafted patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:26Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsx3v6sld2dr7yre9ewldpfuxkdqyzcecy4as5cf66280aqhfrt4rqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvy6j2uz</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsx3v6sld2dr7yre9ewldpfuxkdqyzcecy4as5cf66280aqhfrt4rqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvy6j2uz" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{iter, str::FromStr, sync::Arc};&lt;br/&gt;&lt;br/&gt;use either::Either;&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventBuilder, EventId, Kind, Tag, TagKind},&lt;br/&gt;    filter::Filter,&lt;br/&gt;    hashes::sha1::Hash as Sha1Hash,&lt;br/&gt;    nips::{nip10::Marker, nip19::ToBech32},&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use super::{&lt;br/&gt;    issue::IssueStatus,&lt;br/&gt;    patch::PatchStatus,&lt;br/&gt;    types::{NaddrOrSet, NostrEvent},&lt;br/&gt;};&lt;br/&gt;use crate::{&lt;br/&gt;    cli::traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;    nostr_utils::{NostrClient, traits::NaddrsUtils, utils},&lt;br/&gt;};&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, patch::GitPatch},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;    nostr_utils::traits::{GitIssueUtils, GitPatchUtils, ReposUtils},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Updates the issue&amp;#39;s status to `new_status` after validating it with&lt;br/&gt;/// `check_fn`.&lt;br/&gt;pub async fn issue_status_command(&lt;br/&gt;    options: CliOptions,&lt;br/&gt;    issue_id: NostrEvent,&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    new_status: IssueStatus,&lt;br/&gt;    check_fn: impl FnOnce(&amp;amp;IssueStatus) -&amp;gt; N34Result&amp;lt;()&amp;gt;,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;    let naddrs = utils::naddrs_or_file(&lt;br/&gt;        naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;        &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;    )?;&lt;br/&gt;    let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;    let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;    let user_pubk = client.pubkey().await?;&lt;br/&gt;    client&lt;br/&gt;        .add_relays(&amp;amp;[naddrs.extract_relays(), issue_id.relays].concat())&lt;br/&gt;        .await;&lt;br/&gt;&lt;br/&gt;    let owners = naddrs.extract_owners();&lt;br/&gt;    let coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;    let repos = client.fetch_repos(&amp;amp;coordinates).await?;&lt;br/&gt;    let maintainers = repos.extract_maintainers();&lt;br/&gt;    let relay_hint = repos.extract_relays().first().cloned();&lt;br/&gt;    client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;    let issue_event = client&lt;br/&gt;        .fetch_event(Filter::new().id(issue_id.event_id))&lt;br/&gt;        .await?&lt;br/&gt;        .ok_or(N34Error::CanNotFoundIssue)?;&lt;br/&gt;&lt;br/&gt;    let issue_status = client&lt;br/&gt;        .fetch_issue_status(&lt;br/&gt;            issue_id.event_id,&lt;br/&gt;            [maintainers.as_slice(), &amp;amp;[issue_event.pubkey], &amp;amp;owners].concat(),&lt;br/&gt;        )&lt;br/&gt;        .await?;&lt;br/&gt;&lt;br/&gt;    check_fn(&amp;amp;issue_status)?;&lt;br/&gt;&lt;br/&gt;    let status_event = EventBuilder::new(new_status.kind(), &amp;#34;&amp;#34;)&lt;br/&gt;        .pow(options.pow.unwrap_or_default())&lt;br/&gt;        .tag(utils::event_reply_tag(&lt;br/&gt;            &amp;amp;issue_id.event_id,&lt;br/&gt;            relay_hint.as_ref(),&lt;br/&gt;            Marker::Root,&lt;br/&gt;        ))&lt;br/&gt;        .tag(Tag::public_key(issue_event.pubkey))&lt;br/&gt;        .tags(maintainers.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;        .tags(owners.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;        .tags(&lt;br/&gt;            coordinates&lt;br/&gt;                .into_iter()&lt;br/&gt;                .map(|c| Tag::coordinate(c, relay_hint.clone())),&lt;br/&gt;        )&lt;br/&gt;        .dedup_tags()&lt;br/&gt;        .build(user_pubk);&lt;br/&gt;&lt;br/&gt;    let event_id = status_event.id.expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;    let user_relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;    let write_relays = [&lt;br/&gt;        relays,&lt;br/&gt;        naddrs.extract_relays(),&lt;br/&gt;        repos.extract_relays(),&lt;br/&gt;        utils::add_write_relays(user_relays_list.as_ref()),&lt;br/&gt;        client.read_relays_from_user(issue_event.pubkey).await,&lt;br/&gt;        client&lt;br/&gt;            .read_relays_from_users(&amp;amp;[maintainers, owners].concat())&lt;br/&gt;            .await,&lt;br/&gt;    ]&lt;br/&gt;    .concat();&lt;br/&gt;&lt;br/&gt;    let success = client&lt;br/&gt;        .send_event_to(status_event, user_relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;        .await?;&lt;br/&gt;    let nevent = utils::new_nevent(event_id, &amp;amp;success)?;&lt;br/&gt;    println!(&amp;#34;Issue status created: {nevent}&amp;#34;);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Updates the patch&amp;#39;s status to `new_status` after validating it with&lt;br/&gt;/// `check_fn`.&lt;br/&gt;pub async fn patch_status_command(&lt;br/&gt;    options: CliOptions,&lt;br/&gt;    patch_id: NostrEvent,&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    new_status: PatchStatus,&lt;br/&gt;    merge_or_applied_commits: Option&amp;lt;Either&amp;lt;Sha1Hash, Vec&amp;lt;Sha1Hash&amp;gt;&amp;gt;&amp;gt;,&lt;br/&gt;    merge_or_applied_patches: Vec&amp;lt;EventId&amp;gt;,&lt;br/&gt;    check_fn: impl FnOnce(&amp;amp;PatchStatus) -&amp;gt; N34Result&amp;lt;()&amp;gt;,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;    let naddrs = utils::naddrs_or_file(&lt;br/&gt;        naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;        &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;    )?;&lt;br/&gt;    let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;    let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;    let user_pubk = client.pubkey().await?;&lt;br/&gt;    client&lt;br/&gt;        .add_relays(&amp;amp;[naddrs.extract_relays(), patch_id.relays].concat())&lt;br/&gt;        .await;&lt;br/&gt;&lt;br/&gt;    let owners = naddrs.extract_owners();&lt;br/&gt;    let coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;    let repos = client.fetch_repos(&amp;amp;coordinates).await?;&lt;br/&gt;    let maintainers = repos.extract_maintainers();&lt;br/&gt;    let relay_hint = repos.extract_relays().first().cloned();&lt;br/&gt;    client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;    let patch_event = client.fetch_patch(patch_id.event_id).await?;&lt;br/&gt;&lt;br/&gt;    if patch_event.is_revision_patch() &amp;amp;&amp;amp; !new_status.is_merged_or_applied() {&lt;br/&gt;        return Err(N34Error::InvalidStatus(&lt;br/&gt;            &amp;#34;Invalid action for patch revision. Only &amp;#39;apply&amp;#39; or &amp;#39;merge&amp;#39; are allowed, &amp;#39;open&amp;#39;, \&lt;br/&gt;             &amp;#39;close&amp;#39;, and &amp;#39;draft&amp;#39; are not supported.&amp;#34;&lt;br/&gt;                .to_owned(),&lt;br/&gt;        ));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let (root_patch, root_revision) = get_patch_root_revision(&amp;amp;patch_event)?;&lt;br/&gt;    let patch_status = client&lt;br/&gt;        .fetch_patch_status(&lt;br/&gt;            root_patch,&lt;br/&gt;            root_revision,&lt;br/&gt;            [maintainers.as_slice(), &amp;amp;[patch_event.pubkey], &amp;amp;owners].concat(),&lt;br/&gt;        )&lt;br/&gt;        .await?;&lt;br/&gt;&lt;br/&gt;    check_fn(&amp;amp;patch_status)?;&lt;br/&gt;&lt;br/&gt;    let mut status_builder = EventBuilder::new(new_status.kind(), &amp;#34;&amp;#34;)&lt;br/&gt;        .pow(options.pow.unwrap_or_default())&lt;br/&gt;        .tag(utils::event_reply_tag(&lt;br/&gt;            &amp;amp;root_patch,&lt;br/&gt;            relay_hint.as_ref(),&lt;br/&gt;            Marker::Root,&lt;br/&gt;        ))&lt;br/&gt;        .tag(Tag::public_key(patch_event.pubkey))&lt;br/&gt;        .tags(maintainers.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;        .tags(owners.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;        .tags(&lt;br/&gt;            coordinates&lt;br/&gt;                .into_iter()&lt;br/&gt;                .map(|c| Tag::coordinate(c, relay_hint.clone())),&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;    if new_status.is_merged_or_applied() {&lt;br/&gt;        if let Some(merge_commit) = merge_or_applied_commits&lt;br/&gt;            .as_ref()&lt;br/&gt;            .and_then(|e| e.as_ref().left())&lt;br/&gt;        {&lt;br/&gt;            let commit = merge_commit.to_string();&lt;br/&gt;            status_builder = status_builder&lt;br/&gt;                .tag(Tag::custom(&lt;br/&gt;                    TagKind::custom(&amp;#34;merge-commit&amp;#34;),&lt;br/&gt;                    iter::once(&amp;amp;commit),&lt;br/&gt;                ))&lt;br/&gt;                .tag(Tag::reference(commit));&lt;br/&gt;        } else if let Some(applied_commits) = merge_or_applied_commits.and_then(|e| e.right()) {&lt;br/&gt;            let commits = applied_commits&lt;br/&gt;                .iter()&lt;br/&gt;                .map(ToString::to_string)&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;            status_builder = status_builder&lt;br/&gt;                .tag(Tag::custom(TagKind::custom(&amp;#34;applied-as-commits&amp;#34;), &amp;amp;commits))&lt;br/&gt;                .tags(commits.into_iter().map(Tag::reference));&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        if let Some(root_revision) = root_revision {&lt;br/&gt;            status_builder = status_builder.tag(utils::event_reply_tag(&lt;br/&gt;                &amp;amp;root_revision,&lt;br/&gt;                relay_hint.as_ref(),&lt;br/&gt;                Marker::Reply,&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        if !merge_or_applied_patches.is_empty() {&lt;br/&gt;            status_builder = status_builder.tags(&lt;br/&gt;                build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)&lt;br/&gt;                    .await,&lt;br/&gt;            );&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let status_event = status_builder.dedup_tags().build(user_pubk);&lt;br/&gt;&lt;br/&gt;    let event_id = status_event.id.expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;    let user_relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;    let write_relays = [&lt;br/&gt;        relays,&lt;br/&gt;        naddrs.extract_relays(),&lt;br/&gt;        repos.extract_relays(),&lt;br/&gt;        utils::add_write_relays(user_relays_list.as_ref()),&lt;br/&gt;        client.read_relays_from_user(patch_event.pubkey).await,&lt;br/&gt;        client&lt;br/&gt;            .read_relays_from_users(&amp;amp;[maintainers, owners].concat())&lt;br/&gt;            .await,&lt;br/&gt;    ]&lt;br/&gt;    .concat();&lt;br/&gt;&lt;br/&gt;    let success = client&lt;br/&gt;        .send_event_to(status_event, user_relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;        .await?;&lt;br/&gt;    let nevent = utils::new_nevent(event_id, &amp;amp;success)?;&lt;br/&gt;    println!(&amp;#34;Patch status created: {nevent}&amp;#34;);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Fetch and display patches and issues for given repositories.&lt;br/&gt;/// If `list_patches` is true, lists patches instead of issues.&lt;br/&gt;/// `limit` controls the maximum number of items to fetch.&lt;br/&gt;pub async fn list_patches_and_issues(&lt;br/&gt;    options: CliOptions,&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    list_patches: bool,&lt;br/&gt;    limit: usize,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;    let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(&lt;br/&gt;        naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;        &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;    )?)?;&lt;br/&gt;&lt;br/&gt;    let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;    let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;    client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;    let coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;    let repos = client.fetch_repos(&amp;amp;coordinates).await?;&lt;br/&gt;    let authorized_pubkeys = [naddrs.extract_owners(), repos.extract_maintainers()].concat();&lt;br/&gt;    client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;    // This helps discover issues and their status.&lt;br/&gt;    client&lt;br/&gt;        .add_relays(&amp;amp;client.read_relays_from_users(&amp;amp;authorized_pubkeys).await)&lt;br/&gt;        .await;&lt;br/&gt;&lt;br/&gt;    let kind = if list_patches {&lt;br/&gt;        Kind::GitPatch&lt;br/&gt;    } else {&lt;br/&gt;        Kind::GitIssue&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let mut filter = Filter::new()&lt;br/&gt;        .coordinates(coordinates.iter())&lt;br/&gt;        .kind(kind)&lt;br/&gt;        .limit(limit);&lt;br/&gt;&lt;br/&gt;    if list_patches {&lt;br/&gt;        filter = filter.hashtag(&amp;#34;root&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let arc_client = Arc::new(client);&lt;br/&gt;    // Events are sorted by kind in ascending order:&lt;br/&gt;    // 1630 (Open), 1631 (Resolved/Applied), 1632 (Closed), 1633 (Draft)&lt;br/&gt;    let events = utils::sort_by_key(&lt;br/&gt;        future::join_all(&lt;br/&gt;            arc_client&lt;br/&gt;                .fetch_events(filter)&lt;br/&gt;                .await?&lt;br/&gt;                .into_iter()&lt;br/&gt;                .take(limit)&lt;br/&gt;                .map(|event| {&lt;br/&gt;                    let c = arc_client.clone();&lt;br/&gt;                    let keys = authorized_pubkeys.clone();&lt;br/&gt;                    async move {&lt;br/&gt;                        let status = if list_patches {&lt;br/&gt;                            let (root, root_revision) = get_patch_root_revision(&amp;amp;event)?;&lt;br/&gt;                            c.fetch_patch_status(&lt;br/&gt;                                root,&lt;br/&gt;                                root_revision,&lt;br/&gt;                                [keys.as_slice(), &amp;amp;[event.pubkey]].concat(),&lt;br/&gt;                            )&lt;br/&gt;                            .await&lt;br/&gt;                            .map(Either::Left)?&lt;br/&gt;                        } else {&lt;br/&gt;                            c.fetch_issue_status(&lt;br/&gt;                                event.id,&lt;br/&gt;                                [keys.as_slice(), &amp;amp;[event.pubkey]].concat(),&lt;br/&gt;                            )&lt;br/&gt;                            .await&lt;br/&gt;                            .map(Either::Right)?&lt;br/&gt;                        };&lt;br/&gt;                        N34Result::Ok((event, status))&lt;br/&gt;                    }&lt;br/&gt;                }),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .into_iter()&lt;br/&gt;        .filter_map(|r| r.ok()),&lt;br/&gt;        |(_, status)| status.as_ref().either_into::&amp;lt;Kind&amp;gt;(),&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    let lines = events&lt;br/&gt;        .map(|(event, status)| format_patch_and_issue(&amp;amp;event, status))&lt;br/&gt;        .collect::&amp;lt;Vec&amp;lt;String&amp;gt;&amp;gt;();&lt;br/&gt;&lt;br/&gt;    let max_width = lines&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|s| s.split_once(&amp;#39;\n&amp;#39;).map_or(85, |(l, _)| l.chars().count()))&lt;br/&gt;        .max()&lt;br/&gt;        .unwrap_or(85)&lt;br/&gt;        .max(67); // length of the event id&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;{}&amp;#34;, lines.join(&amp;amp;format!(&amp;#34;{}\n&amp;#34;, &amp;#34;-&amp;#34;.repeat(max_width))));&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns a tuple of (root_id, patch_id) if this is a valid root or revision&lt;br/&gt;/// patch.&lt;br/&gt;fn get_patch_root_revision(patch_event: &amp;amp;Event) -&amp;gt; N34Result&amp;lt;(EventId, Option&amp;lt;EventId&amp;gt;)&amp;gt; {&lt;br/&gt;    if patch_event.is_revision_patch() {&lt;br/&gt;        Ok((&lt;br/&gt;            patch_event.root_patch_from_revision()?,&lt;br/&gt;            Some(patch_event.id),&lt;br/&gt;        ))&lt;br/&gt;    } else if patch_event.is_root_patch() {&lt;br/&gt;        Ok((patch_event.id, None))&lt;br/&gt;    } else {&lt;br/&gt;        Err(N34Error::NotRootPatch)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Formats an event as either a patch or an issue. For patches, extracts the&lt;br/&gt;/// subject line from the Git patch format. For issues, combines the subject&lt;br/&gt;/// with labels. The output includes status and formatted ID.&lt;br/&gt;fn format_patch_and_issue(event: &amp;amp;Event, status: Either&amp;lt;PatchStatus, IssueStatus&amp;gt;) -&amp;gt; String {&lt;br/&gt;    let subject = if status.is_left() {&lt;br/&gt;        GitPatch::from_str(&amp;amp;event.content)&lt;br/&gt;            .map(|p| p.subject)&lt;br/&gt;            .unwrap_or_else(|_| {&lt;br/&gt;                event&lt;br/&gt;                    .content&lt;br/&gt;                    .lines()&lt;br/&gt;                    .find(|line| line.trim().starts_with(&amp;#34;Subject: &amp;#34;))&lt;br/&gt;                    .unwrap_or_default()&lt;br/&gt;                    .trim()&lt;br/&gt;                    .trim_start_matches(&amp;#34;Subject: &amp;#34;)&lt;br/&gt;                    .to_owned()&lt;br/&gt;            })&lt;br/&gt;    } else {&lt;br/&gt;        let labels = event.extract_issue_labels();&lt;br/&gt;        let subject = event.extract_issue_subject();&lt;br/&gt;&lt;br/&gt;        if labels.is_empty() {&lt;br/&gt;            subject.to_owned()&lt;br/&gt;        } else {&lt;br/&gt;            format!(r#&amp;#34;&amp;#34;{subject}&amp;#34; {labels}&amp;#34;#)&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;    format!(&lt;br/&gt;        &amp;#34;({status}) {}\nID: {}\n&amp;#34;,&lt;br/&gt;        utils::smart_wrap(&amp;amp;subject, 85),&lt;br/&gt;        event.id.to_bech32().expect(&amp;#34;Infallible&amp;#34;)&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Generates a list of tags for quoting patches in merge/applied status events.&lt;br/&gt;async fn build_patches_quote(&lt;br/&gt;    client: NostrClient,&lt;br/&gt;    relay_hint: Option&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;    patches: Vec&amp;lt;EventId&amp;gt;,&lt;br/&gt;) -&amp;gt; Vec&amp;lt;Tag&amp;gt; {&lt;br/&gt;    let client = Arc::new(client);&lt;br/&gt;    let relay_hint = Arc::new(relay_hint);&lt;br/&gt;&lt;br/&gt;    future::join_all(patches.into_iter().map(|eid| {&lt;br/&gt;        let task_relay = Arc::clone(&amp;amp;relay_hint);&lt;br/&gt;        let task_client = Arc::clone(&amp;amp;client);&lt;br/&gt;&lt;br/&gt;        async move {&lt;br/&gt;            Tag::custom(&lt;br/&gt;                TagKind::q(),&lt;br/&gt;                [&lt;br/&gt;                    eid.to_hex(),&lt;br/&gt;                    task_relay&lt;br/&gt;                        .as_ref()&lt;br/&gt;                        .as_ref()&lt;br/&gt;                        .map(|r| r.to_string())&lt;br/&gt;                        .unwrap_or_default(),&lt;br/&gt;                    task_client&lt;br/&gt;                        .event_author(eid)&lt;br/&gt;                        .await&lt;br/&gt;                        .ok()&lt;br/&gt;                        .flatten()&lt;br/&gt;                        .map(|p| p.to_hex())&lt;br/&gt;                        .unwrap_or_default(),&lt;br/&gt;                ],&lt;br/&gt;            )&lt;br/&gt;        }&lt;br/&gt;    }))&lt;br/&gt;    .await&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:16Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqszjm6czyyn3zqhzas7hmwy0utxrnukd9ynkr275tm9krjlm60234szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8lsefj</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqszjm6czyyn3zqhzas7hmwy0utxrnukd9ynkr275tm9krjlm60234szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8lsefj" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::num::NonZeroUsize;&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ListArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;)]&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// Maximum number of patches to list&lt;br/&gt;    #[arg(long, default_value = &amp;#34;15&amp;#34;)]&lt;br/&gt;    limit:  NonZeroUsize,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ListArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        common_commands::list_patches_and_issues(options, self.naddrs, true, self.limit.into())&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:15Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqswkvyq4gzmg5sksa2avkkfzw7qu8uysvkyj7gqahvf7fqnpmxzmmszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvpmamvt</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqswkvyq4gzmg5sksa2avkkfzw7qu8uysvkyj7gqahvf7fqnpmxzmmszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvpmamvt" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    fs,&lt;br/&gt;    path::{Path, PathBuf},&lt;br/&gt;    str::FromStr,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Kind, TagKind},&lt;br/&gt;    filter::Filter,&lt;br/&gt;    nips::nip19::ToBech32,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;    nostr_utils::{&lt;br/&gt;        NostrClient,&lt;br/&gt;        traits::{NaddrsUtils, ReposUtils},&lt;br/&gt;        utils,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct FetchArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// Output directory for the patches. Default to the current directory&lt;br/&gt;    #[arg(short, long, value_name = &amp;#34;PATH&amp;#34;)]&lt;br/&gt;    output:   Option&amp;lt;PathBuf&amp;gt;,&lt;br/&gt;    /// The patch id to fetch it&lt;br/&gt;    patch_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for FetchArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = utils::naddrs_or_file(&lt;br/&gt;            self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;            &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;        )?;&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        let output_path = self.output.unwrap_or_default();&lt;br/&gt;&lt;br/&gt;        client&lt;br/&gt;            .add_relays(&lt;br/&gt;                &amp;amp;[&lt;br/&gt;                    naddrs.extract_relays(),&lt;br/&gt;                    self.patch_id.relays,&lt;br/&gt;                    client&lt;br/&gt;                        .fetch_repos(&amp;amp;naddrs.into_coordinates())&lt;br/&gt;                        .await?&lt;br/&gt;                        .extract_relays(),&lt;br/&gt;                ]&lt;br/&gt;                .concat(),&lt;br/&gt;            )&lt;br/&gt;            .await;&lt;br/&gt;&lt;br/&gt;        let root_patch = client&lt;br/&gt;            .fetch_event(&lt;br/&gt;                Filter::new()&lt;br/&gt;                    .id(self.patch_id.event_id)&lt;br/&gt;                    .kind(Kind::GitPatch),&lt;br/&gt;            )&lt;br/&gt;            .await?&lt;br/&gt;            .ok_or(N34Error::CanNotFoundPatch)?;&lt;br/&gt;&lt;br/&gt;        if !root_patch&lt;br/&gt;            .tags&lt;br/&gt;            .iter()&lt;br/&gt;            .any(|t| t.kind() == TagKind::t() &amp;amp;&amp;amp; t.content().is_some_and(|c| c == &amp;#34;root&amp;#34;))&lt;br/&gt;        {&lt;br/&gt;            return Err(N34Error::NotRootPatch);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let root_author = root_patch.pubkey;&lt;br/&gt;        let root_patch = super::GitPatch::from_str(&amp;amp;root_patch.content)&lt;br/&gt;            .map_err(|err| N34Error::InvalidEvent(format!(&amp;#34;Failed to parse the patch: {err}&amp;#34;)))?;&lt;br/&gt;&lt;br/&gt;        tracing::info!(&amp;#34;Found the root patch: `{}`&amp;#34;, root_patch.subject);&lt;br/&gt;&lt;br/&gt;        let mut patches = client&lt;br/&gt;            .fetch_patch_series(self.patch_id.event_id, root_author)&lt;br/&gt;            .await?&lt;br/&gt;            .into_iter()&lt;br/&gt;            .map(|p| {&lt;br/&gt;                let patch = super::GitPatch::from_str(&amp;amp;p.content).map_err(|err| {&lt;br/&gt;                    N34Error::InvalidEvent(format!(&lt;br/&gt;                        &amp;#34;Failed to parse the patch `{}`: {err}&amp;#34;,&lt;br/&gt;                        p.id.to_bech32().expect(&amp;#34;Infallible&amp;#34;)&lt;br/&gt;                    ))&lt;br/&gt;                })?;&lt;br/&gt;                N34Result::Ok((patch.filename(&amp;amp;output_path)?, patch))&lt;br/&gt;            })&lt;br/&gt;            .collect::&amp;lt;N34Result&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;&amp;gt;()?;&lt;br/&gt;        patches.push((root_patch.filename(&amp;amp;output_path)?, root_patch));&lt;br/&gt;        patches.sort_unstable_by_key(|p| p.0.clone());&lt;br/&gt;        patches.dedup_by_key(|p| p.0.clone());&lt;br/&gt;&lt;br/&gt;        if output_path.as_path() != Path::new(&amp;#34;&amp;#34;) &amp;amp;&amp;amp; !output_path.exists() {&lt;br/&gt;            fs::create_dir_all(&amp;amp;output_path)?;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        for (patch_path, patch) in patches {&lt;br/&gt;            tracing::info!(&amp;#34;Writeing `{}` in `{}`&amp;#34;, patch.subject, patch_path.display());&lt;br/&gt;            fs::write(patch_path, patch.inner)?;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:03Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsg2h2kgnwdse7lsdurt5xjusd6yw5nm53lzn7nh89xx0krqkqml3szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvc587c3</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsg2h2kgnwdse7lsdurt5xjusd6yw5nm53lzn7nh89xx0krqkqml3szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvc587c3" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::collections::HashSet;&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, RelayOrSet},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct UpdateArgs {&lt;br/&gt;    /// Name of the set to update&lt;br/&gt;    name:         String,&lt;br/&gt;    /// Add relay to the set, either as URL or set name to extract its relays.&lt;br/&gt;    /// [aliases: `--sr`]&lt;br/&gt;    #[arg(long = &amp;#34;set-relay&amp;#34;, alias(&amp;#34;sr&amp;#34;))]&lt;br/&gt;    relays:       Vec&amp;lt;RelayOrSet&amp;gt;,&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:       Vec&amp;lt;NaddrOrSet&amp;gt;,&lt;br/&gt;    /// Replace existing relays/repositories instead of adding to them&lt;br/&gt;    #[arg(long = &amp;#34;override&amp;#34;)]&lt;br/&gt;    override_set: bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for UpdateArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?;&lt;br/&gt;        let relays = self.relays.flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;&lt;br/&gt;        let set = options.config.sets.get_mut_set(&amp;amp;self.name)?;&lt;br/&gt;&lt;br/&gt;        if self.override_set {&lt;br/&gt;            if !relays.is_empty() {&lt;br/&gt;                set.relays = HashSet::from_iter(relays);&lt;br/&gt;            }&lt;br/&gt;            if !naddrs.is_empty() {&lt;br/&gt;                set.naddrs = HashSet::from_iter(naddrs)&lt;br/&gt;            }&lt;br/&gt;        } else {&lt;br/&gt;            set.relays.extend(relays);&lt;br/&gt;            set.naddrs.extend(naddrs);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:46:03Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsts248wd76ge6d55rk3evtrkkar2ky8jky48dajek0c299qewzfrszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvh63zdq</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsts248wd76ge6d55rk3evtrkkar2ky8jky48dajek0c299qewzfrszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvh63zdq" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use super::PatchStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::CommandRunner,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct DraftArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The open patch id to draft it. Must be orignal root patch&lt;br/&gt;    patch_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for DraftArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::patch_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.patch_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            PatchStatus::Draft,&lt;br/&gt;            None,&lt;br/&gt;            Vec::new(),&lt;br/&gt;            |patch_status| {&lt;br/&gt;                if patch_status.is_drafted() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t draft an already drafted patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_closed() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t draft a closed patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_merged_or_applied() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t draft a merged/applied patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:52Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs8sd0v2lhauaklyt7pejurq7qxsexx587v3v84wwsmftjer6fqz2gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsvwx3t</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs8sd0v2lhauaklyt7pejurq7qxsexx587v3v84wwsmftjer6fqz2gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsvwx3t" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::nips::nip19::ToBech32;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        RepoRelaySet,&lt;br/&gt;        traits::{CommandRunner, RepoRelaySetsExt},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct ShowArgs {&lt;br/&gt;    /// Name of the set to display. If not provided, lists all available sets.&lt;br/&gt;    name: Option&amp;lt;String&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ShowArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        if let Some(name) = self.name {&lt;br/&gt;            println!(&lt;br/&gt;                &amp;#34;{}&amp;#34;,&lt;br/&gt;                format_set(options.config.sets.as_slice().get_set(&amp;amp;name)?)&lt;br/&gt;            );&lt;br/&gt;        } else {&lt;br/&gt;            println!(&lt;br/&gt;                &amp;#34;{}&amp;#34;,&lt;br/&gt;                options&lt;br/&gt;                    .config&lt;br/&gt;                    .sets&lt;br/&gt;                    .iter()&lt;br/&gt;                    .map(format_set)&lt;br/&gt;                    .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;                    .join(&amp;#34;\n----------\n&amp;#34;)&lt;br/&gt;            );&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Format a set to view it to the user&lt;br/&gt;fn format_set(set: &amp;amp;RepoRelaySet) -&amp;gt; String {&lt;br/&gt;    let naddrs = if set.naddrs.is_empty() {&lt;br/&gt;        &amp;#34;Nothing&amp;#34;.to_owned()&lt;br/&gt;    } else {&lt;br/&gt;        format!(&lt;br/&gt;            &amp;#34;\n- {}&amp;#34;,&lt;br/&gt;            set.naddrs&lt;br/&gt;                .iter()&lt;br/&gt;                .map(|naddr| naddr.to_bech32().expect(&amp;#34;We did decoded before&amp;#34;))&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;                .join(&amp;#34;\n- &amp;#34;)&lt;br/&gt;        )&lt;br/&gt;    };&lt;br/&gt;    let relays = if set.relays.is_empty() {&lt;br/&gt;        &amp;#34;Nothing&amp;#34;.to_owned()&lt;br/&gt;    } else {&lt;br/&gt;        format!(&lt;br/&gt;            &amp;#34;\n- {}&amp;#34;,&lt;br/&gt;            set.relays&lt;br/&gt;                .iter()&lt;br/&gt;                .map(|relay| relay.to_string())&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;                .join(&amp;#34;\n- &amp;#34;)&lt;br/&gt;        )&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    format!(&lt;br/&gt;        &amp;#34;Name: {}\nّّRepositories: {naddrs}\nRelays: {relays}&amp;#34;,&lt;br/&gt;        set.name&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:52Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqspw7jgyrtgkccqj4r0y6nwt2xy4ppf5v9yl7x7r33wdd485v9f03gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvt9sg3l</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqspw7jgyrtgkccqj4r0y6nwt2xy4ppf5v9yl7x7r33wdd485v9f03gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvt9sg3l" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use super::PatchStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::CommandRunner,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct CloseArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The open/drafted patch id to close it. Must be orignal root patch&lt;br/&gt;    patch_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for CloseArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::patch_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.patch_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            PatchStatus::Closed,&lt;br/&gt;            None,&lt;br/&gt;            Vec::new(),&lt;br/&gt;            |patch_status| {&lt;br/&gt;                if patch_status.is_closed() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t close an already closed patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_merged_or_applied() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t close a merged/applied patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:41Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsqesvjxlnwdcfymtjswmfdqyehem6gk3uwmnfh602k0gu0zg0sttczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv6aug9p</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsqesvjxlnwdcfymtjswmfdqyehem6gk3uwmnfh602k0gu0zg0sttczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv6aug9p" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, RelayOrSet},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct RemoveArgs {&lt;br/&gt;    /// Set name to delete&lt;br/&gt;    name:   String,&lt;br/&gt;    /// Specific relay to remove it from the set, either as URL or set name to&lt;br/&gt;    /// extract its relays. [aliases: `--sr`]&lt;br/&gt;    #[arg(long = &amp;#34;set-relay&amp;#34;, alias(&amp;#34;sr&amp;#34;))]&lt;br/&gt;    relays: Vec&amp;lt;RelayOrSet&amp;gt;,&lt;br/&gt;&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs: Vec&amp;lt;NaddrOrSet&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for RemoveArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?;&lt;br/&gt;        let relays = self.relays.flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;&lt;br/&gt;        if relays.is_empty() &amp;amp;&amp;amp; naddrs.is_empty() {&lt;br/&gt;            options.config.sets.remove_set(self.name)?;&lt;br/&gt;        } else {&lt;br/&gt;            if !relays.is_empty() {&lt;br/&gt;                options&lt;br/&gt;                    .config&lt;br/&gt;                    .sets&lt;br/&gt;                    .remove_relays(&amp;amp;self.name, relays.into_iter())?;&lt;br/&gt;            }&lt;br/&gt;            if !naddrs.is_empty() {&lt;br/&gt;                options&lt;br/&gt;                    .config&lt;br/&gt;                    .sets&lt;br/&gt;                    .remove_naddrs(self.name, naddrs.into_iter())?;&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:41Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqspjsf8zymt2tglhphn5na3520hew308hvwsw3mq3dhjrxnhwqy92czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4t457e</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqspjsf8zymt2tglhphn5na3520hew308hvwsw3mq3dhjrxnhwqy92czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4t457e" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::hashes::sha1::Hash as Sha1Hash;&lt;br/&gt;&lt;br/&gt;use super::PatchStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, VecNostrEventExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ApplyArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:          Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The open patch id to apply it. Must be orignal root patch or&lt;br/&gt;    /// revision root&lt;br/&gt;    patch_id:        NostrEvent,&lt;br/&gt;    /// Patches that have been applied. Use this when only some patches have&lt;br/&gt;    /// been applied, not all.&lt;br/&gt;    #[arg(long = &amp;#34;patches&amp;#34;, value_name = &amp;#34;PATCH-EVENT-ID&amp;#34;)]&lt;br/&gt;    applied_patches: Vec&amp;lt;NostrEvent&amp;gt;,&lt;br/&gt;    /// The applied commits&lt;br/&gt;    #[arg(num_args = 1..)]&lt;br/&gt;    applied_commits: Vec&amp;lt;Sha1Hash&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ApplyArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::patch_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.patch_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            PatchStatus::MergedApplied,&lt;br/&gt;            Some(either::Either::Right(self.applied_commits)),&lt;br/&gt;            self.applied_patches.into_event_ids(),&lt;br/&gt;            |patch_status| {&lt;br/&gt;                if patch_status.is_merged_or_applied() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t apply an already applied patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_closed() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t apply a closed patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_drafted() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t apply a drafted patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:27Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs86urpmaltjs5nefa4x6c9rhus0pwtcdmlrpnmgyew3xxphc29z6czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv85axnl</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs86urpmaltjs5nefa4x6c9rhus0pwtcdmlrpnmgyew3xxphc29z6czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv85axnl" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        ConfigError,&lt;br/&gt;        traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, RelayOrSet},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct NewArgs {&lt;br/&gt;    /// Unique name for the set&lt;br/&gt;    name:   String,&lt;br/&gt;    /// Optional relay to add it to the set, either as URL or set name to&lt;br/&gt;    /// extract its relays. [aliases: `--sr`]&lt;br/&gt;    #[arg(long = &amp;#34;set-relay&amp;#34;, alias(&amp;#34;sr&amp;#34;))]&lt;br/&gt;    relays: Vec&amp;lt;RelayOrSet&amp;gt;,&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs: Vec&amp;lt;NaddrOrSet&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for NewArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?;&lt;br/&gt;        let relays = self.relays.flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;&lt;br/&gt;        if relays.is_empty() &amp;amp;&amp;amp; naddrs.is_empty() {&lt;br/&gt;            return Err(ConfigError::NewEmptySet.into());&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.sets.push_set(self.name, naddrs, relays)?;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:26Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsggrn9ww07rjv0lt4znc38zxawj53rh8jef57ew23tjqjlswqtf2qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8xqfxh</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsggrn9ww07rjv0lt4znc38zxawj53rh8jef57ew23tjqjlswqtf2qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8xqfxh" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `sets new` command&lt;br/&gt;mod new;&lt;br/&gt;/// `sets remove` command&lt;br/&gt;mod remove;&lt;br/&gt;/// `sets show` commands&lt;br/&gt;mod show;&lt;br/&gt;/// `sets update` command&lt;br/&gt;mod update;&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;&lt;br/&gt;use self::new::NewArgs;&lt;br/&gt;use self::remove::RemoveArgs;&lt;br/&gt;use self::show::ShowArgs;&lt;br/&gt;use self::update::UpdateArgs;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::error::N34Result;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum SetsSubcommands {&lt;br/&gt;    /// Remove a set, or specific repos and relays within it&lt;br/&gt;    Remove(RemoveArgs),&lt;br/&gt;    /// Create a new set&lt;br/&gt;    New(NewArgs),&lt;br/&gt;    /// Modify an existing set&lt;br/&gt;    Update(UpdateArgs),&lt;br/&gt;    /// Show a single set or all the stored sets&lt;br/&gt;    Show(ShowArgs),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl CommandRunner for SetsSubcommands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::run_command!(self, options, &amp;amp; Remove New Update Show)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:16Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsqf94rcrztg782c4qvehz2h442myylg6p3v958wq95evekvykz02szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvxnuskw</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsqf94rcrztg782c4qvehz2h442myylg6p3v958wq95evekvykz02szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvxnuskw" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `config` subcommands&lt;br/&gt;pub mod config;&lt;br/&gt;/// `issue` subcommands&lt;br/&gt;pub mod issue;&lt;br/&gt;/// `patch` subcommands&lt;br/&gt;pub mod patch;&lt;br/&gt;/// `reply` command&lt;br/&gt;pub mod reply;&lt;br/&gt;/// `repo` subcommands&lt;br/&gt;pub mod repo;&lt;br/&gt;/// `sets` subcommands&lt;br/&gt;pub mod sets;&lt;br/&gt;&lt;br/&gt;use std::fmt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;use std::time::Duration;&lt;br/&gt;&lt;br/&gt;use clap::{Args, Parser};&lt;br/&gt;use nostr::key::{Keys, SecretKey};&lt;br/&gt;use nostr::nips::nip46::NostrConnectURI;&lt;br/&gt;use nostr::signer::{IntoNostrSigner, NostrSigner};&lt;br/&gt;use nostr_connect::client::NostrConnect;&lt;br/&gt;&lt;br/&gt;use self::config::ConfigSubcommands;&lt;br/&gt;use self::issue::IssueSubcommands;&lt;br/&gt;use self::patch::PatchSubcommands;&lt;br/&gt;use self::reply::ReplyArgs;&lt;br/&gt;use self::repo::RepoSubcommands;&lt;br/&gt;use self::sets::SetsSubcommands;&lt;br/&gt;use super::CliConfig;&lt;br/&gt;use super::options_state::OptionsState;&lt;br/&gt;use super::types::RelayOrSet;&lt;br/&gt;use super::{parsers, traits::CommandRunner};&lt;br/&gt;use crate::cli::Cli;&lt;br/&gt;use crate::cli::types::EchoAuthUrl;&lt;br/&gt;use crate::error::{N34Error, N34Result};&lt;br/&gt;&lt;br/&gt;/// Default path used when no path is provided via command line arguments.&lt;br/&gt;///&lt;br/&gt;/// This is a workaround since Clap doesn&amp;#39;t support lazy evaluation of defaults.&lt;br/&gt;pub const DEFAULT_FALLBACK_PATH: &amp;amp;str = &amp;#34;I_DO_NOT_KNOW_WHY_CLAP_DO_NOT_SUPPORT_LAZY_DEFAULT&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// How long to wait for bunker response (3 minutes).&lt;br/&gt;const BUNKER_TIMEOUT: Duration = Duration::from_secs(60 * 3);&lt;br/&gt;&lt;br/&gt;/// The command-line interface options&lt;br/&gt;#[derive(Args)]&lt;br/&gt;pub struct CliOptions {&lt;br/&gt;    /// Your Nostr secret key&lt;br/&gt;    #[arg(short, long, group = &amp;#34;signer&amp;#34;)]&lt;br/&gt;    pub secret_key: Option&amp;lt;SecretKey&amp;gt;,&lt;br/&gt;    /// NIP-46 bunker url used for signing events&lt;br/&gt;    #[arg(short, long, group = &amp;#34;signer&amp;#34;, value_parser = parsers::parse_bunker_url)]&lt;br/&gt;    pub bunker_url: Option&amp;lt;NostrConnectURI&amp;gt;,&lt;br/&gt;    /// Enables signing events using the browser&amp;#39;s NIP-07 extension. Listens on&lt;br/&gt;    /// `127.0.0.1:51034`.&lt;br/&gt;    #[arg(short = &amp;#39;7&amp;#39;, long, group = &amp;#34;signer&amp;#34;)]&lt;br/&gt;    pub nip07:      bool,&lt;br/&gt;    /// Fallbacks relay to write and read from it. Multiple relays can be&lt;br/&gt;    /// passed.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    pub relays:     Vec&amp;lt;RelayOrSet&amp;gt;,&lt;br/&gt;    /// Proof of Work difficulty when creatring events&lt;br/&gt;    #[arg(long, value_name = &amp;#34;DIFFICULTY&amp;#34;)]&lt;br/&gt;    pub pow:        Option&amp;lt;u8&amp;gt;,&lt;br/&gt;    /// Config path [default: `$XDG_CONFIG_HOME` or `$HOME/.config`]&lt;br/&gt;    #[arg(long, value_name = &amp;#34;PATH&amp;#34;, default_value = DEFAULT_FALLBACK_PATH,&lt;br/&gt;         hide_default_value = true, value_parser = parsers::parse_config_path&lt;br/&gt;     )]&lt;br/&gt;    pub config:     CliConfig,&lt;br/&gt;    /// The state of options. Some values that are used by them but should not&lt;br/&gt;    /// be entered via the CLI&lt;br/&gt;    #[arg(skip)]&lt;br/&gt;    pub state:      OptionsState,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// N34 commands&lt;br/&gt;#[derive(Parser, Debug)]&lt;br/&gt;pub enum Commands {&lt;br/&gt;    /// Manage repositories and relays sets&lt;br/&gt;    Sets {&lt;br/&gt;        #[command(subcommand)]&lt;br/&gt;        subcommands: SetsSubcommands,&lt;br/&gt;    },&lt;br/&gt;    /// Manage repositories&lt;br/&gt;    Repo {&lt;br/&gt;        #[command(subcommand)]&lt;br/&gt;        subcommands: RepoSubcommands,&lt;br/&gt;    },&lt;br/&gt;    /// Manage issues&lt;br/&gt;    Issue {&lt;br/&gt;        #[command(subcommand)]&lt;br/&gt;        subcommands: IssueSubcommands,&lt;br/&gt;    },&lt;br/&gt;    /// Manage patches&lt;br/&gt;    Patch {&lt;br/&gt;        #[command(subcommand)]&lt;br/&gt;        subcommands: PatchSubcommands,&lt;br/&gt;    },&lt;br/&gt;    /// Manage configuration&lt;br/&gt;    Config {&lt;br/&gt;        #[command(subcommand)]&lt;br/&gt;        subcommands: ConfigSubcommands,&lt;br/&gt;    },&lt;br/&gt;    /// Reply to issues and patches.&lt;br/&gt;    Reply(ReplyArgs),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl CliOptions {&lt;br/&gt;    /// Returns the signer&lt;br/&gt;    pub async fn signer(&amp;amp;self) -&amp;gt; N34Result&amp;lt;Option&amp;lt;Arc&amp;lt;dyn NostrSigner &#43; &amp;#39;static&amp;gt;&amp;gt;&amp;gt; {&lt;br/&gt;        if self.nip07 {&lt;br/&gt;            self.state.browser_signer_proxy.start().await?;&lt;br/&gt;&lt;br/&gt;            println!(&lt;br/&gt;                &amp;#34;Browser signer proxy started at: {}&amp;#34;,&lt;br/&gt;                self.state.browser_signer_proxy.url()&lt;br/&gt;            );&lt;br/&gt;&lt;br/&gt;            // FIXME: Use `BrowserSignerProxy::is_session_active` after it release&lt;br/&gt;            // nostr@0.44.0&lt;br/&gt;            tokio::time::sleep(Duration::from_secs(10)).await;&lt;br/&gt;&lt;br/&gt;            return Ok(Some(&lt;br/&gt;                self.state.browser_signer_proxy.clone().into_nostr_signer(),&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        if let Some(sk) = &amp;amp;self.secret_key {&lt;br/&gt;            return Ok(Some(Keys::new(sk.clone()).into_nostr_signer()));&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        if let Some(ref bunker_url) = self.bunker_url {&lt;br/&gt;            let mut nostrconnect = NostrConnect::new(&lt;br/&gt;                bunker_url.clone(),&lt;br/&gt;                Cli::n34_keypair()?,&lt;br/&gt;                BUNKER_TIMEOUT,&lt;br/&gt;                None,&lt;br/&gt;            )&lt;br/&gt;            .expect(&amp;#34;It&amp;#39;s a bunker url and not a client&amp;#34;);&lt;br/&gt;&lt;br/&gt;            nostrconnect.auth_url_handler(EchoAuthUrl);&lt;br/&gt;            return Ok(Some(nostrconnect.into_nostr_signer()));&lt;br/&gt;        }&lt;br/&gt;        Ok(None)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns an error if there are no relays.&lt;br/&gt;    pub fn ensure_relays(&amp;amp;self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        if self.relays.is_empty() {&lt;br/&gt;            return Err(N34Error::EmptyRelays);&lt;br/&gt;        }&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns an error if there are no signers&lt;br/&gt;    pub fn ensure_signer(&amp;amp;self) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        if !self.config.keyring_secret_key &amp;amp;&amp;amp; self.secret_key.is_none() &amp;amp;&amp;amp; self.bunker_url.is_none()&lt;br/&gt;        {&lt;br/&gt;            return Err(N34Error::SignerRequired);&lt;br/&gt;        }&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl fmt::Debug for CliOptions {&lt;br/&gt;    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter&amp;lt;&amp;#39;_&amp;gt;) -&amp;gt; fmt::Result {&lt;br/&gt;        f.debug_struct(&amp;#34;CliOptions&amp;#34;)&lt;br/&gt;            .field(&amp;#34;secret_key&amp;#34;, &amp;amp;self.secret_key.as_ref().map(|_| &amp;#34;*******&amp;#34;))&lt;br/&gt;            .field(&amp;#34;bunker_url&amp;#34;, &amp;amp;self.bunker_url.as_ref().map(|_| &amp;#34;*******&amp;#34;))&lt;br/&gt;            .field(&amp;#34;relays&amp;#34;, &amp;amp;self.relays)&lt;br/&gt;            .field(&amp;#34;pow&amp;#34;, &amp;amp;self.pow)&lt;br/&gt;            .field(&amp;#34;config&amp;#34;, &amp;amp;self.config)&lt;br/&gt;            .finish()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for Commands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        tracing::trace!(&amp;#34;Options: {options:#?}&amp;#34;);&lt;br/&gt;        tracing::trace!(&amp;#34;Handling: {self:#?}&amp;#34;);&lt;br/&gt;&lt;br/&gt;        crate::run_command!(self, options, Repo Issue Sets Patch Config &amp;amp; Reply)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:16Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsg75dz0nnd9q64wfck0s9d4eacfl6e2sgtjqzgzllz748j2jlcdfqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvx7203d</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsg75dz0nnd9q64wfck0s9d4eacfl6e2sgtjqzgzllz748j2jlcdfqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvx7203d" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::fmt;&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::nips::nip19::ToBech32;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        CommandRunner,&lt;br/&gt;        traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::NaddrOrSet,&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;    nostr_utils::{NostrClient, traits::NaddrsUtils, utils},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Arguments for the `repo view` command&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct ViewArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;)]&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ViewArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(&lt;br/&gt;            self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;            &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;        )?)?;&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;        let repos = client.fetch_repos(&amp;amp;naddrs.into_coordinates()).await?;&lt;br/&gt;        let mut repos_details: Vec&amp;lt;String&amp;gt; = Vec::new();&lt;br/&gt;&lt;br/&gt;        for repo in repos {&lt;br/&gt;            let mut repo_details = format!(&amp;#34;ID: {}&amp;#34;, repo.id);&lt;br/&gt;&lt;br/&gt;            if let Some(name) = repo.name {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nName: {name}&amp;#34;));&lt;br/&gt;            }&lt;br/&gt;            if let Some(desc) = repo.description {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nDescription: {desc}&amp;#34;));&lt;br/&gt;            }&lt;br/&gt;            if !repo.web.is_empty() {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nWebpages:\n{}&amp;#34;, format_list(repo.web)));&lt;br/&gt;            }&lt;br/&gt;            if !repo.clone.is_empty() {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nClone urls:\n{}&amp;#34;, format_list(repo.clone)));&lt;br/&gt;            }&lt;br/&gt;            if !repo.relays.is_empty() {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nRelays:\n{}&amp;#34;, format_list(repo.relays)));&lt;br/&gt;            }&lt;br/&gt;            if let Some(euc) = repo.euc {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&amp;#34;\nEarliest unique commit: {euc}&amp;#34;));&lt;br/&gt;            }&lt;br/&gt;            if !repo.maintainers.is_empty() {&lt;br/&gt;                repo_details.push_str(&amp;amp;format!(&lt;br/&gt;                    &amp;#34;\nMaintainers:\n{}&amp;#34;,&lt;br/&gt;                    format_list(&lt;br/&gt;                        repo.maintainers&lt;br/&gt;                            .iter()&lt;br/&gt;                            .map(|p| p.to_bech32().expect(&amp;#34;Infallible&amp;#34;))&lt;br/&gt;                    )&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;            repos_details.push(repo_details);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;{}&amp;#34;, repos_details.join(&amp;#34;\n----------\n&amp;#34;));&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Format a vector to print it&lt;br/&gt;fn format_list&amp;lt;I, T&amp;gt;(iterator: I) -&amp;gt; String&lt;br/&gt;where&lt;br/&gt;    I: IntoIterator&amp;lt;Item = T&amp;gt;,&lt;br/&gt;    T: fmt::Display,&lt;br/&gt;{&lt;br/&gt;    iterator&lt;br/&gt;        .into_iter()&lt;br/&gt;        .map(|t| format!(&amp;#34; - {t}&amp;#34;))&lt;br/&gt;        .collect::&amp;lt;Vec&amp;lt;String&amp;gt;&amp;gt;()&lt;br/&gt;        .join(&amp;#34;\n&amp;#34;)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:05Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsz2dpqaj72mvpmml0e8fkwx7lzpqpc5vqdh50lhj7n3eaacll6qaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8gcleg</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsz2dpqaj72mvpmml0e8fkwx7lzpqpc5vqdh50lhj7n3eaacll6qaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8gcleg" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::{event::Kind, filter::Filter};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;    nostr_utils::{&lt;br/&gt;        NostrClient,&lt;br/&gt;        traits::{GitIssueUtils, NaddrsUtils, ReposUtils},&lt;br/&gt;        utils,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ViewArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The issue id to view it&lt;br/&gt;    issue_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ViewArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = utils::naddrs_or_file(&lt;br/&gt;            self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;            &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;        )?;&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;&lt;br/&gt;        client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;        client.add_relays(&amp;amp;self.issue_id.relays).await;&lt;br/&gt;        client&lt;br/&gt;            .add_relays(&lt;br/&gt;                &amp;amp;client&lt;br/&gt;                    .fetch_repos(&amp;amp;naddrs.into_coordinates())&lt;br/&gt;                    .await?&lt;br/&gt;                    .extract_relays(),&lt;br/&gt;            )&lt;br/&gt;            .await;&lt;br/&gt;&lt;br/&gt;        let issue = client&lt;br/&gt;            .fetch_event(&lt;br/&gt;                Filter::new()&lt;br/&gt;                    .id(self.issue_id.event_id)&lt;br/&gt;                    .kind(Kind::GitIssue),&lt;br/&gt;            )&lt;br/&gt;            .await?&lt;br/&gt;            .ok_or(N34Error::CanNotFoundIssue)?;&lt;br/&gt;&lt;br/&gt;        let issue_subject = utils::smart_wrap(issue.extract_issue_subject(), 70);&lt;br/&gt;        let issue_author = client.get_username(issue.pubkey).await;&lt;br/&gt;        let mut issue_labels = utils::smart_wrap(&amp;amp;issue.extract_issue_labels(), 70);&lt;br/&gt;&lt;br/&gt;        if issue_labels.is_empty() {&lt;br/&gt;            issue_labels = &amp;#34;\n&amp;#34;.to_owned();&lt;br/&gt;        } else {&lt;br/&gt;            issue_labels = format!(&amp;#34;{issue_labels}\n\n&amp;#34;)&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{issue_subject} - [by {issue_author}]\n{issue_labels}{}&amp;#34;,&lt;br/&gt;            utils::smart_wrap(&amp;amp;issue.content, 80)&lt;br/&gt;        );&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:45:05Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsdx95nsqjhr70qhzzksa9725aqkl5fk3kxlwwn9gjn405dtqxu3sczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrefna7</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsdx95nsqjhr70qhzzksa9725aqkl5fk3kxlwwn9gjn405dtqxu3sczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrefna7" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use super::IssueStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::CommandRunner,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ResolveArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The issue id to resolve it&lt;br/&gt;    issue_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ResolveArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::issue_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.issue_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            IssueStatus::Resolved,&lt;br/&gt;            |issue_status| {&lt;br/&gt;                if issue_status.is_resolved() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t resolve an resolved issue&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:44:54Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs2uz85kglcpttutn7qmkuec7h5fzd4g25ww6h32jwkj5snl8j9xaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3xytyz</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs2uz85kglcpttutn7qmkuec7h5fzd4g25ww6h32jwkj5snl8j9xaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3xytyz" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `repo announce` subcommand&lt;br/&gt;mod announce;&lt;br/&gt;/// `repo view` subcommand&lt;br/&gt;mod view;&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;&lt;br/&gt;use self::announce::AnnounceArgs;&lt;br/&gt;use self::view::ViewArgs;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::error::N34Result;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum RepoSubcommands {&lt;br/&gt;    /// View details of a nostr git repository&lt;br/&gt;    View(ViewArgs),&lt;br/&gt;    /// Broadcast and update a git repository&lt;br/&gt;    Announce(AnnounceArgs),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for RepoSubcommands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::run_command!(self, options, &amp;amp; View Announce)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:44:54Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsgjhnn3mlzudg42qys7ghlpejppxtppvgq0eea85a8ufj7wvaj25qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvswea26</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsgjhnn3mlzudg42qys7ghlpejppxtppvgq0eea85a8ufj7wvaj25qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvswea26" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use super::IssueStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::CommandRunner,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ReopenArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The closed issue id to reopen it&lt;br/&gt;    issue_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ReopenArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::issue_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.issue_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            IssueStatus::Open,&lt;br/&gt;            |issue_status| {&lt;br/&gt;                if issue_status.is_open() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t reopen an open issue&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if issue_status.is_resolved() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t open a resolved issue&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:44:43Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsgs4lky3yq6w5877ufdjgatzelehlwqznfyza0lm8xfkpknnqkrzgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvg7etpm</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsgs4lky3yq6w5877ufdjgatzelehlwqznfyza0lm8xfkpknnqkrzgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvg7etpm" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{fs, io::Write};&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{event::EventBuilder, key::PublicKey, types::Url};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, traits::RelayOrSetVecExt},&lt;br/&gt;    error::N34Result,&lt;br/&gt;    nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Header written to new `nostr-address` files. Contains two trailing newline&lt;br/&gt;/// for formatting.&lt;br/&gt;const NOSTR_ADDRESS_FILE_HEADER: &amp;amp;str = r##&amp;#34;# This file contains NIP-19 `naddr` entities for repositories that accept this&lt;br/&gt;# project&amp;#39;s issues and patches.&lt;br/&gt;#&lt;br/&gt;# The file acts as a **read-only reference** for retrieving repository relays&lt;br/&gt;# when embedded in an `naddr` and mentions those repositories when opening&lt;br/&gt;# patches or issues. Modifications here will not affect in the relays, as the&lt;br/&gt;# file is **explicitly untracked**. Its goal is to simplify contributions by&lt;br/&gt;# removing the need for manual address entry.&lt;br/&gt;#&lt;br/&gt;# Each entry must start with &amp;#34;naddr&amp;#34;. Embedded relays are **strongly recommended**&lt;br/&gt;# to assist client-side discovery.&lt;br/&gt;#&lt;br/&gt;# Empty lines are ignored. Lines starting with &amp;#34;#&amp;#34; are treated as comments.&lt;br/&gt;&lt;br/&gt;&amp;#34;##;&lt;br/&gt;&lt;br/&gt;/// Arguments for the `repo announce` command&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct AnnounceArgs {&lt;br/&gt;    /// Unique identifier for the repository in kebab-case.&lt;br/&gt;    #[arg(long = &amp;#34;id&amp;#34;)]&lt;br/&gt;    repo_id:      String,&lt;br/&gt;    /// A name for the repository.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    name:         Option&amp;lt;String&amp;gt;,&lt;br/&gt;    /// A description for the repository.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    description:  Option&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Webpage URLs for the repository (if provided by the git server).&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    web:          Vec&amp;lt;Url&amp;gt;,&lt;br/&gt;    /// URLs for cloning the repository.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    clone:        Vec&amp;lt;Url&amp;gt;,&lt;br/&gt;    /// Additional maintainers of the repository (besides yourself).&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    maintainers:  Vec&amp;lt;PublicKey&amp;gt;,&lt;br/&gt;    /// Labels to categorize the repository. Can be specified multiple times.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    label:        Vec&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Skip kebab-case validation for the repository ID&lt;br/&gt;    #[arg(long)]&lt;br/&gt;    force_id:     bool,&lt;br/&gt;    /// If set, creates a `nostr-address` file to enable automatic address&lt;br/&gt;    /// discovery by n34&lt;br/&gt;    #[arg(long)]&lt;br/&gt;    address_file: bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for AnnounceArgs {&lt;br/&gt;    const NEED_RELAYS: bool = true;&lt;br/&gt;&lt;br/&gt;    async fn run(mut self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        let user_pubk = client.pubkey().await?;&lt;br/&gt;        let relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;        client&lt;br/&gt;            .add_relays(&amp;amp;utils::add_read_relays(relays_list.as_ref()))&lt;br/&gt;            .await;&lt;br/&gt;&lt;br/&gt;        if !self.maintainers.contains(&amp;amp;user_pubk) {&lt;br/&gt;            self.maintainers.insert(0, user_pubk);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let naddr = utils::repo_naddr(&amp;amp;self.repo_id, user_pubk, &amp;amp;relays)?;&lt;br/&gt;        let event = EventBuilder::new_git_repo(&lt;br/&gt;            self.repo_id,&lt;br/&gt;            self.name.map(utils::str_trim),&lt;br/&gt;            self.description.map(utils::str_trim),&lt;br/&gt;            self.web,&lt;br/&gt;            self.clone,&lt;br/&gt;            relays.clone(),&lt;br/&gt;            self.maintainers.clone(),&lt;br/&gt;            self.label.into_iter().map(utils::str_trim).collect(),&lt;br/&gt;            self.force_id,&lt;br/&gt;        )?&lt;br/&gt;        .dedup_tags()&lt;br/&gt;        .pow(options.pow.unwrap_or_default())&lt;br/&gt;        .build(user_pubk);&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;        if self.address_file {&lt;br/&gt;            let address_path = std::env::current_dir()?.join(NOSTR_ADDRESS_FILE);&lt;br/&gt;            if !address_path.exists() {&lt;br/&gt;                tracing::info!(&lt;br/&gt;                    &amp;#34;Creating new address file: &amp;#39;{NOSTR_ADDRESS_FILE}&amp;#39; at path &amp;#39;{}&amp;#39; with default \&lt;br/&gt;                     header&amp;#34;,&lt;br/&gt;                    address_path.display()&lt;br/&gt;                );&lt;br/&gt;                fs::write(&amp;amp;address_path, NOSTR_ADDRESS_FILE_HEADER)?;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let mut file = fs::OpenOptions::new().append(true).open(&amp;amp;address_path)?;&lt;br/&gt;&lt;br/&gt;            tracing::info!(&amp;#34;Appending naddr &amp;#39;{naddr}&amp;#39; to address file: &amp;#39;{NOSTR_ADDRESS_FILE}&amp;#39;&amp;#34;);&lt;br/&gt;            file.write_all(format!(&amp;#34;{naddr}\n&amp;#34;).as_bytes())?;&lt;br/&gt;            tracing::info!(&amp;#34;Successfully wrote naddr to address file&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let write_relays = [&lt;br/&gt;            relays,&lt;br/&gt;            utils::add_write_relays(relays_list.as_ref()),&lt;br/&gt;            // Include read relays for each maintainer (if found)&lt;br/&gt;            future::join_all(&lt;br/&gt;                self.maintainers&lt;br/&gt;                    .iter()&lt;br/&gt;                    .map(|pkey| client.read_relays_from_user(*pkey)),&lt;br/&gt;            )&lt;br/&gt;            .await&lt;br/&gt;            .into_iter()&lt;br/&gt;            .flatten()&lt;br/&gt;            .collect(),&lt;br/&gt;        ]&lt;br/&gt;        .concat();&lt;br/&gt;        let nevent = utils::new_nevent(event.id.expect(&amp;#34;There is an id&amp;#34;), &amp;amp;write_relays)?;&lt;br/&gt;&lt;br/&gt;        client&lt;br/&gt;            .send_event_to(event, relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;            .await?;&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;Event: {nevent}&amp;#34;,);&lt;br/&gt;        println!(&amp;#34;Repo Address: {naddr}&amp;#34;,);&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:44:43Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsv3aezdre8987c3g39e7cww7gfj7k74sunwgswxu6ghq8m4p0yw3szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv04szr0</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsv3aezdre8987c3g39e7cww7gfj7k74sunwgswxu6ghq8m4p0yw3szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv04szr0" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;use clap::{ArgGroup, Args};&lt;br/&gt;use nostr::event::{EventBuilder, Tag};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        CommandRunner,&lt;br/&gt;        traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::NaddrOrSet,&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;    nostr_utils::{&lt;br/&gt;        NostrClient,&lt;br/&gt;        traits::{NaddrsUtils, NewGitRepositoryAnnouncement, ReposUtils},&lt;br/&gt;        utils,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Arguments for the `issue new` command&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;#[clap(&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;issue-content&amp;#34;)&lt;br/&gt;            .args([&amp;#34;content&amp;#34;, &amp;#34;editor&amp;#34;])&lt;br/&gt;            .required(true)&lt;br/&gt;    ),&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;issue-subject&amp;#34;)&lt;br/&gt;            .args([&amp;#34;editor&amp;#34;, &amp;#34;subject&amp;#34;])&lt;br/&gt;    )&lt;br/&gt;)]&lt;br/&gt;pub struct NewArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:  Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// Markdown content for the issue. Cannot be used together with the&lt;br/&gt;    /// `--editor` flag.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    content: Option&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Opens the user&amp;#39;s default editor to write issue content. The first line&lt;br/&gt;    /// will be used as the issue subject.&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    editor:  bool,&lt;br/&gt;    /// The issue subject. Cannot be used together with the `--editor` flag.&lt;br/&gt;    #[arg(long)]&lt;br/&gt;    subject: Option&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Labels for the issue. Can be specified as arguments (-l bug) or hashtags&lt;br/&gt;    /// in content (#bug).&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    label:   Vec&amp;lt;String&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl NewArgs {&lt;br/&gt;    /// Returns the subject and the content of the issue. (subject, content)&lt;br/&gt;    pub fn issue_content(&amp;amp;self) -&amp;gt; N34Result&amp;lt;(Option&amp;lt;String&amp;gt;, String)&amp;gt; {&lt;br/&gt;        if let Some(content) = self.content.as_ref() {&lt;br/&gt;            if let Some(subject) = self.subject.as_ref() {&lt;br/&gt;                return Ok((Some(subject.trim().to_owned()), content.trim().to_owned()));&lt;br/&gt;            }&lt;br/&gt;            return Ok((None, content.trim().to_owned()));&lt;br/&gt;        }&lt;br/&gt;        // If the `self.content` is `None` then the `self.editor` is `true`&lt;br/&gt;        let file_content = utils::read_editor(None, &amp;#34;.md&amp;#34;)?;&lt;br/&gt;        if file_content.contains(&amp;#39;\n&amp;#39;) {&lt;br/&gt;            Ok(file_content&lt;br/&gt;                .split_once(&amp;#39;\n&amp;#39;)&lt;br/&gt;                .map(|(s, c)| (Some(s.trim().to_owned()), c.trim().to_owned()))&lt;br/&gt;                .expect(&amp;#34;There is a new line&amp;#34;))&lt;br/&gt;        } else {&lt;br/&gt;            tracing::info!(&amp;#34;File content contains only issue body without a subject line&amp;#34;);&lt;br/&gt;            Ok((None, file_content))&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for NewArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(&lt;br/&gt;            self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;            &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;        )?)?;&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        let user_pubk = client.pubkey().await?;&lt;br/&gt;        let coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;        client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;        let repos = client.fetch_repos(coordinates.as_slice()).await?;&lt;br/&gt;        let maintainers = repos.extract_maintainers();&lt;br/&gt;        client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;        let relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;        client&lt;br/&gt;            .add_relays(&amp;amp;utils::add_read_relays(relays_list.as_ref()))&lt;br/&gt;            .await;&lt;br/&gt;&lt;br/&gt;        let (subject, content) = self.issue_content()?;&lt;br/&gt;        let content_details = client.parse_content(&amp;amp;content).await;&lt;br/&gt;&lt;br/&gt;        let event =&lt;br/&gt;            EventBuilder::new_git_issue(coordinates.as_slice(), content, subject, self.label)?&lt;br/&gt;                .dedup_tags()&lt;br/&gt;                .pow(options.pow.unwrap_or_default())&lt;br/&gt;                .tags(maintainers.iter().map(|p| Tag::public_key(*p)))&lt;br/&gt;                .tags(content_details.clone().into_tags())&lt;br/&gt;                .build(user_pubk);&lt;br/&gt;        let event_id = event.id.expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;&lt;br/&gt;        let write_relays = [&lt;br/&gt;            relays,&lt;br/&gt;            naddrs.extract_relays(),&lt;br/&gt;            utils::add_write_relays(relays_list.as_ref()),&lt;br/&gt;            client&lt;br/&gt;                .fetch_repos(&amp;amp;naddrs.into_coordinates())&lt;br/&gt;                .await?&lt;br/&gt;                .extract_relays(),&lt;br/&gt;            // Include read relays for each maintainer (if found)&lt;br/&gt;            client.read_relays_from_users(&amp;amp;maintainers).await,&lt;br/&gt;            content_details.write_relays.clone().into_iter().collect(),&lt;br/&gt;        ]&lt;br/&gt;        .concat();&lt;br/&gt;&lt;br/&gt;        tracing::trace!(relays = ?write_relays, &amp;#34;Write relays list&amp;#34;);&lt;br/&gt;        let success = client&lt;br/&gt;            .send_event_to(event, relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;            .await?;&lt;br/&gt;&lt;br/&gt;        let nevent = utils::new_nevent(event_id, &amp;amp;success)?;&lt;br/&gt;        println!(&amp;#34;Issue created: {nevent}&amp;#34;);&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:44:23Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsr7petpakfjye2mcvpu89y5gwrfr0fuqup2rhzeqzaenfdl99yz0qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvn4ufzj</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsr7petpakfjye2mcvpu89y5gwrfr0fuqup2rhzeqzaenfdl99yz0qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvn4ufzj" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::fs;&lt;br/&gt;&lt;br/&gt;use clap::{ArgGroup, Args};&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventBuilder, Kind},&lt;br/&gt;    filter::Filter,&lt;br/&gt;    nips::nip01::Coordinate,&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;    nostr_utils::{&lt;br/&gt;        NostrClient,&lt;br/&gt;        traits::{NaddrsUtils, ReposUtils},&lt;br/&gt;        utils,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// The max date &amp;#34;9999-01-01 at 00:00 UTC&amp;#34;&lt;br/&gt;const MAX_DATE: i64 = 253370764800;&lt;br/&gt;&lt;br/&gt;/// Arguments for the `reply` command&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;#[clap(&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;comment-content&amp;#34;)&lt;br/&gt;            .args([&amp;#34;comment&amp;#34;, &amp;#34;editor&amp;#34;])&lt;br/&gt;            .required(true)&lt;br/&gt;    ),&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;quote-reply-to&amp;#34;)&lt;br/&gt;            .args([&amp;#34;comment&amp;#34;, &amp;#34;quote_to&amp;#34;])&lt;br/&gt;    )&lt;br/&gt;)]&lt;br/&gt;pub struct ReplyArgs {&lt;br/&gt;    /// The issue, patch, or comment to reply to&lt;br/&gt;    #[arg(value_name = &amp;#34;nevent1-or-note1&amp;#34;)]&lt;br/&gt;    to:       NostrEvent,&lt;br/&gt;    /// Quote the replied-to event in the editor&lt;br/&gt;    #[arg(long)]&lt;br/&gt;    quote_to: bool,&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The comment (cannot be used with --editor)&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    comment:  Option&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Open editor to write comment (cannot be used with --content)&lt;br/&gt;    #[arg(short, long)]&lt;br/&gt;    editor:   bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ReplyArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let nostr_address_path = utils::nostr_address_path()?;&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        let user_pubk = client.pubkey().await?;&lt;br/&gt;        let repo_naddrs = if let Some(naddrs) = self.naddrs.flat_naddrs(&amp;amp;options.config.sets)? {&lt;br/&gt;            client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;            Some(naddrs)&lt;br/&gt;        } else if fs::exists(&amp;amp;nostr_address_path).is_ok() {&lt;br/&gt;            let naddrs = utils::naddrs_or_file(None, &amp;amp;nostr_address_path)?;&lt;br/&gt;            client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;            Some(naddrs)&lt;br/&gt;        } else {&lt;br/&gt;            None&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        client.add_relays(&amp;amp;self.to.relays).await;&lt;br/&gt;        let relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;        let author_read_relays =&lt;br/&gt;            utils::add_read_relays(client.user_relays_list(user_pubk).await?.as_ref());&lt;br/&gt;        client.add_relays(&amp;amp;author_read_relays).await;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;        let reply_to = client&lt;br/&gt;            .fetch_event(Filter::new().id(self.to.event_id))&lt;br/&gt;            .await?&lt;br/&gt;            .ok_or(N34Error::EventNotFound)?;&lt;br/&gt;        let root = client.find_root(reply_to.clone()).await?;&lt;br/&gt;&lt;br/&gt;        let repos_coordinate = if let Some(naddrs) = repo_naddrs {&lt;br/&gt;            naddrs.into_coordinates()&lt;br/&gt;        } else if let Some(ref root_event) = root {&lt;br/&gt;            coordinates_from_root(root_event)?&lt;br/&gt;        } else {&lt;br/&gt;            return Err(N34Error::NotFoundRepo);&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let repos = client.fetch_repos(&amp;amp;repos_coordinate).await?;&lt;br/&gt;        let maintainers = repos.extract_maintainers();&lt;br/&gt;&lt;br/&gt;        let quoted_content = if self.quote_to {&lt;br/&gt;            Some(quote_reply_to_content(&amp;amp;client, &amp;amp;reply_to).await)&lt;br/&gt;        } else {&lt;br/&gt;            None&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let content = utils::get_content(self.comment.as_ref(), quoted_content.as_ref(), &amp;#34;.txt&amp;#34;)?;&lt;br/&gt;        let content_details = client.parse_content(&amp;amp;content).await;&lt;br/&gt;&lt;br/&gt;        let event = EventBuilder::comment(&lt;br/&gt;            content,&lt;br/&gt;            &amp;amp;reply_to,&lt;br/&gt;            root.as_ref(),&lt;br/&gt;            repos.first().and_then(|r| r.relays.first()).cloned(),&lt;br/&gt;        )&lt;br/&gt;        .dedup_tags()&lt;br/&gt;        .pow(options.pow.unwrap_or_default())&lt;br/&gt;        .tags(content_details.clone().into_tags())&lt;br/&gt;        .build(user_pubk);&lt;br/&gt;&lt;br/&gt;        let event_id = event.id.expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;        let write_relays = [&lt;br/&gt;            relays,&lt;br/&gt;            utils::add_write_relays(relays_list.as_ref()),&lt;br/&gt;            // Merge repository announcement relays into write relays&lt;br/&gt;            repos.extract_relays(),&lt;br/&gt;            // Include read relays for each repository maintainer (if found)&lt;br/&gt;            client.read_relays_from_users(&amp;amp;maintainers).await,&lt;br/&gt;            // read relays of the root event and the reply to event&lt;br/&gt;            {&lt;br/&gt;                let (r1, r2) = future::join(&lt;br/&gt;                    client.read_relays_from_user(reply_to.pubkey),&lt;br/&gt;                    event_author_read_relays(&amp;amp;client, root.as_ref()),&lt;br/&gt;                )&lt;br/&gt;                .await;&lt;br/&gt;                [r1, r2].concat()&lt;br/&gt;            },&lt;br/&gt;            content_details.write_relays.into_iter().collect(),&lt;br/&gt;        ]&lt;br/&gt;        .concat();&lt;br/&gt;&lt;br/&gt;        tracing::trace!(relays = ?write_relays, &amp;#34;Write relays list&amp;#34;);&lt;br/&gt;        let (success, ..) = futures::join!(&lt;br/&gt;            client.send_event_to(event, relays_list.as_ref(), &amp;amp;write_relays),&lt;br/&gt;            client.broadcast(&amp;amp;reply_to, &amp;amp;author_read_relays),&lt;br/&gt;            async {&lt;br/&gt;                if let Some(root_event) = root {&lt;br/&gt;                    let _ = client.broadcast(&amp;amp;root_event, &amp;amp;author_read_relays).await;&lt;br/&gt;                }&lt;br/&gt;            },&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;        let nevent = utils::new_nevent(event_id, &amp;amp;success?)?;&lt;br/&gt;        println!(&amp;#34;Comment created: {nevent}&amp;#34;);&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates a quoted reply string in the format &amp;#34;On yyyy-mm-dd at hh:mm UTC,&lt;br/&gt;/// {author} wrote:&amp;#34; followed by the event content. Uses display name if&lt;br/&gt;/// available, otherwise falls back to a shortened npub string. Dates are&lt;br/&gt;/// formatted in UTC.&lt;br/&gt;async fn quote_reply_to_content(client: &amp;amp;NostrClient, quoted_event: &amp;amp;Event) -&amp;gt; String {&lt;br/&gt;    let author_name = client.get_username(quoted_event.pubkey).await;&lt;br/&gt;&lt;br/&gt;    let fdate = chrono::DateTime::from_timestamp(&lt;br/&gt;        quoted_event&lt;br/&gt;            .created_at&lt;br/&gt;            .as_u64()&lt;br/&gt;            .try_into()&lt;br/&gt;            .unwrap_or(MAX_DATE),&lt;br/&gt;        0,&lt;br/&gt;    )&lt;br/&gt;    .map(|datetime| datetime.format(&amp;#34;On %F at %R UTC, &amp;#34;).to_string())&lt;br/&gt;    .unwrap_or_default();&lt;br/&gt;&lt;br/&gt;    format!(&lt;br/&gt;        &amp;#34;{fdate}{author_name} wrote:\n&amp;gt; {}&amp;#34;,&lt;br/&gt;        quoted_event.content.trim().replace(&amp;#34;\n&amp;#34;, &amp;#34;\n&amp;gt; &amp;#34;)&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Gets the repository coordinate from a root Nostr event&amp;#39;s tags.&lt;br/&gt;/// The event must contain a coordinate tag with GitRepoAnnouncement kind.&lt;br/&gt;fn coordinates_from_root(root: &amp;amp;Event) -&amp;gt; N34Result&amp;lt;Vec&amp;lt;Coordinate&amp;gt;&amp;gt; {&lt;br/&gt;    let coordinates: Vec&amp;lt;Coordinate&amp;gt; = root&lt;br/&gt;        .tags&lt;br/&gt;        .coordinates()&lt;br/&gt;        .filter(|c| c.kind == Kind::GitRepoAnnouncement)&lt;br/&gt;        .cloned()&lt;br/&gt;        .collect();&lt;br/&gt;&lt;br/&gt;    if coordinates.is_empty() {&lt;br/&gt;        return Err(N34Error::InvalidEvent(&lt;br/&gt;            &amp;#34;The Git issue/patch does not specify a target repository&amp;#34;.to_owned(),&lt;br/&gt;        ));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(coordinates)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Returns the event author read relays if found, otherwise an empty vector&lt;br/&gt;async fn event_author_read_relays(client: &amp;amp;NostrClient, event: Option&amp;lt;&amp;amp;Event&amp;gt;) -&amp;gt; Vec&amp;lt;RelayUrl&amp;gt; {&lt;br/&gt;    if let Some(root_event) = event {&lt;br/&gt;        client.read_relays_from_user(root_event.pubkey).await&lt;br/&gt;    } else {&lt;br/&gt;        Vec::new()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:44:23Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs2j780a5k2xcef0kw9mlaket0fnlk9fzhu46ggtm8jf3z96cxqq0czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8exgky</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs2j780a5k2xcef0kw9mlaket0fnlk9fzhu46ggtm8jf3z96cxqq0czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8exgky" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `issue close` subcommand&lt;br/&gt;mod close;&lt;br/&gt;/// `issue list` subcommand&lt;br/&gt;mod list;&lt;br/&gt;/// `issue new` subcommand&lt;br/&gt;mod new;&lt;br/&gt;/// `issue reopen` subcommand&lt;br/&gt;mod reopen;&lt;br/&gt;/// `issue resolve` subcommand&lt;br/&gt;mod resolve;&lt;br/&gt;/// `issue view` subcommand&lt;br/&gt;mod view;&lt;br/&gt;&lt;br/&gt;use std::fmt;&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;use nostr::event::Kind;&lt;br/&gt;&lt;br/&gt;use self::close::CloseArgs;&lt;br/&gt;use self::list::ListArgs;&lt;br/&gt;use self::new::NewArgs;&lt;br/&gt;use self::reopen::ReopenArgs;&lt;br/&gt;use self::resolve::ResolveArgs;&lt;br/&gt;use self::view::ViewArgs;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::error::{N34Error, N34Result};&lt;br/&gt;&lt;br/&gt;/// Prefix used for git issue alt.&lt;br/&gt;pub const ISSUE_ALT_PREFIX: &amp;amp;str = &amp;#34;git issue: &amp;#34;;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum IssueSubcommands {&lt;br/&gt;    /// Create a new repository issue&lt;br/&gt;    New(NewArgs),&lt;br/&gt;    /// View an issue by its ID&lt;br/&gt;    View(ViewArgs),&lt;br/&gt;    /// Reopens a closed issue.&lt;br/&gt;    Reopen(ReopenArgs),&lt;br/&gt;    /// Closes an open issue.&lt;br/&gt;    Close(CloseArgs),&lt;br/&gt;    /// Resolves an issue.&lt;br/&gt;    Resolve(ResolveArgs),&lt;br/&gt;    /// List the repositories issues.&lt;br/&gt;    List(ListArgs),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Possible states for a Git issue&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub enum IssueStatus {&lt;br/&gt;    /// The issue is currently open&lt;br/&gt;    Open,&lt;br/&gt;    /// The issue has been resolved&lt;br/&gt;    Resolved,&lt;br/&gt;    /// The issue has been closed&lt;br/&gt;    Closed,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl IssueStatus {&lt;br/&gt;    /// Maps the issue status to its corresponding Nostr kind.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn kind(&amp;amp;self) -&amp;gt; Kind {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Open =&amp;gt; Kind::GitStatusOpen,&lt;br/&gt;            Self::Resolved =&amp;gt; Kind::GitStatusApplied,&lt;br/&gt;            Self::Closed =&amp;gt; Kind::GitStatusClosed,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the string representation of the issue status.&lt;br/&gt;    pub const fn as_str(&amp;amp;self) -&amp;gt; &amp;amp;&amp;#39;static str {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Open =&amp;gt; &amp;#34;Open&amp;#34;,&lt;br/&gt;            Self::Resolved =&amp;gt; &amp;#34;Resolved&amp;#34;,&lt;br/&gt;            Self::Closed =&amp;gt; &amp;#34;Closed&amp;#34;,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the issue is open.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_open(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Open)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the issue is resolved.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_resolved(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Resolved)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the issue is closed.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_closed(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Closed)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl From&amp;lt;&amp;amp;IssueStatus&amp;gt; for Kind {&lt;br/&gt;    fn from(status: &amp;amp;IssueStatus) -&amp;gt; Self {&lt;br/&gt;        status.kind()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl fmt::Display for IssueStatus {&lt;br/&gt;    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter&amp;lt;&amp;#39;_&amp;gt;) -&amp;gt; fmt::Result {&lt;br/&gt;        write!(f, &amp;#34;{}&amp;#34;, self.as_str())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl TryFrom&amp;lt;Kind&amp;gt; for IssueStatus {&lt;br/&gt;    type Error = N34Error;&lt;br/&gt;&lt;br/&gt;    fn try_from(kind: Kind) -&amp;gt; Result&amp;lt;Self, Self::Error&amp;gt; {&lt;br/&gt;        match kind {&lt;br/&gt;            Kind::GitStatusOpen =&amp;gt; Ok(Self::Open),&lt;br/&gt;            Kind::GitStatusApplied =&amp;gt; Ok(Self::Resolved),&lt;br/&gt;            Kind::GitStatusClosed =&amp;gt; Ok(Self::Closed),&lt;br/&gt;            _ =&amp;gt; Err(N34Error::InvalidIssueStatus(kind)),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for IssueSubcommands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::run_command!(self, options, &amp;amp; New View Reopen Close Resolve List)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:44:05Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsfvrujmpuc6tfgy5dcn8mtmtr7f6lanuqnjqzhze0ze2hmvc3m3qczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvt7kmyv</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsfvrujmpuc6tfgy5dcn8mtmtr7f6lanuqnjqzhze0ze2hmvc3m3qczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvt7kmyv" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use super::*;&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_normal() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: a to abc&lt;br/&gt;&lt;br/&gt;Abc patch&lt;br/&gt;---&lt;br/&gt; src/nostr_utils/mod.rs            |  1 &#43;&lt;br/&gt; 1files changed, 3 insertions(&#43;), 1 deletions(-)&lt;br/&gt;&lt;br/&gt;diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs&lt;br/&gt;index 4120f5a..e68783c 100644&lt;br/&gt;--- a/src/nostr_utils/mod.rs&lt;br/&gt;&#43;&#43;&#43; b/src/nostr_utils/mod.rs&lt;br/&gt;@@ -103,31 &#43;103,9 @@ impl CommandRunner for NewArgs {&lt;br/&gt;&lt;br/&gt;- a&lt;br/&gt;&#43; abc&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(patch.subject, &amp;#34;[PATCH] chore: a to abc&amp;#34;);&lt;br/&gt;    assert_eq!(patch.body, &amp;#34;Abc patch&amp;#34;);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_normal_with_patch_in_content() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: Subject in subject&lt;br/&gt;&lt;br/&gt;A good test patch&lt;br/&gt;---&lt;br/&gt; src/nostr_utils/mod.rs            |  1 &#43;&lt;br/&gt; 1files changed, 3 insertions(&#43;), 1 deletions(-)&lt;br/&gt;&lt;br/&gt;diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs&lt;br/&gt;index 4120f5a..e68783c 100644&lt;br/&gt;--- a/src/nostr_utils/mod.rs&lt;br/&gt;&#43;&#43;&#43; b/src/nostr_utils/mod.rs&lt;br/&gt;@@ -103,31 &#43;103,9 @@ impl CommandRunner for NewArgs {&lt;br/&gt;&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: What a subject&lt;br/&gt;&lt;br/&gt;hi&lt;br/&gt;---&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(patch.subject, &amp;#34;[PATCH] chore: Subject in subject&amp;#34;);&lt;br/&gt;    assert_eq!(patch.body, &amp;#34;A good test patch&amp;#34;);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_multiline_subject() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: Some long subject yes so long one Some long subject yes&lt;br/&gt; so long one&lt;br/&gt;&lt;br/&gt;Abc patch&lt;br/&gt;---&lt;br/&gt; src/nostr_utils/mod.rs            |  1 &#43;&lt;br/&gt; 1files changed, 3 insertions(&#43;), 1 deletions(-)&lt;br/&gt;&lt;br/&gt;diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs&lt;br/&gt;index 4120f5a..e68783c 100644&lt;br/&gt;--- a/src/nostr_utils/mod.rs&lt;br/&gt;&#43;&#43;&#43; b/src/nostr_utils/mod.rs&lt;br/&gt;@@ -103,31 &#43;103,9 @@ impl CommandRunner for NewArgs {&lt;br/&gt;&lt;br/&gt;- a&lt;br/&gt;&#43; abc&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.subject,&lt;br/&gt;        &amp;#34;[PATCH] chore: Some long subject yes so long one Some long subject yes so long one&amp;#34;&lt;br/&gt;    );&lt;br/&gt;    assert_eq!(patch.body, &amp;#34;Abc patch&amp;#34;);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_multiline_body() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 27 May 2025 19:20:42 &#43;0000&lt;br/&gt;Subject: [PATCH] chore: a to abc&lt;br/&gt;&lt;br/&gt;Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus&lt;br/&gt;aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.&lt;br/&gt;Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione&lt;br/&gt;atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci&lt;br/&gt;aspernatur.&lt;br/&gt;---&lt;br/&gt; src/nostr_utils/mod.rs            |  1 &#43;&lt;br/&gt; 1files changed, 3 insertions(&#43;), 1 deletions(-)&lt;br/&gt;&lt;br/&gt;diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs&lt;br/&gt;index 4120f5a..e68783c 100644&lt;br/&gt;--- a/src/nostr_utils/mod.rs&lt;br/&gt;&#43;&#43;&#43; b/src/nostr_utils/mod.rs&lt;br/&gt;@@ -103,31 &#43;103,9 @@ impl CommandRunner for NewArgs {&lt;br/&gt;&lt;br/&gt;- a&lt;br/&gt;&#43; abc&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(patch.subject, &amp;#34;[PATCH] chore: a to abc&amp;#34;);&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.body,&lt;br/&gt;        &amp;#34;Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus&lt;br/&gt;aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.&lt;br/&gt;Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione&lt;br/&gt;atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci&lt;br/&gt;aspernatur.&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_cover_letter() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 3 Jun 2025 08:41:12 &#43;0000&lt;br/&gt;Subject: [PATCH v2 0/7] feat: Some test just a test&lt;br/&gt;&lt;br/&gt;Cover body&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(patch.subject, &amp;#34;[PATCH v2 0/7] feat: Some test just a test&amp;#34;);&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.body,&lt;br/&gt;        &amp;#34;Cover body&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_multiline_cover_subject() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 3 Jun 2025 08:41:12 &#43;0000&lt;br/&gt;Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some&lt;br/&gt; test just a test&lt;br/&gt;&lt;br/&gt;Cover body&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.subject,&lt;br/&gt;        &amp;#34;[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a test&amp;#34;&lt;br/&gt;    );&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.body,&lt;br/&gt;        &amp;#34;Cover body&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_multiline_cover_body() {&lt;br/&gt;    let patch_content = r#&amp;#34;From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001&lt;br/&gt;From: Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;Date: Tue, 3 Jun 2025 08:41:12 &#43;0000&lt;br/&gt;Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some&lt;br/&gt; test just a test&lt;br/&gt;&lt;br/&gt;Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus&lt;br/&gt;aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.&lt;br/&gt;Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione&lt;br/&gt;atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci&lt;br/&gt;aspernatur.&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&lt;br/&gt;-- &lt;br/&gt;2.49.0&amp;#34;#;&lt;br/&gt;    let patch = GitPatch::from_str(patch_content).unwrap();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.subject,&lt;br/&gt;        &amp;#34;[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a test&amp;#34;&lt;br/&gt;    );&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.body,&lt;br/&gt;        &amp;#34;Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus&lt;br/&gt;aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.&lt;br/&gt;Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione&lt;br/&gt;atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci&lt;br/&gt;aspernatur.&lt;br/&gt;&lt;br/&gt;Awiteb (1):&lt;br/&gt;  chore: Update `README.md`&lt;br/&gt;&lt;br/&gt; README.md      |  2 &#43;-&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;base-commit: f670859b92d525874fd621452080c8479964ac6a&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn normal_patch_filename() {&lt;br/&gt;    let mut patch = GitPatch {&lt;br/&gt;        inner:   String::new(),&lt;br/&gt;        subject: String::new(),&lt;br/&gt;        body:    String::new(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v2 0/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v2-0000-cover-letter.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH 0/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;0000-cover-letter.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v2 1/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v2-0001-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v42 1/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v42-0001-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v42 23/30] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v42-0023-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH 1/3] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;0001-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH 32/50] feat: Some test just a test&amp;#34;.to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;0032-feat-some-test-just-a-test.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v100 32/50] feat: some long subject some long subject some long \&lt;br/&gt;                     subject some long subject&amp;#34;&lt;br/&gt;        .to_owned();&lt;br/&gt;    assert_eq!(&lt;br/&gt;        patch.filename(&amp;#34;&amp;#34;).unwrap(),&lt;br/&gt;        PathBuf::from(&amp;#34;v100-0032-feat-some-long-subject-some-long-subject-some-long-subject.patch&amp;#34;)&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_filename_without_patch() {&lt;br/&gt;    let mut patch = GitPatch {&lt;br/&gt;        inner:   String::new(),&lt;br/&gt;        subject: &amp;#34;[RFC v5 1/2] Something&amp;#34;.to_owned(),&lt;br/&gt;        body:    String::new(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_err());&lt;br/&gt;    patch.subject = &amp;#34;Something&amp;#34;.to_owned();&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_err());&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_filename_without_number() {&lt;br/&gt;    let mut patch = GitPatch {&lt;br/&gt;        inner:   String::new(),&lt;br/&gt;        subject: &amp;#34;[PATCH v5 /2] Something&amp;#34;.to_owned(),&lt;br/&gt;        body:    String::new(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_err());&lt;br/&gt;    patch.subject = &amp;#34;[PATCH v5 2/] Something&amp;#34;.to_owned();&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_err());&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[test]&lt;br/&gt;fn patch_filename_without_version() {&lt;br/&gt;    let patch = GitPatch {&lt;br/&gt;        inner:   String::new(),&lt;br/&gt;        subject: &amp;#34;[PATCH 1/2] Something&amp;#34;.to_owned(),&lt;br/&gt;        body:    String::new(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    assert!(patch.filename(&amp;#34;&amp;#34;).is_ok());&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:44:05Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs95ls367yxvkey7dfyhkvdnh0wptsw69glxkus0hyc0qpxp7f4j9czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvap694f</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs95ls367yxvkey7dfyhkvdnh0wptsw69glxkus0hyc0qpxp7f4j9czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvap694f" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{fs, str::FromStr};&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use futures::future;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{EventBuilder, EventId, Kind, Tag, TagKind, Tags, UnsignedEvent},&lt;br/&gt;    hashes::sha1::Hash as Sha1Hash,&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    nips::{nip01::Coordinate, nip10::Marker},&lt;br/&gt;    types::RelayUrl,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use super::GitPatch;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        patch::{REVISION_ROOT_HASHTAG_CONTENT, ROOT_HASHTAG_CONTENT},&lt;br/&gt;        traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::N34Result,&lt;br/&gt;    nostr_utils::{&lt;br/&gt;        NostrClient,&lt;br/&gt;        traits::{NaddrsUtils, ReposUtils},&lt;br/&gt;        utils,&lt;br/&gt;    },&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Prefix used for git patch alt.&lt;br/&gt;const PATCH_ALT_PREFIX: &amp;amp;str = &amp;#34;git patch: &amp;#34;;&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct SendArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:         Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// List of patch files to send (space separated).&lt;br/&gt;    ///&lt;br/&gt;    /// For p-tagging users, include them in the cover letter with&lt;br/&gt;    /// `nostr:npub1...`.&lt;br/&gt;    #[arg(value_name = &amp;#34;PATCH-PATH&amp;#34;, required = true, value_parser = parse_patch_path)]&lt;br/&gt;    patches:        Vec&amp;lt;GitPatch&amp;gt;,&lt;br/&gt;    /// Original patch ID if this is a revision of it&lt;br/&gt;    #[arg(long, value_name = &amp;#34;EVENT-ID&amp;#34;)]&lt;br/&gt;    original_patch: Option&amp;lt;NostrEvent&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for SendArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(&lt;br/&gt;            self.naddrs.flat_naddrs(&amp;amp;options.config.sets)?,&lt;br/&gt;            &amp;amp;utils::nostr_address_path()?,&lt;br/&gt;        )?)?;&lt;br/&gt;&lt;br/&gt;        let repo_coordinates = naddrs.clone().into_coordinates();&lt;br/&gt;        let relays = options.relays.clone().flat_relays(&amp;amp;options.config.sets)?;&lt;br/&gt;        let client = NostrClient::init(&amp;amp;options, &amp;amp;relays).await;&lt;br/&gt;        let user_pubk = client.pubkey().await?;&lt;br/&gt;&lt;br/&gt;        client.add_relays(&amp;amp;naddrs.extract_relays()).await;&lt;br/&gt;        if let Some(original_patch) = &amp;amp;self.original_patch {&lt;br/&gt;            client.add_relays(&amp;amp;original_patch.relays).await;&lt;br/&gt;        }&lt;br/&gt;        let relays_list = client.user_relays_list(user_pubk).await?;&lt;br/&gt;        client&lt;br/&gt;            .add_relays(&amp;amp;utils::add_read_relays(relays_list.as_ref()))&lt;br/&gt;            .await;&lt;br/&gt;        let repos = client.fetch_repos(&amp;amp;repo_coordinates).await?;&lt;br/&gt;        let euc = repos.extract_euc();&lt;br/&gt;        let maintainers = repos.extract_maintainers();&lt;br/&gt;        client.add_relays(&amp;amp;repos.extract_relays()).await;&lt;br/&gt;&lt;br/&gt;        let (events, events_write_relays) = make_patch_series(&lt;br/&gt;            &amp;amp;client,&lt;br/&gt;            self.patches,&lt;br/&gt;            self.original_patch.as_ref().map(|e| e.event_id),&lt;br/&gt;            repos.extract_relays().first().cloned(),&lt;br/&gt;            repo_coordinates,&lt;br/&gt;            euc,&lt;br/&gt;            user_pubk,&lt;br/&gt;        )&lt;br/&gt;        .await?;&lt;br/&gt;&lt;br/&gt;        let write_relays = [&lt;br/&gt;            relays,&lt;br/&gt;            repos.extract_relays(),&lt;br/&gt;            events_write_relays,&lt;br/&gt;            naddrs.extract_relays(),&lt;br/&gt;            self.original_patch.map(|e| e.relays).unwrap_or_default(),&lt;br/&gt;            utils::add_write_relays(relays_list.as_ref()),&lt;br/&gt;            client.read_relays_from_users(&amp;amp;maintainers).await,&lt;br/&gt;        ]&lt;br/&gt;        .concat();&lt;br/&gt;&lt;br/&gt;        tracing::trace!(write_relays = ?write_relays, &amp;#34;Write relays of the patches&amp;#34;);&lt;br/&gt;        let nevents = future::join_all(events.into_iter().map(|mut event| {&lt;br/&gt;            async {&lt;br/&gt;                let event_id = event.id();&lt;br/&gt;                let subject = event&lt;br/&gt;                    .tags&lt;br/&gt;                    .find(TagKind::Alt)&lt;br/&gt;                    .and_then(Tag::content)&lt;br/&gt;                    .expect(&amp;#34;There is an alt&amp;#34;)&lt;br/&gt;                    .replace(PATCH_ALT_PREFIX, &amp;#34;&amp;#34;);&lt;br/&gt;                client&lt;br/&gt;                    .send_event_to(event, relays_list.as_ref(), &amp;amp;write_relays)&lt;br/&gt;                    .await&lt;br/&gt;                    .map(|r| Ok((subject, utils::new_nevent(event_id, &amp;amp;r)?)))?&lt;br/&gt;            }&lt;br/&gt;        }))&lt;br/&gt;        .await&lt;br/&gt;        .into_iter()&lt;br/&gt;        .collect::&amp;lt;N34Result&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;&amp;gt;()?;&lt;br/&gt;&lt;br/&gt;        for (subject, nevent) in nevents {&lt;br/&gt;            println!(&amp;#34;Created &amp;#39;{subject}&amp;#39;: {nevent}&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn parse_patch_path(patch_path: &amp;amp;str) -&amp;gt; Result&amp;lt;GitPatch, String&amp;gt; {&lt;br/&gt;    tracing::debug!(&amp;#34;Parsing patch file `{patch_path}`&amp;#34;);&lt;br/&gt;    let patch_content = fs::read_to_string(patch_path)&lt;br/&gt;        .map_err(|err| format!(&amp;#34;Failed to read patch file `{patch_path}`: {err}&amp;#34;))?;&lt;br/&gt;    GitPatch::from_str(&amp;amp;patch_content)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn make_patch_series(&lt;br/&gt;    client: &amp;amp;NostrClient,&lt;br/&gt;    patches: Vec&amp;lt;GitPatch&amp;gt;,&lt;br/&gt;    original_patch: Option&amp;lt;EventId&amp;gt;,&lt;br/&gt;    relay_hint: Option&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;    repo_coordinates: Vec&amp;lt;Coordinate&amp;gt;,&lt;br/&gt;    euc: Option&amp;lt;&amp;amp;Sha1Hash&amp;gt;,&lt;br/&gt;    author_pkey: PublicKey,&lt;br/&gt;) -&amp;gt; N34Result&amp;lt;(Vec&amp;lt;UnsignedEvent&amp;gt;, Vec&amp;lt;RelayUrl&amp;gt;)&amp;gt; {&lt;br/&gt;    let mut write_relays = Vec::new();&lt;br/&gt;    let mut patch_series = Vec::new();&lt;br/&gt;    let mut patches = patches.into_iter();&lt;br/&gt;    let root_patch = patches.next().expect(&amp;#34;Patches can&amp;#39;t be empty&amp;#34;);&lt;br/&gt;    let (root_event, root_relays) = make_patch(&lt;br/&gt;        client,&lt;br/&gt;        root_patch,&lt;br/&gt;        None,&lt;br/&gt;        original_patch,&lt;br/&gt;        relay_hint.as_ref(),&lt;br/&gt;        &amp;amp;repo_coordinates,&lt;br/&gt;        euc,&lt;br/&gt;        author_pkey,&lt;br/&gt;    )&lt;br/&gt;    .await;&lt;br/&gt;    write_relays.extend(root_relays);&lt;br/&gt;    let root_id = *root_event.id.as_ref().expect(&amp;#34;There is an id&amp;#34;);&lt;br/&gt;    let mut previous_patch = root_id;&lt;br/&gt;    patch_series.push(root_event);&lt;br/&gt;&lt;br/&gt;    for patch in patches {&lt;br/&gt;        let (patch_event, patch_relays) = make_patch(&lt;br/&gt;            client,&lt;br/&gt;            patch,&lt;br/&gt;            Some(root_id),&lt;br/&gt;            Some(previous_patch),&lt;br/&gt;            relay_hint.as_ref(),&lt;br/&gt;            &amp;amp;repo_coordinates,&lt;br/&gt;            euc,&lt;br/&gt;            author_pkey,&lt;br/&gt;        )&lt;br/&gt;        .await;&lt;br/&gt;        previous_patch = patch_event.id.expect(&amp;#34;there is an id&amp;#34;);&lt;br/&gt;        write_relays.extend(patch_relays);&lt;br/&gt;        patch_series.push(patch_event);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok((patch_series, write_relays))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[allow(clippy::too_many_arguments)]&lt;br/&gt;async fn make_patch(&lt;br/&gt;    client: &amp;amp;NostrClient,&lt;br/&gt;    patch: GitPatch,&lt;br/&gt;    root: Option&amp;lt;EventId&amp;gt;,&lt;br/&gt;    reply_to: Option&amp;lt;EventId&amp;gt;,&lt;br/&gt;    write_relay: Option&amp;lt;&amp;amp;RelayUrl&amp;gt;,&lt;br/&gt;    repo_coordinates: &amp;amp;[Coordinate],&lt;br/&gt;    euc: Option&amp;lt;&amp;amp;Sha1Hash&amp;gt;,&lt;br/&gt;    author_pkey: PublicKey,&lt;br/&gt;) -&amp;gt; (UnsignedEvent, Vec&amp;lt;RelayUrl&amp;gt;) {&lt;br/&gt;    let content_details = client.parse_content(&amp;amp;patch.body).await;&lt;br/&gt;    let content_relays = content_details.write_relays.clone();&lt;br/&gt;    // NIP-34 compliance requires referencing the previous patch using `NIP-10 e&lt;br/&gt;    // reply`. However, this fails for the second patch when&lt;br/&gt;    // `EventBuilder::dedup_tags` is enabled because:&lt;br/&gt;    // 1. The tag is treated as a duplicate based on its content (the root ID).&lt;br/&gt;    // 2. The second patch would reply to the root twice:&lt;br/&gt;    //    - First with the &amp;#39;root&amp;#39; marker&lt;br/&gt;    //    - Then with the &amp;#39;reply&amp;#39; marker&lt;br/&gt;    // The `EventBuilder::dedup_tags` function then removes the &amp;#39;reply&amp;#39; marker as a&lt;br/&gt;    // duplicate.&lt;br/&gt;    let mut safe_dedup_tags = Tags::new();&lt;br/&gt;    safe_dedup_tags.push(Tag::alt(format!(&amp;#34;{PATCH_ALT_PREFIX}{}&amp;#34;, patch.subject)));&lt;br/&gt;    safe_dedup_tags.push(Tag::description(patch.subject));&lt;br/&gt;    safe_dedup_tags.extend(content_details.into_tags());&lt;br/&gt;    safe_dedup_tags.extend(&lt;br/&gt;        repo_coordinates&lt;br/&gt;            .iter()&lt;br/&gt;            .map(|c| Tag::coordinate(c.clone(), None)),&lt;br/&gt;    );&lt;br/&gt;    safe_dedup_tags.extend(&lt;br/&gt;        repo_coordinates&lt;br/&gt;            .iter()&lt;br/&gt;            .map(|c| Tag::public_key(c.public_key)),&lt;br/&gt;    );&lt;br/&gt;    if let Some(euc) = euc {&lt;br/&gt;        safe_dedup_tags.push(Tag::reference(euc.to_string()));&lt;br/&gt;    }&lt;br/&gt;    safe_dedup_tags.dedup();&lt;br/&gt;    let mut event_builder = EventBuilder::new(Kind::GitPatch, patch.inner).tags(safe_dedup_tags);&lt;br/&gt;&lt;br/&gt;    // If the root is None, this indicates we&amp;#39;re handling the root event&lt;br/&gt;    if let Some(root_id) = root {&lt;br/&gt;        event_builder =&lt;br/&gt;            event_builder.tag(utils::event_reply_tag(&amp;amp;root_id, write_relay, Marker::Root));&lt;br/&gt;    } else {&lt;br/&gt;        event_builder = event_builder.tag(Tag::hashtag(ROOT_HASHTAG_CONTENT));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Handles the case where there is a patch to reply to but no root. This&lt;br/&gt;    // indicates we are processing a revision, as the root revision should reply&lt;br/&gt;    // directly to the original patch.&lt;br/&gt;    if let Some(reply_to_id) = reply_to {&lt;br/&gt;        if root.is_none() {&lt;br/&gt;            event_builder = event_builder.tags([&lt;br/&gt;                utils::event_reply_tag(&amp;amp;reply_to_id, write_relay, Marker::Reply),&lt;br/&gt;                Tag::hashtag(REVISION_ROOT_HASHTAG_CONTENT),&lt;br/&gt;            ]);&lt;br/&gt;        } else {&lt;br/&gt;            event_builder = event_builder.tag(utils::event_reply_tag(&lt;br/&gt;                &amp;amp;reply_to_id,&lt;br/&gt;                write_relay,&lt;br/&gt;                Marker::Reply,&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;    (&lt;br/&gt;        event_builder.build(author_pkey),&lt;br/&gt;        content_relays.into_iter().collect(),&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:43:46Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsqk0zc4lsxfrl25ltzpgm8zntlc594fqps8vfte72avlsr2spa49gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3js9v8</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsqk0zc4lsxfrl25ltzpgm8zntlc594fqps8vfte72avlsr2spa49gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3js9v8" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::num::NonZeroUsize;&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ListArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;)]&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// Maximum number of issues to list&lt;br/&gt;    #[arg(long, default_value = &amp;#34;15&amp;#34;)]&lt;br/&gt;    limit:  NonZeroUsize,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ListArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        common_commands::list_patches_and_issues(options, self.naddrs, false, self.limit.into())&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:43:46Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsvmy7nzg92yc7hna4zcq2xn82c7c8gx60udarj9898uamf4ejrg0gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9fvh04</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsvmy7nzg92yc7hna4zcq2xn82c7c8gx60udarj9898uamf4ejrg0gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9fvh04" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use super::IssueStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::CommandRunner,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct CloseArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The open issue id to close it&lt;br/&gt;    issue_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for CloseArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::issue_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.issue_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            IssueStatus::Closed,&lt;br/&gt;            |issue_status| {&lt;br/&gt;                if issue_status.is_closed() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t close an already closed issue&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if issue_status.is_resolved() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t close a resolved issue&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:43:33Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs23z9f8lstqd8lgfekzuqz6m5lj6hhadglxl0y6vy4s4h4vaympzgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv26fh8p</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs23z9f8lstqd8lgfekzuqz6m5lj6hhadglxl0y6vy4s4h4vaympzgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv26fh8p" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use super::PatchStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::CommandRunner,&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ReopenArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:   Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The closed/drafted patch id to reopen it. Must be orignal root patch&lt;br/&gt;    patch_id: NostrEvent,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ReopenArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::patch_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.patch_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            PatchStatus::Open,&lt;br/&gt;            None,&lt;br/&gt;            Vec::new(),&lt;br/&gt;            |patch_status| {&lt;br/&gt;                if patch_status.is_open() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t open an already open patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_merged_or_applied() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t open a merged/applied patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:43:33Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsvshjz37uxnmz4kxsem3gmk8kppl6rcl59vm4dxwu7ktkuykymxhgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3x40xn</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsvshjz37uxnmz4kxsem3gmk8kppl6rcl59vm4dxwu7ktkuykymxhgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3x40xn" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `patch apply` suubcommand&lt;br/&gt;mod apply;&lt;br/&gt;/// `patch close` subcommand&lt;br/&gt;mod close;&lt;br/&gt;/// `patch draft` subcommand&lt;br/&gt;mod draft;&lt;br/&gt;/// `patch fetch` subcommand&lt;br/&gt;mod fetch;&lt;br/&gt;/// `patch list` subcommand&lt;br/&gt;mod list;&lt;br/&gt;/// `patch merge` subcommand&lt;br/&gt;mod merge;&lt;br/&gt;/// `patch reopen` subcommand&lt;br/&gt;mod reopen;&lt;br/&gt;/// `patch send` subcommand&lt;br/&gt;mod send;&lt;br/&gt;#[cfg(test)]&lt;br/&gt;mod tests;&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    fmt,&lt;br/&gt;    path::{Path, PathBuf},&lt;br/&gt;    str::FromStr,&lt;br/&gt;    sync::LazyLock,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;use nostr::event::Kind;&lt;br/&gt;use regex::Regex;&lt;br/&gt;&lt;br/&gt;use self::apply::ApplyArgs;&lt;br/&gt;use self::close::CloseArgs;&lt;br/&gt;use self::draft::DraftArgs;&lt;br/&gt;use self::fetch::FetchArgs;&lt;br/&gt;use self::list::ListArgs;&lt;br/&gt;use self::merge::MergeArgs;&lt;br/&gt;use self::reopen::ReopenArgs;&lt;br/&gt;use self::send::SendArgs;&lt;br/&gt;use super::{CliOptions, CommandRunner};&lt;br/&gt;use crate::error::{N34Error, N34Result};&lt;br/&gt;&lt;br/&gt;/// Regular expression for extracting the patch subject.&lt;br/&gt;static SUBJECT_RE: LazyLock&amp;lt;Regex&amp;gt; =&lt;br/&gt;    LazyLock::new(|| Regex::new(r&amp;#34;(?m)^Subject: (.*(?:\n .*)*)&amp;#34;).unwrap());&lt;br/&gt;&lt;br/&gt;/// Regular expression for extracting the patch body.&lt;br/&gt;static BODY_RE: LazyLock&amp;lt;Regex&amp;gt; =&lt;br/&gt;    LazyLock::new(|| Regex::new(r&amp;#34;\n\n((?:.|\n)*?)(?:\n--[ -]|\z)&amp;#34;).unwrap());&lt;br/&gt;&lt;br/&gt;/// Regular expiration for extracting the patch version and number&lt;br/&gt;static PATCH_VERSION_NUMBER_RE: LazyLock&amp;lt;Regex&amp;gt; = LazyLock::new(|| {&lt;br/&gt;    Regex::new(r&amp;#34;\[PATCH\s&#43;(?:v(?&amp;lt;version&amp;gt;\d&#43;)\s*)?(?&amp;lt;number&amp;gt;\d&#43;)/(?:\d&#43;)&amp;#34;).unwrap()&lt;br/&gt;});&lt;br/&gt;&lt;br/&gt;/// Content of the hashtag representing the root patch.&lt;br/&gt;pub const ROOT_HASHTAG_CONTENT: &amp;amp;str = &amp;#34;root&amp;#34;;&lt;br/&gt;/// Content of the hashtag representing the root revision patch.&lt;br/&gt;pub const REVISION_ROOT_HASHTAG_CONTENT: &amp;amp;str = &amp;#34;root-revision&amp;#34;;&lt;br/&gt;/// The content of the hashtag used by `ngit-cli` to represent a root revision&lt;br/&gt;/// patch before the commit 6ae42e67d9da36f6c2e1356acba30a3a62112bc7. This was a&lt;br/&gt;/// typo.&lt;br/&gt;pub const LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT: &amp;amp;str = &amp;#34;revision-root&amp;#34;;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum PatchSubcommands {&lt;br/&gt;    /// Send one or more patches to a repository.&lt;br/&gt;    Send(SendArgs),&lt;br/&gt;    /// Fetches a patch by its id.&lt;br/&gt;    Fetch(FetchArgs),&lt;br/&gt;    /// Closes an open or drafted patch.&lt;br/&gt;    Close(CloseArgs),&lt;br/&gt;    /// Converts an open patch to draft state.&lt;br/&gt;    Draft(DraftArgs),&lt;br/&gt;    /// Reopens a closed or drafted patch.&lt;br/&gt;    Reopen(ReopenArgs),&lt;br/&gt;    /// Set an open patch status to applied.&lt;br/&gt;    Apply(ApplyArgs),&lt;br/&gt;    /// Set an open patch status to merged.&lt;br/&gt;    Merge(MergeArgs),&lt;br/&gt;    /// List the repositories patches.&lt;br/&gt;    List(ListArgs),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Represents a git patch&lt;br/&gt;#[derive(Clone, Debug)]&lt;br/&gt;pub struct GitPatch {&lt;br/&gt;    /// Full content of the patch file&lt;br/&gt;    pub inner:   String,&lt;br/&gt;    /// Short description of the patch changes&lt;br/&gt;    pub subject: String,&lt;br/&gt;    /// Detailed explanation of the patch changes&lt;br/&gt;    pub body:    String,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub enum PatchStatus {&lt;br/&gt;    /// The patch is currently open&lt;br/&gt;    Open,&lt;br/&gt;    /// The patch has been merged/applied&lt;br/&gt;    MergedApplied,&lt;br/&gt;    /// The patch has been closed&lt;br/&gt;    Closed,&lt;br/&gt;    /// A patch that has been drafted but not yet applied.&lt;br/&gt;    Draft,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl PatchStatus {&lt;br/&gt;    /// Maps the issue status to its corresponding Nostr kind.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn kind(&amp;amp;self) -&amp;gt; Kind {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Open =&amp;gt; Kind::GitStatusOpen,&lt;br/&gt;            Self::MergedApplied =&amp;gt; Kind::GitStatusApplied,&lt;br/&gt;            Self::Closed =&amp;gt; Kind::GitStatusClosed,&lt;br/&gt;            Self::Draft =&amp;gt; Kind::GitStatusDraft,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns the string representation of the patch status.&lt;br/&gt;    pub const fn as_str(&amp;amp;self) -&amp;gt; &amp;amp;&amp;#39;static str {&lt;br/&gt;        match self {&lt;br/&gt;            Self::Open =&amp;gt; &amp;#34;Open&amp;#34;,&lt;br/&gt;            Self::MergedApplied =&amp;gt; &amp;#34;Merged/Applied&amp;#34;,&lt;br/&gt;            Self::Closed =&amp;gt; &amp;#34;Closed&amp;#34;,&lt;br/&gt;            Self::Draft =&amp;gt; &amp;#34;Draft&amp;#34;,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the patch is open.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_open(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Open)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the patch is merged/applied.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_merged_or_applied(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::MergedApplied)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the patch is closed.&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_closed(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Closed)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the patch is drafted&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn is_drafted(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Self::Draft)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl From&amp;lt;&amp;amp;PatchStatus&amp;gt; for Kind {&lt;br/&gt;    fn from(status: &amp;amp;PatchStatus) -&amp;gt; Self {&lt;br/&gt;        status.kind()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl fmt::Display for PatchStatus {&lt;br/&gt;    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter&amp;lt;&amp;#39;_&amp;gt;) -&amp;gt; fmt::Result {&lt;br/&gt;        write!(f, &amp;#34;{}&amp;#34;, self.as_str())&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl TryFrom&amp;lt;Kind&amp;gt; for PatchStatus {&lt;br/&gt;    type Error = N34Error;&lt;br/&gt;&lt;br/&gt;    fn try_from(kind: Kind) -&amp;gt; Result&amp;lt;Self, Self::Error&amp;gt; {&lt;br/&gt;        match kind {&lt;br/&gt;            Kind::GitStatusOpen =&amp;gt; Ok(Self::Open),&lt;br/&gt;            Kind::GitStatusApplied =&amp;gt; Ok(Self::MergedApplied),&lt;br/&gt;            Kind::GitStatusClosed =&amp;gt; Ok(Self::Closed),&lt;br/&gt;            Kind::GitStatusDraft =&amp;gt; Ok(Self::Draft),&lt;br/&gt;            _ =&amp;gt; Err(N34Error::InvalidPatchStatus(kind)),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl GitPatch {&lt;br/&gt;    /// Returns the patch file name from the subject&lt;br/&gt;    pub fn filename(&amp;amp;self, parent: impl AsRef&amp;lt;Path&amp;gt;) -&amp;gt; N34Result&amp;lt;PathBuf&amp;gt; {&lt;br/&gt;        let (patch_version, patch_number) = if self.subject.contains(&amp;#34;[PATCH]&amp;#34;) {&lt;br/&gt;            (String::new(), &amp;#34;1&amp;#34;)&lt;br/&gt;        } else {&lt;br/&gt;            patch_version_and_subject(&amp;amp;self.subject)?&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let patch_name = if patch_number == &amp;#34;0&amp;#34; {&lt;br/&gt;            &amp;#34;cover-letter&amp;#34;.to_owned()&lt;br/&gt;        } else {&lt;br/&gt;            patch_file_name(&amp;amp;self.subject)?&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        Ok(parent&lt;br/&gt;            .as_ref()&lt;br/&gt;            .join(format!(&amp;#34;{patch_version}{patch_number:0&amp;gt;4}-{patch_name}&amp;#34;).replace(&amp;#34;--&amp;#34;, &amp;#34;-&amp;#34;))&lt;br/&gt;            .with_extension(&amp;#34;patch&amp;#34;))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for PatchSubcommands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::run_command!(self, options, &amp;amp; Send Fetch Close Reopen Draft Apply Merge List)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl FromStr for GitPatch {&lt;br/&gt;    type Err = String;&lt;br/&gt;&lt;br/&gt;    fn from_str(patch_content: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, Self::Err&amp;gt; {&lt;br/&gt;        // Regex for subject (handles multi-line subjects)&lt;br/&gt;        let subject = SUBJECT_RE&lt;br/&gt;            .captures(patch_content)&lt;br/&gt;            .and_then(|cap| cap.get(1))&lt;br/&gt;            .ok_or(&amp;#34;No subject found&amp;#34;)?&lt;br/&gt;            .as_str()&lt;br/&gt;            .trim()&lt;br/&gt;            .replace(&amp;#39;\n&amp;#39;, &amp;#34;&amp;#34;)&lt;br/&gt;            .to_string();&lt;br/&gt;&lt;br/&gt;        // Regex for body&lt;br/&gt;        let body = BODY_RE&lt;br/&gt;            .captures(patch_content)&lt;br/&gt;            .and_then(|cap| cap.get(1))&lt;br/&gt;            .ok_or(&amp;#34;No body found&amp;#34;)?&lt;br/&gt;            .as_str()&lt;br/&gt;            .trim()&lt;br/&gt;            .to_string();&lt;br/&gt;        Ok(Self {&lt;br/&gt;            inner: patch_content.to_owned(),&lt;br/&gt;            subject,&lt;br/&gt;            body,&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts the version prefix and patch number from a patch subject string.&lt;br/&gt;///&lt;br/&gt;/// The version prefix is formatted as &amp;#34;v{version}-&amp;#34; if present, or an empty&lt;br/&gt;/// string. The patch number is mandatory and will cause an error if not found.&lt;br/&gt;fn patch_version_and_subject(subject: &amp;amp;str) -&amp;gt; N34Result&amp;lt;(String, &amp;amp;str)&amp;gt; {&lt;br/&gt;    let captures = PATCH_VERSION_NUMBER_RE.captures(subject).ok_or_else(|| {&lt;br/&gt;        N34Error::InvalidEvent(format!(&amp;#34;Can not parse the patch subject `{subject}`&amp;#34;))&lt;br/&gt;    })?;&lt;br/&gt;    Ok((&lt;br/&gt;        captures&lt;br/&gt;            .name(&amp;#34;version&amp;#34;)&lt;br/&gt;            .map(|m| format!(&amp;#34;v{}-&amp;#34;, m.as_str()))&lt;br/&gt;            .unwrap_or_default(),&lt;br/&gt;        captures&lt;br/&gt;            .name(&amp;#34;number&amp;#34;)&lt;br/&gt;            .map(|m| m.as_str())&lt;br/&gt;            .expect(&amp;#34;It&amp;#39;s not optional, regex will fail if it&amp;#39;s not found&amp;#34;),&lt;br/&gt;    ))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts a clean file name from the patch subject by removing version info&lt;br/&gt;/// and special characters. Converts to lowercase and ensures the name only&lt;br/&gt;/// contains alphanumeric, &amp;#39;.&amp;#39;, &amp;#39;-&amp;#39;, or &amp;#39;_&amp;#39; characters.&lt;br/&gt;fn patch_file_name(subject: &amp;amp;str) -&amp;gt; N34Result&amp;lt;String&amp;gt; {&lt;br/&gt;    Ok(subject&lt;br/&gt;        .split_once(&amp;#34;]&amp;#34;)&lt;br/&gt;        .ok_or_else(|| {&lt;br/&gt;            N34Error::InvalidEvent(format!(&lt;br/&gt;                &amp;#34;Invalid patch subject. No `[PATCH ...]`: `{subject}`&amp;#34;,&lt;br/&gt;            ))&lt;br/&gt;        })?&lt;br/&gt;        .1&lt;br/&gt;        .trim()&lt;br/&gt;        .to_lowercase()&lt;br/&gt;        .replace(&lt;br/&gt;            |c: char| !c.is_ascii_alphanumeric() &amp;amp;&amp;amp; ![&amp;#39;.&amp;#39;, &amp;#39;-&amp;#39;, &amp;#39;_&amp;#39;].contains(&amp;amp;c),&lt;br/&gt;            &amp;#34;-&amp;#34;,&lt;br/&gt;        )&lt;br/&gt;        .chars()&lt;br/&gt;        .take(60)&lt;br/&gt;        .collect::&amp;lt;String&amp;gt;()&lt;br/&gt;        .trim_matches(&amp;#39;-&amp;#39;)&lt;br/&gt;        .trim()&lt;br/&gt;        .replace(&amp;#34;--&amp;#34;, &amp;#34;-&amp;#34;))&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:43:21Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsg6t0vj3qms7hgaj6fm3svd4lddkwjjy6rg6qjul0vpsr8qtpaggqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8edczy</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsg6t0vj3qms7hgaj6fm3svd4lddkwjjy6rg6qjul0vpsr8qtpaggqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8edczy" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::types::RelayUrl;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, traits::CommandRunner},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct RelaysArgs {&lt;br/&gt;    /// List of relay URLs to append to fallback relays. If empty, removes all&lt;br/&gt;    /// fallback relays.&lt;br/&gt;    relays:          Vec&amp;lt;RelayUrl&amp;gt;,&lt;br/&gt;    /// Replace existing fallback relays instead of appending new ones.&lt;br/&gt;    #[arg(long = &amp;#34;override&amp;#34;)]&lt;br/&gt;    override_relays: bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for RelaysArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        if self.relays.is_empty() {&lt;br/&gt;            options.config.fallback_relays = None;&lt;br/&gt;        } else if self.override_relays {&lt;br/&gt;            options.config.fallback_relays = Some(self.relays);&lt;br/&gt;        } else {&lt;br/&gt;            let mut relays = options.config.fallback_relays.clone().unwrap_or_default();&lt;br/&gt;            relays.extend(self.relays);&lt;br/&gt;            relays.sort_unstable();&lt;br/&gt;            relays.dedup();&lt;br/&gt;            options.config.fallback_relays = Some(relays);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:43:21Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsf7rwj9zu66emg34j7a0ptqmu93ckfw6v22ufqwtwewzxdatnrasczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvnj06t0</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsf7rwj9zu66emg34j7a0ptqmu93ckfw6v22ufqwtwewzxdatnrasczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvnj06t0" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, traits::CommandRunner},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct PowArgs {&lt;br/&gt;    /// The new default PoW difficulty&lt;br/&gt;    difficulty: u8,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for PowArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        options.config.pow = Some(self.difficulty);&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:43:08Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqsffht9qac5kat7t68re7nnuzsqzdexe5qwn2dhfcsk9dmx6tcr9fgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdsg832</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqsffht9qac5kat7t68re7nnuzsqzdexe5qwn2dhfcsk9dmx6tcr9fgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdsg832" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;use nostr::hashes::sha1::Hash as Sha1Hash;&lt;br/&gt;&lt;br/&gt;use super::PatchStatus;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{&lt;br/&gt;        CliOptions,&lt;br/&gt;        traits::{CommandRunner, VecNostrEventExt},&lt;br/&gt;        types::{NaddrOrSet, NostrEvent},&lt;br/&gt;    },&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct MergeArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;, long = &amp;#34;repo&amp;#34;)]&lt;br/&gt;    naddrs:         Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// The open patch id to merge it. Must be orignal root patch or&lt;br/&gt;    /// revision root&lt;br/&gt;    patch_id:       NostrEvent,&lt;br/&gt;    /// Patches that have been merged. Use this when only some patches have been&lt;br/&gt;    /// merged, not all.&lt;br/&gt;    #[arg(long = &amp;#34;patches&amp;#34;, value_name = &amp;#34;PATCH-EVENT-ID&amp;#34;)]&lt;br/&gt;    merged_patches: Vec&amp;lt;NostrEvent&amp;gt;,&lt;br/&gt;    /// The merge commit id&lt;br/&gt;    merge_commit:   Sha1Hash,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for MergeArgs {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::cli::common_commands::patch_status_command(&lt;br/&gt;            options,&lt;br/&gt;            self.patch_id,&lt;br/&gt;            self.naddrs,&lt;br/&gt;            PatchStatus::MergedApplied,&lt;br/&gt;            Some(either::Either::Left(self.merge_commit)),&lt;br/&gt;            self.merged_patches.into_event_ids(),&lt;br/&gt;            |patch_status| {&lt;br/&gt;                if patch_status.is_merged_or_applied() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t merge an already merged patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_closed() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t merge a closed patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                if patch_status.is_drafted() {&lt;br/&gt;                    return Err(N34Error::InvalidStatus(&lt;br/&gt;                        &amp;#34;You can&amp;#39;t merge a drafted patch&amp;#34;.to_owned(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;                Ok(())&lt;br/&gt;            },&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:43:08Z</updated>
  </entry>

  <entry>
    <id>https://nostr.ae/nevent1qqs8j3g9ce4f9yl7ce4ljpkcz7uwng2elagyeware5g2tuwlmjavyqszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvw96jwm</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://nostr.ae/nevent1qqs8j3g9ce4f9yl7ce4ljpkcz7uwng2elagyeware5g2tuwlmjavyqszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvw96jwm" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::num::NonZeroUsize;&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Debug, Args)]&lt;br/&gt;pub struct ListArgs {&lt;br/&gt;    /// Repository address in `naddr` format (`naddr1...`), NIP-05 format&lt;br/&gt;    /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.&lt;br/&gt;    ///&lt;br/&gt;    /// If omitted, looks for a `nostr-address` file.&lt;br/&gt;    #[arg(value_name = &amp;#34;NADDR-NIP05-OR-SET&amp;#34;)]&lt;br/&gt;    naddrs: Option&amp;lt;Vec&amp;lt;NaddrOrSet&amp;gt;&amp;gt;,&lt;br/&gt;    /// Maximum number of patches to list&lt;br/&gt;    #[arg(long, default_value = &amp;#34;15&amp;#34;)]&lt;br/&gt;    limit:  NonZeroUsize,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ListArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        common_commands::list_patches_and_issues(options, self.naddrs, true, self.limit.into())&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:42:54Z</updated>
  </entry>

</feed>