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

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




  <entry>
    <id>https://njump.me/nevent1qqsrlsfazqlpccd9qplp86sghavhya43uftt8p5yhnm2chv0x2d2htczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jq97gef</id>
    
      <title type="html">Build manifest for get_file_hash v0.5.3</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsrlsfazqlpccd9qplp86sghavhya43uftt8p5yhnm2chv0x2d2htczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jq97gef" />
    <content type="html">
      In reply to &lt;a href=&#39;/nevent1qqs2y3rewsme5s09gm85eshhg3zhu9kyzjxmmrq6jrw2ene0ce70qvqqxag0u&#39;&gt;nevent1q…ag0u&lt;/a&gt;&lt;br/&gt;_________________________&lt;br/&gt;&lt;br/&gt;Build manifest for get_file_hash v0.5.3
    </content>
    <updated>2026-10-09T01:55:15Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs2y3rewsme5s09gm85eshhg3zhu9kyzjxmmrq6jrw2ene0ce70qvqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jdua4hn</id>
    
      <title type="html">&amp;lt;?xml version=&amp;#39;1.0&amp;#39; ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs2y3rewsme5s09gm85eshhg3zhu9kyzjxmmrq6jrw2ene0ce70qvqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jdua4hn" />
    <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-10-09T01:55:01Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsyymtd2s7x4xl8f5l7ty7y3xvm3f0pzx2zy9d5v67zwg9zly8crpszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6ju3r6fg</id>
    
      <title type="html">use std::process::Command; use std::fs; use sha2::{Digest, ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsyymtd2s7x4xl8f5l7ty7y3xvm3f0pzx2zy9d5v67zwg9zly8crpszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6ju3r6fg" />
    <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-10-09T01:54:55Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqstc7f4sxcnz9t4t2ctu4gnzvney6jprafp03qv6p75gpa9vx3vxmszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jx8rmlp</id>
    
      <title type="html">//! Recursive packet framing and parity helpers for the ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqstc7f4sxcnz9t4t2ctu4gnzvney6jprafp03qv6p75gpa9vx3vxmszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jx8rmlp" />
    <content type="html">
      //! Recursive packet framing and parity helpers for the `perfect_ip` protocol.&lt;br/&gt;//!&lt;br/&gt;//! `perfect_ip` is a repair-oriented tree protocol. It does not try to be a&lt;br/&gt;//! general transport; instead it turns a byte buffer into a deterministic tree&lt;br/&gt;//! of MTU-safe data slices and parity slices that can be reconstructed when a&lt;br/&gt;//! sibling packet is missing.&lt;br/&gt;//!&lt;br/&gt;//! ## Packet model&lt;br/&gt;//!&lt;br/&gt;//! - [`process_slice`] recursively splits payloads until each emitted leaf fits&lt;br/&gt;//!   within the packet budget.&lt;br/&gt;//! - Internal nodes emit a left child, right child, and parity frame.&lt;br/&gt;//! - Packet ids form a stable recursive path such as `ROOT.1.0.P`.&lt;br/&gt;//! - [`packetize`] finalizes the batch by filling `total_packets` into every&lt;br/&gt;//!   packet header.&lt;br/&gt;//!&lt;br/&gt;//! ## Wire model&lt;br/&gt;//!&lt;br/&gt;//! On the wire, packet slices are exchanged as Nostr events. Each slice is&lt;br/&gt;//! encoded as JSON content under a dedicated custom event kind, which keeps the&lt;br/&gt;//! protocol relay-native and removes the old transport dependency.&lt;br/&gt;//!&lt;br/&gt;//! ## Repair model&lt;br/&gt;//!&lt;br/&gt;//! The parity scheme is XOR-based. Given one sibling payload and the matching&lt;br/&gt;//! parity frame, [`recover_missing_data`] and [`recover_missing_slice`] can&lt;br/&gt;//! rebuild the missing sibling payload. [`IntegrityManager`] tracks the&lt;br/&gt;//! manifest, records received slices, persists the partial state to disk, and&lt;br/&gt;//! verifies that parity frames still match their children.&lt;br/&gt;//!&lt;br/&gt;//! The layout intentionally mirrors the packet-tree sketch in the&lt;br/&gt;//! `perfect_ip.rs` gist.&lt;br/&gt;extern crate alloc;&lt;br/&gt;use alloc::collections::{BTreeMap, BTreeSet};&lt;br/&gt;use alloc::string::{String, ToString};&lt;br/&gt;use alloc::vec::Vec;&lt;br/&gt;use std::io;&lt;br/&gt;use std::path::Path;&lt;br/&gt;&lt;br/&gt;use serde::{Deserialize, Serialize};&lt;br/&gt;use nostr_sdk::prelude::*;&lt;br/&gt;&lt;br/&gt;/// Packet header metadata shared by all packet types in the tree.&lt;br/&gt;///&lt;br/&gt;/// `seq_num` is assigned in the order packets are emitted by the recursive&lt;br/&gt;/// packetizer. `total_packets` is populated only after packetization has&lt;br/&gt;/// finished, so callers can treat the batch as self-describing once finalized.&lt;br/&gt;#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]&lt;br/&gt;pub struct Header {&lt;br/&gt;    /// Monotonic sequence number assigned during packetization.&lt;br/&gt;    pub seq_num: u32,&lt;br/&gt;    /// Total number of packets in the finalized batch.&lt;br/&gt;    pub total_packets: u32,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// A single packet produced by the recursive packetizer.&lt;br/&gt;///&lt;br/&gt;/// `id` carries the recursive path for the packet, such as `ROOT.0.1.P`.&lt;br/&gt;/// `is_parity` marks frames that store XOR parity rather than original user&lt;br/&gt;/// data. `data` always contains the raw bytes that would be transmitted on the&lt;br/&gt;/// wire inside the JSON-encoded repair message.&lt;br/&gt;#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]&lt;br/&gt;pub struct ProtocolSlice {&lt;br/&gt;    /// Stable recursive packet identifier.&lt;br/&gt;    pub id: String,&lt;br/&gt;    /// Packet sequencing metadata.&lt;br/&gt;    pub header: Header,&lt;br/&gt;    /// Raw payload bytes for the packet or parity frame.&lt;br/&gt;    pub data: Vec&amp;lt;u8&amp;gt;,&lt;br/&gt;    /// `true` when this slice is a parity frame.&lt;br/&gt;    pub is_parity: bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Finalized packet tree output.&lt;br/&gt;///&lt;br/&gt;/// `total_packets` is duplicated here so callers can inspect batch metadata&lt;br/&gt;/// without walking the packet list. The packet list is stable and already has&lt;br/&gt;/// each packet header patched to reflect the finalized count.&lt;br/&gt;#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]&lt;br/&gt;pub struct PacketBatch {&lt;br/&gt;    /// Number of packets in the batch.&lt;br/&gt;    pub total_packets: u32,&lt;br/&gt;    /// Finalized packets with matching `total_packets` headers.&lt;br/&gt;    pub packets: Vec&amp;lt;ProtocolSlice&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Tracks the expected manifest and the packets that have arrived so far.&lt;br/&gt;///&lt;br/&gt;/// The manager is deliberately simple: it stores the expected ids, the received&lt;br/&gt;/// slices, and exposes helpers for missing-node inspection, parity validation,&lt;br/&gt;/// and persistence.&lt;br/&gt;#[derive(Debug, Clone, Default)]&lt;br/&gt;pub struct IntegrityManager {&lt;br/&gt;    manifest: BTreeSet&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Received packet slices keyed by packet id.&lt;br/&gt;    pub received_slices: BTreeMap&amp;lt;String, ProtocolSlice&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl IntegrityManager {&lt;br/&gt;    /// Create a manager from the set of expected packet ids.&lt;br/&gt;    ///&lt;br/&gt;    /// The manifest is stored as a set so lookups stay cheap even for larger&lt;br/&gt;    /// packet trees.&lt;br/&gt;    pub fn new(expected_ids: Vec&amp;lt;String&amp;gt;) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            manifest: expected_ids.into_iter().collect(),&lt;br/&gt;            received_slices: BTreeMap::new(),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Record a received packet by id, replacing any prior packet with the same id.&lt;br/&gt;    ///&lt;br/&gt;    /// Re-recording a slice is allowed and simply overwrites the previous&lt;br/&gt;    /// entry. That makes repair retries and late arrivals idempotent.&lt;br/&gt;    pub fn record_slice(&amp;amp;mut self, slice: ProtocolSlice) {&lt;br/&gt;        self.received_slices.insert(slice.id.clone(), slice);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Persist the received packet map to disk using JSON.&lt;br/&gt;    ///&lt;br/&gt;    /// This writes only the received slice map. The caller supplies the&lt;br/&gt;    /// manifest again on load so persisted state stays lightweight and portable.&lt;br/&gt;    pub fn persist(&amp;amp;self, path: impl AsRef&amp;lt;Path&amp;gt;) -&amp;gt; io::Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let encoded = serde_json::to_vec(&amp;amp;self.received_slices).map_err(io::Error::other)?;&lt;br/&gt;        std::fs::write(path, encoded)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Load a persisted packet map from disk and attach it to the given manifest.&lt;br/&gt;    ///&lt;br/&gt;    /// The manifest is not persisted with the slice data; callers are expected&lt;br/&gt;    /// to regenerate or reload it independently.&lt;br/&gt;    pub fn load_from_disk(path: impl AsRef&amp;lt;Path&amp;gt;, manifest: BTreeSet&amp;lt;String&amp;gt;) -&amp;gt; io::Result&amp;lt;Self&amp;gt; {&lt;br/&gt;        let bytes = std::fs::read(path)?;&lt;br/&gt;        let received_slices: BTreeMap&amp;lt;String, ProtocolSlice&amp;gt; =&lt;br/&gt;            serde_json::from_slice(&amp;amp;bytes).map_err(io::Error::other)?;&lt;br/&gt;        Ok(Self {&lt;br/&gt;            manifest,&lt;br/&gt;            received_slices,&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Return the packet ids from the manifest that are still missing.&lt;br/&gt;    ///&lt;br/&gt;    /// The returned ids preserve the manifest set&amp;#39;s iteration order only in the&lt;br/&gt;    /// sense that they are collected into a vector; callers should sort if they&lt;br/&gt;    /// need deterministic display order.&lt;br/&gt;    pub fn get_missing_nodes(&amp;amp;self) -&amp;gt; Vec&amp;lt;String&amp;gt; {&lt;br/&gt;        self.manifest&lt;br/&gt;            .iter()&lt;br/&gt;            .filter(|id| !self.received_slices.contains_key(*id))&lt;br/&gt;            .cloned()&lt;br/&gt;            .collect()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Verify that every parity slice matches the XOR of its sibling data slices.&lt;br/&gt;    ///&lt;br/&gt;    /// If a parity node&amp;#39;s siblings are both present, the check recomputes the&lt;br/&gt;    /// XOR and compares it with the stored parity bytes. Missing siblings are&lt;br/&gt;    /// ignored so partial downloads can still pass for the fragments they have.&lt;br/&gt;    pub fn verify_integrity(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        for (id, slice) in &amp;amp;self.received_slices {&lt;br/&gt;            if !slice.is_parity {&lt;br/&gt;                continue;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let Some(base_id) = id.strip_suffix(&amp;#34;.P&amp;#34;) else {&lt;br/&gt;                return false;&lt;br/&gt;            };&lt;br/&gt;            let left = self.received_slices.get(&amp;amp;format!(&amp;#34;{}.0&amp;#34;, base_id));&lt;br/&gt;            let right = self.received_slices.get(&amp;amp;format!(&amp;#34;{}.1&amp;#34;, base_id));&lt;br/&gt;&lt;br/&gt;            if let (Some(left), Some(right)) = (left, right) {&lt;br/&gt;                if calculate_parity(&amp;amp;left.data, &amp;amp;right.data) != slice.data {&lt;br/&gt;                    return false;&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        true&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Nostr event kind used for packet slices.&lt;br/&gt;pub const PERFECT_IP_KIND: Kind = Kind::Custom(31_337);&lt;br/&gt;&lt;br/&gt;/// Hashtag used to query perfect_ip events on relays.&lt;br/&gt;pub const PERFECT_IP_HASHTAG: &amp;amp;str = &amp;#34;perfect_ip&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// PIP manifest event kind.&lt;br/&gt;pub const PIP_MANIFEST_KIND: Kind = Kind::Custom(39_078);&lt;br/&gt;&lt;br/&gt;/// PIP slice event kind.&lt;br/&gt;pub const PIP_SLICE_KIND: Kind = Kind::Custom(39_079);&lt;br/&gt;&lt;br/&gt;/// PIP repair request event kind.&lt;br/&gt;pub const PIP_REPAIR_REQUEST_KIND: Kind = Kind::Custom(39_080);&lt;br/&gt;&lt;br/&gt;/// Hashtag used to query PIP events on relays.&lt;br/&gt;pub const PIP_HASHTAG: &amp;amp;str = &amp;#34;pip&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// Packet manifest describing one tree upload.&lt;br/&gt;#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]&lt;br/&gt;pub struct PacketManifest {&lt;br/&gt;    /// Stable root id for the packet tree.&lt;br/&gt;    pub root: String,&lt;br/&gt;    /// Hex-encoded SHA-256 of the original payload.&lt;br/&gt;    pub sha256: String,&lt;br/&gt;    /// Original payload size in bytes.&lt;br/&gt;    pub size: u64,&lt;br/&gt;    /// Total packet count for the tree.&lt;br/&gt;    pub packets: u64,&lt;br/&gt;    /// Maximum packet tree depth.&lt;br/&gt;    pub depth: u32,&lt;br/&gt;    /// Target MTU used by the packetizer.&lt;br/&gt;    pub mtu: u64,&lt;br/&gt;    /// Encoding name used on the wire.&lt;br/&gt;    pub encoding: String,&lt;br/&gt;    /// Relative path for directory uploads.&lt;br/&gt;    pub path: String,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Repair request for missing packet ids.&lt;br/&gt;#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]&lt;br/&gt;pub struct RepairRequest {&lt;br/&gt;    /// Stable root id for the packet tree.&lt;br/&gt;    pub root: String,&lt;br/&gt;    /// Packet ids still needed for reconstruction.&lt;br/&gt;    pub want: Vec&amp;lt;String&amp;gt;,&lt;br/&gt;    /// Encoding name used on the wire.&lt;br/&gt;    pub encoding: String,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Build a filter for PIP events.&lt;br/&gt;pub fn pip_filter(kind: Kind, id: Option&amp;lt;&amp;amp;str&amp;gt;, hashtag: &amp;amp;str) -&amp;gt; Filter {&lt;br/&gt;    let filter = Filter::new().kind(kind).hashtag(hashtag);&lt;br/&gt;&lt;br/&gt;    match id {&lt;br/&gt;        Some(id) =&amp;gt; filter.identifier(id),&lt;br/&gt;        None =&amp;gt; filter,&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Build a filter for PIP manifest events.&lt;br/&gt;pub fn pip_manifest_filter(id: Option&amp;lt;&amp;amp;str&amp;gt;) -&amp;gt; Filter {&lt;br/&gt;    pip_filter(PIP_MANIFEST_KIND, id, PIP_HASHTAG)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Build a filter for PIP slice events.&lt;br/&gt;pub fn pip_slice_filter(id: Option&amp;lt;&amp;amp;str&amp;gt;) -&amp;gt; Filter {&lt;br/&gt;    pip_filter(PIP_SLICE_KIND, id, PIP_HASHTAG)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Build a filter for PIP repair requests.&lt;br/&gt;pub fn pip_repair_request_filter(id: Option&amp;lt;&amp;amp;str&amp;gt;) -&amp;gt; Filter {&lt;br/&gt;    pip_filter(PIP_REPAIR_REQUEST_KIND, id, PIP_HASHTAG)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Convert a manifest into a Nostr event builder.&lt;br/&gt;pub fn packet_manifest_to_event(&lt;br/&gt;    manifest: &amp;amp;PacketManifest,&lt;br/&gt;    packet_event_ids: &amp;amp;[EventId],&lt;br/&gt;) -&amp;gt; Result&amp;lt;EventBuilder, serde_json::Error&amp;gt; {&lt;br/&gt;    Ok(&lt;br/&gt;        EventBuilder::new(PIP_MANIFEST_KIND, serde_json::to_string(manifest)?)&lt;br/&gt;            .tag(Tag::identifier(manifest.root.clone()))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;sha256&amp;#34;.into(), [manifest.sha256.clone()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;size&amp;#34;.into(), [manifest.size.to_string()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;packets&amp;#34;.into(), [manifest.packets.to_string()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;depth&amp;#34;.into(), [manifest.depth.to_string()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;mtu&amp;#34;.into(), [manifest.mtu.to_string()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;encoding&amp;#34;.into(), [manifest.encoding.clone()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;path&amp;#34;.into(), [manifest.path.clone()]))&lt;br/&gt;            .tag(Tag::hashtag(PIP_HASHTAG))&lt;br/&gt;            .tag(Tag::hashtag(&amp;#34;manifest&amp;#34;))&lt;br/&gt;            .tags(packet_event_ids.iter().map(|event_id| {&lt;br/&gt;                Tag::custom(&lt;br/&gt;                    &amp;#34;e&amp;#34;.into(),&lt;br/&gt;                    [event_id.to_string(), String::new(), &amp;#34;slice&amp;#34;.to_string()],&lt;br/&gt;                )&lt;br/&gt;            })),&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extract packet event ids referenced by a PIP manifest event.&lt;br/&gt;pub fn manifest_packet_event_ids(event: &amp;amp;Event) -&amp;gt; Vec&amp;lt;EventId&amp;gt; {&lt;br/&gt;    event&lt;br/&gt;        .tags&lt;br/&gt;        .iter()&lt;br/&gt;        .filter(|tag| tag.kind().as_str() == &amp;#34;e&amp;#34; &amp;amp;&amp;amp; tag.len() &amp;gt;= 4 &amp;amp;&amp;amp; tag.as_slice()[3].as_str() == &amp;#34;slice&amp;#34;)&lt;br/&gt;        .filter_map(|tag| tag.content().and_then(|value| EventId::parse(value).ok()))&lt;br/&gt;        .collect()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Convert a repair request into a Nostr event builder.&lt;br/&gt;pub fn repair_request_to_event(&lt;br/&gt;    request: &amp;amp;RepairRequest,&lt;br/&gt;    manifest_event_id: EventId,&lt;br/&gt;) -&amp;gt; Result&amp;lt;EventBuilder, serde_json::Error&amp;gt; {&lt;br/&gt;    let builder = request.want.iter().fold(&lt;br/&gt;        EventBuilder::new(PIP_REPAIR_REQUEST_KIND, serde_json::to_string(request)?)&lt;br/&gt;            .tag(Tag::identifier(request.root.clone()))&lt;br/&gt;            .tag(Tag::custom(&lt;br/&gt;                &amp;#34;e&amp;#34;.into(),&lt;br/&gt;                [&lt;br/&gt;                    manifest_event_id.to_string(),&lt;br/&gt;                    String::new(),&lt;br/&gt;                    &amp;#34;request&amp;#34;.to_string(),&lt;br/&gt;                ],&lt;br/&gt;            ))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;encoding&amp;#34;.into(), [request.encoding.clone()]))&lt;br/&gt;            .tag(Tag::hashtag(PIP_HASHTAG))&lt;br/&gt;            .tag(Tag::hashtag(&amp;#34;repair&amp;#34;)),&lt;br/&gt;        |builder, want| builder.tag(Tag::custom(&amp;#34;want&amp;#34;.into(), [want.clone()])),&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    Ok(builder)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Convert a packet slice into a PIP slice event builder.&lt;br/&gt;pub fn packet_slice_to_event(slice: &amp;amp;ProtocolSlice) -&amp;gt; Result&amp;lt;EventBuilder, serde_json::Error&amp;gt; {&lt;br/&gt;    let type_tag = if slice.is_parity { &amp;#34;parity&amp;#34; } else { &amp;#34;data&amp;#34; };&lt;br/&gt;&lt;br/&gt;    Ok(&lt;br/&gt;        EventBuilder::new(PIP_SLICE_KIND, serde_json::to_string(slice)?)&lt;br/&gt;            .tag(Tag::identifier(slice.id.clone()))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;seq&amp;#34;.into(), [slice.header.seq_num.to_string()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;path&amp;#34;.into(), [slice.id.clone()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;type&amp;#34;.into(), [type_tag.to_string()]))&lt;br/&gt;            .tag(Tag::custom(&amp;#34;encoding&amp;#34;.into(), [&amp;#34;json&amp;#34;]))&lt;br/&gt;            .tag(Tag::hashtag(PIP_HASHTAG))&lt;br/&gt;            .tag(Tag::hashtag(&amp;#34;slice&amp;#34;)),&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn proposal_record_to_slice(&lt;br/&gt;    kind: Kind,&lt;br/&gt;    tags: &amp;amp;Tags,&lt;br/&gt;    content: &amp;amp;str,&lt;br/&gt;) -&amp;gt; Result&amp;lt;Option&amp;lt;ProtocolSlice&amp;gt;, serde_json::Error&amp;gt; {&lt;br/&gt;    if kind != PIP_SLICE_KIND {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let Some(root_id) = tags&lt;br/&gt;        .iter()&lt;br/&gt;        .find(|tag| tag.kind().as_str() == &amp;#34;d&amp;#34;)&lt;br/&gt;        .and_then(|tag| tag.content())&lt;br/&gt;    else {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    if !tags&lt;br/&gt;        .iter()&lt;br/&gt;        .any(|tag| tag.kind().as_str() == &amp;#34;t&amp;#34; &amp;amp;&amp;amp; tag.content() == Some(PIP_HASHTAG))&lt;br/&gt;    {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if !tags&lt;br/&gt;        .iter()&lt;br/&gt;        .any(|tag| tag.kind().as_str() == &amp;#34;t&amp;#34; &amp;amp;&amp;amp; tag.content() == Some(&amp;#34;slice&amp;#34;))&lt;br/&gt;    {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let Some(path) = tags&lt;br/&gt;        .iter()&lt;br/&gt;        .find(|tag| tag.kind().as_str() == &amp;#34;path&amp;#34;)&lt;br/&gt;        .and_then(|tag| tag.content())&lt;br/&gt;    else {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    if !path.starts_with(root_id) {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let slice: ProtocolSlice = serde_json::from_str(content)?;&lt;br/&gt;    if slice.id != path {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(Some(slice))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Try to decode a PIP event back into a packet slice.&lt;br/&gt;pub fn packet_slice_from_event(event: &amp;amp;Event) -&amp;gt; Result&amp;lt;Option&amp;lt;ProtocolSlice&amp;gt;, serde_json::Error&amp;gt; {&lt;br/&gt;    proposal_record_to_slice(event.kind, &amp;amp;event.tags, &amp;amp;event.content)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Try to decode an unsigned PIP event back into a packet slice.&lt;br/&gt;pub fn packet_slice_from_unsigned_event(&lt;br/&gt;    event: &amp;amp;UnsignedEvent,&lt;br/&gt;) -&amp;gt; Result&amp;lt;Option&amp;lt;ProtocolSlice&amp;gt;, serde_json::Error&amp;gt; {&lt;br/&gt;    proposal_record_to_slice(event.kind, &amp;amp;event.tags, &amp;amp;event.content)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Build a Nostr filter that matches perfect_ip packet events.&lt;br/&gt;pub fn perfect_ip_filter(id: Option&amp;lt;&amp;amp;str&amp;gt;) -&amp;gt; Filter {&lt;br/&gt;    let filter = Filter::new()&lt;br/&gt;        .kind(PERFECT_IP_KIND)&lt;br/&gt;        .hashtag(PERFECT_IP_HASHTAG);&lt;br/&gt;&lt;br/&gt;    match id {&lt;br/&gt;        Some(id) =&amp;gt; filter.identifier(id),&lt;br/&gt;        None =&amp;gt; filter,&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Convert a packet slice into a Nostr event builder.&lt;br/&gt;///&lt;br/&gt;/// The slice is serialized into JSON content so relays can store and replay the&lt;br/&gt;/// event without any custom transport logic.&lt;br/&gt;pub fn slice_to_event(slice: &amp;amp;ProtocolSlice) -&amp;gt; Result&amp;lt;EventBuilder, serde_json::Error&amp;gt; {&lt;br/&gt;    Ok(&lt;br/&gt;        EventBuilder::new(PERFECT_IP_KIND, serde_json::to_string(slice)?)&lt;br/&gt;            .tag(Tag::identifier(slice.id.clone()))&lt;br/&gt;            .tag(Tag::hashtag(PERFECT_IP_HASHTAG)),&lt;br/&gt;    )&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Convert a packet batch into Nostr event builders.&lt;br/&gt;pub fn batch_to_events(batch: &amp;amp;PacketBatch) -&amp;gt; Result&amp;lt;Vec&amp;lt;EventBuilder&amp;gt;, serde_json::Error&amp;gt; {&lt;br/&gt;    batch.packets.iter().map(slice_to_event).collect()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn record_to_slice(&lt;br/&gt;    kind: Kind,&lt;br/&gt;    tags: &amp;amp;Tags,&lt;br/&gt;    content: &amp;amp;str,&lt;br/&gt;) -&amp;gt; Result&amp;lt;Option&amp;lt;ProtocolSlice&amp;gt;, serde_json::Error&amp;gt; {&lt;br/&gt;    if kind != PERFECT_IP_KIND {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let Some(identifier) = tags&lt;br/&gt;        .iter()&lt;br/&gt;        .find(|tag| tag.kind().as_str() == &amp;#34;d&amp;#34;)&lt;br/&gt;        .and_then(|tag| tag.content())&lt;br/&gt;    else {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    if !tags&lt;br/&gt;        .iter()&lt;br/&gt;        .any(|tag| tag.kind().as_str() == &amp;#34;t&amp;#34; &amp;amp;&amp;amp; tag.content() == Some(PERFECT_IP_HASHTAG))&lt;br/&gt;    {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let slice: ProtocolSlice = serde_json::from_str(content)?;&lt;br/&gt;    if slice.id != identifier {&lt;br/&gt;        return Ok(None);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(Some(slice))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Try to decode a Nostr event back into a packet slice.&lt;br/&gt;pub fn event_to_slice(event: &amp;amp;Event) -&amp;gt; Result&amp;lt;Option&amp;lt;ProtocolSlice&amp;gt;, serde_json::Error&amp;gt; {&lt;br/&gt;    record_to_slice(event.kind, &amp;amp;event.tags, &amp;amp;event.content)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Try to decode an unsigned Nostr event back into a packet slice.&lt;br/&gt;pub fn unsigned_event_to_slice(&lt;br/&gt;    event: &amp;amp;UnsignedEvent,&lt;br/&gt;) -&amp;gt; Result&amp;lt;Option&amp;lt;ProtocolSlice&amp;gt;, serde_json::Error&amp;gt; {&lt;br/&gt;    record_to_slice(event.kind, &amp;amp;event.tags, &amp;amp;event.content)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Maximum payload size this packet protocol is allowed to emit.&lt;br/&gt;///&lt;br/&gt;/// The tree uses half-sized leaves so that parity slices for sibling pairs stay&lt;br/&gt;/// under the same transport ceiling as the data leaves.&lt;br/&gt;pub const MTU_PAYLOAD: usize = 1460;&lt;br/&gt;const MAX_LEAF_PAYLOAD: usize = MTU_PAYLOAD / 2;&lt;br/&gt;&lt;br/&gt;/// XOR two payloads into a parity buffer.&lt;br/&gt;///&lt;br/&gt;/// Missing bytes are treated as zero so the returned buffer is as long as the&lt;br/&gt;/// larger input. This is the core repair primitive used at every branch in the&lt;br/&gt;/// recursive packet tree.&lt;br/&gt;pub fn calculate_parity(left: &amp;amp;[u8], right: &amp;amp;[u8]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;    let max_len = left.len().max(right.len());&lt;br/&gt;    let mut parity = vec![0; max_len];&lt;br/&gt;    for i in 0..max_len {&lt;br/&gt;        let l = if i &amp;lt; left.len() { left[i] } else { 0 };&lt;br/&gt;        let r = if i &amp;lt; right.len() { right[i] } else { 0 };&lt;br/&gt;        parity[i] = l ^ r;&lt;br/&gt;    }&lt;br/&gt;    parity&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Generate the expected packet ids for a recursive packet tree.&lt;br/&gt;///&lt;br/&gt;/// The returned manifest mirrors [`process_slice`] exactly: leaf nodes are&lt;br/&gt;/// emitted when the payload fits within the leaf payload limit, and internal&lt;br/&gt;/// nodes append `.0`, `.1`, and `.P` entries in tree order. This is what lets&lt;br/&gt;/// [`IntegrityManager`] tell which packets are still missing.&lt;br/&gt;pub fn generate_manifest(id: String, len: usize) -&amp;gt; Vec&amp;lt;String&amp;gt; {&lt;br/&gt;    if len &amp;lt;= MAX_LEAF_PAYLOAD {&lt;br/&gt;        return vec![id];&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let half = len / 2;&lt;br/&gt;    let mut ids = generate_manifest(format!(&amp;#34;{}.0&amp;#34;, id), half);&lt;br/&gt;    ids.append(&amp;amp;mut generate_manifest(format!(&amp;#34;{}.1&amp;#34;, id), len - half));&lt;br/&gt;    ids.push(format!(&amp;#34;{}.P&amp;#34;, id));&lt;br/&gt;    ids&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Recover the missing payload by XORing the sibling payload and parity frame.&lt;br/&gt;///&lt;br/&gt;/// `expected_len` trims the recovered buffer back to the original payload&lt;br/&gt;/// length. If the sibling and parity bytes are from the same branch, the XOR&lt;br/&gt;/// yields the missing sibling exactly.&lt;br/&gt;pub fn recover_missing_data(expected_len: usize, sibling: &amp;amp;[u8], parity: &amp;amp;[u8]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;    let recovered = calculate_parity(sibling, parity);&lt;br/&gt;    recovered.into_iter().take(expected_len).collect()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Rebuild a missing packet using a sibling packet and parity packet.&lt;br/&gt;///&lt;br/&gt;/// The returned slice is marked as data and receives the next sequence number.&lt;br/&gt;/// This is a convenience wrapper for repair handlers that need to synthesize a&lt;br/&gt;/// complete [`ProtocolSlice`] from branch-local evidence.&lt;br/&gt;pub fn recover_missing_slice(&lt;br/&gt;    id: String,&lt;br/&gt;    expected_len: usize,&lt;br/&gt;    sibling: &amp;amp;ProtocolSlice,&lt;br/&gt;    parity: &amp;amp;ProtocolSlice,&lt;br/&gt;    seq: &amp;amp;mut u32,&lt;br/&gt;) -&amp;gt; ProtocolSlice {&lt;br/&gt;    let header = Header {&lt;br/&gt;        seq_num: *seq,&lt;br/&gt;        total_packets: sibling&lt;br/&gt;            .header&lt;br/&gt;            .total_packets&lt;br/&gt;            .max(parity.header.total_packets),&lt;br/&gt;    };&lt;br/&gt;    *seq &#43;= 1;&lt;br/&gt;&lt;br/&gt;    ProtocolSlice {&lt;br/&gt;        id,&lt;br/&gt;        header,&lt;br/&gt;        data: recover_missing_data(expected_len, &amp;amp;sibling.data, &amp;amp;parity.data),&lt;br/&gt;        is_parity: false,&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Recursively split a payload into MTU-safe slices and parity frames.&lt;br/&gt;///&lt;br/&gt;/// Leaf packets are emitted when the payload is at or below the leaf payload&lt;br/&gt;/// limit. Internal nodes emit left, right, and parity frames in&lt;br/&gt;/// a deterministic order so the manifest and sequence numbers line up exactly.&lt;br/&gt;pub fn process_slice(id: String, data: Vec&amp;lt;u8&amp;gt;, seq: &amp;amp;mut u32) -&amp;gt; Vec&amp;lt;ProtocolSlice&amp;gt; {&lt;br/&gt;    if data.len() &amp;lt;= MAX_LEAF_PAYLOAD {&lt;br/&gt;        let slice = ProtocolSlice {&lt;br/&gt;            id,&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: *seq,&lt;br/&gt;                total_packets: 0,&lt;br/&gt;            },&lt;br/&gt;            data,&lt;br/&gt;            is_parity: false,&lt;br/&gt;        };&lt;br/&gt;        *seq &#43;= 1;&lt;br/&gt;        return vec![slice];&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let half = data.len() / 2;&lt;br/&gt;    let left_data = data[..half].to_vec();&lt;br/&gt;    let right_data = data[half..].to_vec();&lt;br/&gt;&lt;br/&gt;    let parity = calculate_parity(&amp;amp;left_data, &amp;amp;right_data);&lt;br/&gt;    let mut slices = process_slice(format!(&amp;#34;{}.0&amp;#34;, id), left_data, seq);&lt;br/&gt;    slices.append(&amp;amp;mut process_slice(format!(&amp;#34;{}.1&amp;#34;, id), right_data, seq));&lt;br/&gt;&lt;br/&gt;    slices.push(ProtocolSlice {&lt;br/&gt;        id: format!(&amp;#34;{}.P&amp;#34;, id),&lt;br/&gt;        header: Header {&lt;br/&gt;            seq_num: *seq,&lt;br/&gt;            total_packets: 0,&lt;br/&gt;        },&lt;br/&gt;        data: parity,&lt;br/&gt;        is_parity: true,&lt;br/&gt;    });&lt;br/&gt;    *seq &#43;= 1;&lt;br/&gt;&lt;br/&gt;    slices&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Packetize a payload and finalize the batch metadata.&lt;br/&gt;///&lt;br/&gt;/// This is the preferred entrypoint for callers that want a ready-to-send&lt;br/&gt;/// packet tree with `total_packets` filled in. The returned batch is ready for&lt;br/&gt;/// logging, persistence, or network repair.&lt;br/&gt;pub fn packetize(id: String, data: Vec&amp;lt;u8&amp;gt;) -&amp;gt; PacketBatch {&lt;br/&gt;    let mut seq = 0;&lt;br/&gt;    let mut packets = process_slice(id, data, &amp;amp;mut seq);&lt;br/&gt;    let total_packets = packets.len() as u32;&lt;br/&gt;    for packet in &amp;amp;mut packets {&lt;br/&gt;        packet.header.total_packets = total_packets;&lt;br/&gt;    }&lt;br/&gt;    PacketBatch {&lt;br/&gt;        total_packets,&lt;br/&gt;        packets,&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Render packet summaries for logs or diagnostics.&lt;br/&gt;///&lt;br/&gt;/// This is intentionally human-facing output for terminal demos and debugging.&lt;br/&gt;/// It keeps the packet ids, sequence numbers, types, and sizes aligned in a&lt;br/&gt;/// single line per packet.&lt;br/&gt;pub fn summarize_packets(packets: &amp;amp;[ProtocolSlice]) -&amp;gt; Vec&amp;lt;String&amp;gt; {&lt;br/&gt;    packets&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|packet| {&lt;br/&gt;            format!(&lt;br/&gt;                &amp;#34;ID: {:&amp;lt;8} | Seq: {:&amp;gt;2}/{} | Type: {:&amp;lt;6} | Size: {}B&amp;#34;,&lt;br/&gt;                packet.id,&lt;br/&gt;                packet.header.seq_num,&lt;br/&gt;                packet.header.total_packets,&lt;br/&gt;                if packet.is_parity { &amp;#34;PARITY&amp;#34; } else { &amp;#34;DATA&amp;#34; },&lt;br/&gt;                packet.data.len()&lt;br/&gt;            )&lt;br/&gt;        })&lt;br/&gt;        .collect()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(test)]&lt;br/&gt;mod tests {&lt;br/&gt;    use std::time::{Duration, SystemTime, UNIX_EPOCH};&lt;br/&gt;&lt;br/&gt;    use hashes::Hash;&lt;br/&gt;    use nostr_sdk::prelude::{&lt;br/&gt;        Client, Event as SdkEvent, Filter as SdkFilter, RelayUrl as SdkRelayUrl,&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    use super::*;&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn process_slice_emits_parity_and_data() {&lt;br/&gt;        let raw_data = vec![0xAB; 2000];&lt;br/&gt;        let batch = packetize(&amp;#34;ROOT&amp;#34;.to_string(), raw_data);&lt;br/&gt;        let packets = batch.packets;&lt;br/&gt;&lt;br/&gt;        assert!(!packets.is_empty());&lt;br/&gt;        assert!(packets.iter().any(|packet| packet.is_parity));&lt;br/&gt;        assert!(packets.iter().any(|packet| packet.id == &amp;#34;ROOT.P&amp;#34;));&lt;br/&gt;        assert!(&lt;br/&gt;            packets&lt;br/&gt;                .iter()&lt;br/&gt;                .any(|packet| packet.id.starts_with(&amp;#34;ROOT.0.&amp;#34;))&lt;br/&gt;        );&lt;br/&gt;        assert!(&lt;br/&gt;            packets&lt;br/&gt;                .iter()&lt;br/&gt;                .any(|packet| packet.id.starts_with(&amp;#34;ROOT.1.&amp;#34;))&lt;br/&gt;        );&lt;br/&gt;        assert!(&lt;br/&gt;            packets&lt;br/&gt;                .iter()&lt;br/&gt;                .all(|packet| packet.data.len() &amp;lt;= MTU_PAYLOAD)&lt;br/&gt;        );&lt;br/&gt;        assert!(&lt;br/&gt;            packets&lt;br/&gt;                .iter()&lt;br/&gt;                .all(|packet| packet.header.total_packets == batch.total_packets)&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn calculate_parity_xors_equal_length_blocks() {&lt;br/&gt;        let left = [0xDE, 0xAD, 0xBE];&lt;br/&gt;        let right = [0x01, 0x02, 0x03];&lt;br/&gt;        assert_eq!(calculate_parity(&amp;amp;left, &amp;amp;right), vec![0xDF, 0xAF, 0xBD]);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn generate_manifest_matches_recursive_packet_tree() {&lt;br/&gt;        let manifest = generate_manifest(&amp;#34;ROOT&amp;#34;.to_string(), 2000);&lt;br/&gt;&lt;br/&gt;        assert!(manifest.contains(&amp;amp;&amp;#34;ROOT.P&amp;#34;.to_string()));&lt;br/&gt;        assert!(manifest.iter().any(|id| id.starts_with(&amp;#34;ROOT.0.&amp;#34;)));&lt;br/&gt;        assert!(manifest.iter().any(|id| id.starts_with(&amp;#34;ROOT.1.&amp;#34;)));&lt;br/&gt;        assert_eq!(&lt;br/&gt;            manifest.len(),&lt;br/&gt;            packetize(&amp;#34;ROOT&amp;#34;.to_string(), vec![0xAB; 2000]).total_packets as usize&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn recover_missing_data_restores_xor_partner() {&lt;br/&gt;        let left = vec![0xDE, 0xAD, 0xBE];&lt;br/&gt;        let right = vec![0x01, 0x02, 0x03];&lt;br/&gt;        let parity = calculate_parity(&amp;amp;left, &amp;amp;right);&lt;br/&gt;&lt;br/&gt;        assert_eq!(recover_missing_data(left.len(), &amp;amp;right, &amp;amp;parity), left);&lt;br/&gt;        assert_eq!(recover_missing_data(right.len(), &amp;amp;left, &amp;amp;parity), right);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn recover_missing_slice_rebuilds_header_and_payload() {&lt;br/&gt;        let sibling = ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.1&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 1,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0x01, 0x02, 0x03],&lt;br/&gt;            is_parity: false,&lt;br/&gt;        };&lt;br/&gt;        let parity = ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.P&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 2,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0xDF, 0xAF, 0xBD],&lt;br/&gt;            is_parity: true,&lt;br/&gt;        };&lt;br/&gt;        let mut seq = 3;&lt;br/&gt;&lt;br/&gt;        let recovered = recover_missing_slice(&amp;#34;ROOT.0&amp;#34;.to_string(), 3, &amp;amp;sibling, &amp;amp;parity, &amp;amp;mut seq);&lt;br/&gt;&lt;br/&gt;        assert_eq!(recovered.id, &amp;#34;ROOT.0&amp;#34;);&lt;br/&gt;        assert_eq!(recovered.data, vec![0xDE, 0xAD, 0xBE]);&lt;br/&gt;        assert_eq!(recovered.header.seq_num, 3);&lt;br/&gt;        assert_eq!(recovered.header.total_packets, 3);&lt;br/&gt;        assert!(!recovered.is_parity);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn get_missing_nodes_returns_unseen_manifest_entries() {&lt;br/&gt;        let mut manager = IntegrityManager::new(vec![&lt;br/&gt;            &amp;#34;ROOT.0&amp;#34;.to_string(),&lt;br/&gt;            &amp;#34;ROOT.1&amp;#34;.to_string(),&lt;br/&gt;            &amp;#34;ROOT.P&amp;#34;.to_string(),&lt;br/&gt;        ]);&lt;br/&gt;        manager.record_slice(ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.0&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 0,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0xDE],&lt;br/&gt;            is_parity: false,&lt;br/&gt;        });&lt;br/&gt;&lt;br/&gt;        let missing = manager.get_missing_nodes();&lt;br/&gt;        assert_eq!(missing.len(), 2);&lt;br/&gt;        assert!(missing.contains(&amp;amp;&amp;#34;ROOT.1&amp;#34;.to_string()));&lt;br/&gt;        assert!(missing.contains(&amp;amp;&amp;#34;ROOT.P&amp;#34;.to_string()));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn verify_integrity_checks_parity_frames() {&lt;br/&gt;        let mut manager = IntegrityManager::new(vec![&lt;br/&gt;            &amp;#34;ROOT.0&amp;#34;.to_string(),&lt;br/&gt;            &amp;#34;ROOT.1&amp;#34;.to_string(),&lt;br/&gt;            &amp;#34;ROOT.P&amp;#34;.to_string(),&lt;br/&gt;        ]);&lt;br/&gt;        let left = ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.0&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 0,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0xDE, 0xAD, 0xBE],&lt;br/&gt;            is_parity: false,&lt;br/&gt;        };&lt;br/&gt;        let right = ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.1&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 1,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0x01, 0x02, 0x03],&lt;br/&gt;            is_parity: false,&lt;br/&gt;        };&lt;br/&gt;        let parity = ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.P&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 2,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: calculate_parity(&amp;amp;left.data, &amp;amp;right.data),&lt;br/&gt;            is_parity: true,&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        manager.record_slice(left);&lt;br/&gt;        manager.record_slice(right);&lt;br/&gt;        manager.record_slice(parity);&lt;br/&gt;        assert!(manager.verify_integrity());&lt;br/&gt;&lt;br/&gt;        let mut corrupted = manager.clone();&lt;br/&gt;        corrupted&lt;br/&gt;            .received_slices&lt;br/&gt;            .get_mut(&amp;#34;ROOT.P&amp;#34;)&lt;br/&gt;            .expect(&amp;#34;parity slice&amp;#34;)&lt;br/&gt;            .data[0] ^= 0xFF;&lt;br/&gt;        assert!(!corrupted.verify_integrity());&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn persist_and_load_from_disk_round_trips_received_slices() {&lt;br/&gt;        let temp_path = std::env::temp_dir().join(format!(&lt;br/&gt;            &amp;#34;nostr-pip-{}-{}.json&amp;#34;,&lt;br/&gt;            std::process::id(),&lt;br/&gt;            Timestamp::now().as_secs()&lt;br/&gt;        ));&lt;br/&gt;        let mut manager = IntegrityManager::new(vec![&lt;br/&gt;            &amp;#34;ROOT.0&amp;#34;.to_string(),&lt;br/&gt;            &amp;#34;ROOT.1&amp;#34;.to_string(),&lt;br/&gt;            &amp;#34;ROOT.P&amp;#34;.to_string(),&lt;br/&gt;        ]);&lt;br/&gt;        manager.record_slice(ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.0&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 0,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0xDE, 0xAD, 0xBE],&lt;br/&gt;            is_parity: false,&lt;br/&gt;        });&lt;br/&gt;        manager.record_slice(ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.P&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 1,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0xAA],&lt;br/&gt;            is_parity: true,&lt;br/&gt;        });&lt;br/&gt;&lt;br/&gt;        manager.persist(&amp;amp;temp_path).expect(&amp;#34;persist&amp;#34;);&lt;br/&gt;        let loaded = IntegrityManager::load_from_disk(&lt;br/&gt;            &amp;amp;temp_path,&lt;br/&gt;            vec![&lt;br/&gt;                &amp;#34;ROOT.0&amp;#34;.to_string(),&lt;br/&gt;                &amp;#34;ROOT.1&amp;#34;.to_string(),&lt;br/&gt;                &amp;#34;ROOT.P&amp;#34;.to_string(),&lt;br/&gt;            ]&lt;br/&gt;            .into_iter()&lt;br/&gt;            .collect(),&lt;br/&gt;        )&lt;br/&gt;        .expect(&amp;#34;load&amp;#34;);&lt;br/&gt;&lt;br/&gt;        assert_eq!(loaded.received_slices.len(), 2);&lt;br/&gt;        assert!(loaded.received_slices.contains_key(&amp;#34;ROOT.0&amp;#34;));&lt;br/&gt;        assert!(loaded.received_slices.contains_key(&amp;#34;ROOT.P&amp;#34;));&lt;br/&gt;        assert!(loaded.get_missing_nodes().contains(&amp;amp;&amp;#34;ROOT.1&amp;#34;.to_string()));&lt;br/&gt;        let _ = std::fs::remove_file(temp_path);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn protocol_slice_json_round_trips_bytes_and_flags() {&lt;br/&gt;        let slice = ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.0.P&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 7,&lt;br/&gt;                total_packets: 63,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0x00, 0xFF, 0x10, 0x20],&lt;br/&gt;            is_parity: true,&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let encoded = serde_json::to_string(&amp;amp;slice).expect(&amp;#34;encode slice&amp;#34;);&lt;br/&gt;        assert!(encoded.contains(&amp;#34;\&amp;#34;id\&amp;#34;:\&amp;#34;ROOT.0.P\&amp;#34;&amp;#34;));&lt;br/&gt;        assert!(encoded.contains(&amp;#34;\&amp;#34;is_parity\&amp;#34;:true&amp;#34;));&lt;br/&gt;        assert!(encoded.contains(&amp;#34;\&amp;#34;data\&amp;#34;:[0,255,16,32]&amp;#34;));&lt;br/&gt;&lt;br/&gt;        let decoded: ProtocolSlice = serde_json::from_str(&amp;amp;encoded).expect(&amp;#34;decode slice&amp;#34;);&lt;br/&gt;        assert_eq!(decoded, slice);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn summarize_packets_shows_packet_inventory_details() {&lt;br/&gt;        let batch = packetize(&amp;#34;ROOT&amp;#34;.to_string(), vec![0xAB; 2000]);&lt;br/&gt;        let lines = summarize_packets(&amp;amp;batch.packets);&lt;br/&gt;&lt;br/&gt;        assert_eq!(lines.len(), batch.total_packets as usize);&lt;br/&gt;        assert!(lines.iter().any(|line| line.contains(&amp;#34;ID: ROOT.P&amp;#34;)));&lt;br/&gt;        assert!(&lt;br/&gt;            lines&lt;br/&gt;                .iter()&lt;br/&gt;                .any(|line| line.contains(&amp;amp;format!(&amp;#34;Seq: {:&amp;gt;2}/{}&amp;#34;, 0, batch.total_packets)))&lt;br/&gt;        );&lt;br/&gt;        assert!(lines.iter().any(|line| line.contains(&amp;#34;Type: DATA&amp;#34;)));&lt;br/&gt;        assert!(lines.iter().any(|line| line.contains(&amp;#34;Type: PARITY&amp;#34;)));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn manifest_packet_ids_point_to_slice_events_only() {&lt;br/&gt;        let payload = vec![0xAB; 64];&lt;br/&gt;        let batch = packetize(&amp;#34;ROOT&amp;#34;.to_string(), payload.clone());&lt;br/&gt;        let keys = Keys::new(&lt;br/&gt;            SecretKey::from_hex(&amp;#34;0000000000000000000000000000000000000000000000000000000000000001&amp;#34;)&lt;br/&gt;                .expect(&amp;#34;secret key&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;        let packet_event_ids: Vec&amp;lt;EventId&amp;gt; = batch&lt;br/&gt;            .packets&lt;br/&gt;            .iter()&lt;br/&gt;            .map(|slice| {&lt;br/&gt;                let event = packet_slice_to_event(slice)&lt;br/&gt;                    .expect(&amp;#34;build packet slice event&amp;#34;)&lt;br/&gt;                    .finalize(&amp;amp;keys)&lt;br/&gt;                    .expect(&amp;#34;finalize slice event&amp;#34;);&lt;br/&gt;                event.id&lt;br/&gt;            })&lt;br/&gt;            .collect();&lt;br/&gt;        let manifest = PacketManifest {&lt;br/&gt;            root: &amp;#34;ROOT&amp;#34;.to_string(),&lt;br/&gt;            sha256: &amp;#34;deadbeef&amp;#34;.to_string(),&lt;br/&gt;            size: payload.len() as u64,&lt;br/&gt;            packets: batch.total_packets as u64,&lt;br/&gt;            depth: 0,&lt;br/&gt;            mtu: MTU_PAYLOAD as u64,&lt;br/&gt;            encoding: &amp;#34;json&amp;#34;.to_string(),&lt;br/&gt;            path: &amp;#34;pip.rs&amp;#34;.to_string(),&lt;br/&gt;        };&lt;br/&gt;        let manifest_event = packet_manifest_to_event(&amp;amp;manifest, &amp;amp;packet_event_ids)&lt;br/&gt;            .expect(&amp;#34;build manifest event&amp;#34;)&lt;br/&gt;            .finalize(&amp;amp;keys)&lt;br/&gt;            .expect(&amp;#34;finalize manifest event&amp;#34;);&lt;br/&gt;&lt;br/&gt;        let refs = manifest_packet_event_ids(&amp;amp;manifest_event);&lt;br/&gt;        assert_eq!(refs.len(), batch.total_packets as usize);&lt;br/&gt;        assert_eq!(refs, packet_event_ids);&lt;br/&gt;        assert!(&lt;br/&gt;            packet_slice_from_event(&amp;amp;manifest_event)&lt;br/&gt;                .expect(&amp;#34;decode manifest&amp;#34;)&lt;br/&gt;                .is_none()&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[tokio::test]&lt;br/&gt;    #[ignore = &amp;#34;live relay test&amp;#34;]&lt;br/&gt;    async fn live_nos_lol_round_trip_reconstructs_and_writes_file() {&lt;br/&gt;        fn reconstruct_payload_from_events(events: &amp;amp;[Event]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;            let mut slices: Vec&amp;lt;_&amp;gt; = events&lt;br/&gt;                .iter()&lt;br/&gt;                .filter_map(|event| packet_slice_from_event(event).expect(&amp;#34;decode packet event&amp;#34;))&lt;br/&gt;                .collect();&lt;br/&gt;            slices.sort_by_key(|slice| slice.header.seq_num);&lt;br/&gt;            slices&lt;br/&gt;                .into_iter()&lt;br/&gt;                .filter(|slice| !slice.is_parity)&lt;br/&gt;                .flat_map(|slice| slice.data)&lt;br/&gt;                .collect()&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let relay_urls = [&lt;br/&gt;            SdkRelayUrl::parse(&amp;#34;wss://nos.lol&amp;#34;).expect(&amp;#34;nos.lol relay url&amp;#34;),&lt;br/&gt;            SdkRelayUrl::parse(&amp;#34;wss://relay.damus.io&amp;#34;).expect(&amp;#34;damus relay url&amp;#34;),&lt;br/&gt;        ];&lt;br/&gt;        let relay = relay_urls[SystemTime::now()&lt;br/&gt;            .duration_since(UNIX_EPOCH)&lt;br/&gt;            .expect(&amp;#34;clock&amp;#34;)&lt;br/&gt;            .subsec_nanos() as usize&lt;br/&gt;            % relay_urls.len()]&lt;br/&gt;        .clone();&lt;br/&gt;        let client = Client::default();&lt;br/&gt;        client&lt;br/&gt;            .add_relay(&amp;amp;relay)&lt;br/&gt;            .and_connect()&lt;br/&gt;            .await&lt;br/&gt;            .expect(&amp;#34;add relay&amp;#34;);&lt;br/&gt;        eprintln!(&amp;#34;perfect_ip live relay test connected: {}&amp;#34;, relay);&lt;br/&gt;&lt;br/&gt;        let keys = Keys::new(&lt;br/&gt;            SecretKey::from_hex(&amp;#34;0000000000000000000000000000000000000000000000000000000000000001&amp;#34;)&lt;br/&gt;                .expect(&amp;#34;secret key&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;        let payload = vec![0xAB; 3000];&lt;br/&gt;        let batch = packetize(&amp;#34;ROOT.live&amp;#34;.to_string(), payload.clone());&lt;br/&gt;        let manifest = PacketManifest {&lt;br/&gt;            root: &amp;#34;ROOT.live&amp;#34;.to_string(),&lt;br/&gt;            sha256: hashes::sha256::Hash::hash(&amp;amp;payload).to_string(),&lt;br/&gt;            size: payload.len() as u64,&lt;br/&gt;            packets: batch.total_packets as u64,&lt;br/&gt;            depth: 0,&lt;br/&gt;            mtu: MTU_PAYLOAD as u64,&lt;br/&gt;            encoding: &amp;#34;json&amp;#34;.to_string(),&lt;br/&gt;            path: &amp;#34;pip.rs&amp;#34;.to_string(),&lt;br/&gt;        };&lt;br/&gt;        eprintln!(&lt;br/&gt;            &amp;#34;perfect_ip live relay test prepared payload: bytes={}, packets={}&amp;#34;,&lt;br/&gt;            payload.len(),&lt;br/&gt;            batch.total_packets&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let mut packet_event_ids = Vec::with_capacity(batch.total_packets as usize);&lt;br/&gt;        let mut sdk_packet_event_ids = Vec::with_capacity(batch.total_packets as usize);&lt;br/&gt;        let mut packet_events = Vec::with_capacity(batch.total_packets as usize);&lt;br/&gt;        for slice in &amp;amp;batch.packets {&lt;br/&gt;            let event = packet_slice_to_event(slice)&lt;br/&gt;                .expect(&amp;#34;build slice event&amp;#34;)&lt;br/&gt;                .finalize(&amp;amp;keys)&lt;br/&gt;                .expect(&amp;#34;finalize slice event&amp;#34;);&lt;br/&gt;            packet_event_ids.push(event.id);&lt;br/&gt;            let sdk_event: SdkEvent =&lt;br/&gt;                serde_json::from_str(&amp;amp;event.as_json()).expect(&amp;#34;sdk slice event&amp;#34;);&lt;br/&gt;            sdk_packet_event_ids.push(sdk_event.id);&lt;br/&gt;            eprintln!(&amp;#34;perfect_ip live relay test publishing slice: {}&amp;#34;, event.id);&lt;br/&gt;            let send_result = client&lt;br/&gt;                .send_event(&amp;amp;sdk_event)&lt;br/&gt;                .to([&amp;amp;relay])&lt;br/&gt;                .await&lt;br/&gt;                .expect(&amp;#34;send slice event&amp;#34;);&lt;br/&gt;            eprintln!(&lt;br/&gt;                &amp;#34;perfect_ip live relay test slice send result: id={}, success={}, failed={}&amp;#34;,&lt;br/&gt;                send_result.id(),&lt;br/&gt;                send_result.success.len(),&lt;br/&gt;                send_result.failed.len()&lt;br/&gt;            );&lt;br/&gt;            if !send_result.failed.is_empty() {&lt;br/&gt;                eprintln!(&lt;br/&gt;                    &amp;#34;perfect_ip live relay test slice send failures: {:?}&amp;#34;,&lt;br/&gt;                    send_result.failed&lt;br/&gt;                );&lt;br/&gt;            }&lt;br/&gt;            packet_events.push(event);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let manifest_event = packet_manifest_to_event(&amp;amp;manifest, &amp;amp;packet_event_ids)&lt;br/&gt;            .expect(&amp;#34;build manifest event&amp;#34;)&lt;br/&gt;            .finalize(&amp;amp;keys)&lt;br/&gt;            .expect(&amp;#34;finalize manifest event&amp;#34;);&lt;br/&gt;        let sdk_manifest_event: SdkEvent =&lt;br/&gt;            serde_json::from_str(&amp;amp;manifest_event.as_json()).expect(&amp;#34;sdk manifest event&amp;#34;);&lt;br/&gt;        eprintln!(&lt;br/&gt;            &amp;#34;perfect_ip live relay test publishing manifest: {}&amp;#34;,&lt;br/&gt;            manifest_event.id&lt;br/&gt;        );&lt;br/&gt;        let manifest_send_result = client&lt;br/&gt;            .send_event(&amp;amp;sdk_manifest_event)&lt;br/&gt;            .to([&amp;amp;relay])&lt;br/&gt;            .await&lt;br/&gt;            .expect(&amp;#34;send manifest event&amp;#34;);&lt;br/&gt;        eprintln!(&lt;br/&gt;            &amp;#34;perfect_ip live relay test manifest send result: id={}, success={}, failed={}&amp;#34;,&lt;br/&gt;            manifest_send_result.id(),&lt;br/&gt;            manifest_send_result.success.len(),&lt;br/&gt;            manifest_send_result.failed.len()&lt;br/&gt;        );&lt;br/&gt;        if !manifest_send_result.failed.is_empty() {&lt;br/&gt;            eprintln!(&lt;br/&gt;                &amp;#34;perfect_ip live relay test manifest send failures: {:?}&amp;#34;,&lt;br/&gt;                manifest_send_result.failed&lt;br/&gt;            );&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        tokio::time::sleep(Duration::from_secs(2)).await;&lt;br/&gt;&lt;br/&gt;        let fetched_manifest = tokio::time::timeout(Duration::from_secs(20), async {&lt;br/&gt;            let mut attempts = 0usize;&lt;br/&gt;            loop {&lt;br/&gt;                attempts &#43;= 1;&lt;br/&gt;                eprintln!(&lt;br/&gt;                    &amp;#34;perfect_ip live relay test fetching manifest attempt {}&amp;#34;,&lt;br/&gt;                    attempts&lt;br/&gt;                );&lt;br/&gt;                let fetched = client&lt;br/&gt;                    .fetch_events(SdkFilter::new().ids([sdk_manifest_event.id]))&lt;br/&gt;                    .await&lt;br/&gt;                    .expect(&amp;#34;fetch manifest&amp;#34;);&lt;br/&gt;                if let Some(event) = fetched.first().cloned() {&lt;br/&gt;                    eprintln!(&lt;br/&gt;                        &amp;#34;perfect_ip live relay test fetched manifest after {} attempts&amp;#34;,&lt;br/&gt;                        attempts&lt;br/&gt;                    );&lt;br/&gt;                    break event;&lt;br/&gt;                }&lt;br/&gt;                tokio::time::sleep(Duration::from_secs(1)).await;&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;        .await&lt;br/&gt;        .expect(&amp;#34;manifest fetch timeout&amp;#34;);&lt;br/&gt;        let fetched_manifest: Event =&lt;br/&gt;            serde_json::from_str(&amp;amp;fetched_manifest.as_json()).expect(&amp;#34;nostr manifest event&amp;#34;);&lt;br/&gt;        let packet_ids = manifest_packet_event_ids(&amp;amp;fetched_manifest);&lt;br/&gt;        assert_eq!(packet_ids, packet_event_ids);&lt;br/&gt;&lt;br/&gt;        let relay_handle = client.relays().await.remove(&amp;amp;relay).expect(&amp;#34;relay handle&amp;#34;);&lt;br/&gt;        let fetched_packets = tokio::time::timeout(Duration::from_secs(20), async {&lt;br/&gt;            let mut attempts = 0usize;&lt;br/&gt;            loop {&lt;br/&gt;                attempts &#43;= 1;&lt;br/&gt;                eprintln!(&lt;br/&gt;                    &amp;#34;perfect_ip live relay test fetching packets attempt {}&amp;#34;,&lt;br/&gt;                    attempts&lt;br/&gt;                );&lt;br/&gt;                let fetched = relay_handle&lt;br/&gt;                    .fetch_events(SdkFilter::new().ids(sdk_packet_event_ids.clone()))&lt;br/&gt;                    .await&lt;br/&gt;                    .expect(&amp;#34;fetch packets&amp;#34;);&lt;br/&gt;                if fetched.len() &amp;gt;= packet_event_ids.len() {&lt;br/&gt;                    eprintln!(&lt;br/&gt;                        &amp;#34;perfect_ip live relay test fetched packets after {} attempts: {} / {}&amp;#34;,&lt;br/&gt;                        attempts,&lt;br/&gt;                        fetched.len(),&lt;br/&gt;                        packet_event_ids.len()&lt;br/&gt;                    );&lt;br/&gt;                    break fetched;&lt;br/&gt;                }&lt;br/&gt;                tokio::time::sleep(Duration::from_secs(1)).await;&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;        .await&lt;br/&gt;        .expect(&amp;#34;packet fetch timeout&amp;#34;);&lt;br/&gt;        let fetched_packets: Vec&amp;lt;Event&amp;gt; = fetched_packets&lt;br/&gt;            .into_iter()&lt;br/&gt;            .map(|event| serde_json::from_str(&amp;amp;event.as_json()).expect(&amp;#34;nostr packet event&amp;#34;))&lt;br/&gt;            .collect();&lt;br/&gt;        let reconstructed = reconstruct_payload_from_events(&amp;amp;fetched_packets);&lt;br/&gt;        assert_eq!(reconstructed, payload);&lt;br/&gt;&lt;br/&gt;        let out_dir = std::env::temp_dir().join(&amp;#34;nostr-pip-live-out&amp;#34;);&lt;br/&gt;        let output_path = out_dir.join(&lt;br/&gt;            Path::new(&amp;amp;manifest.path)&lt;br/&gt;                .file_name()&lt;br/&gt;                .expect(&amp;#34;manifest file&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;        if let Some(parent) = output_path.parent() {&lt;br/&gt;            std::fs::create_dir_all(parent).expect(&amp;#34;create output dir&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;        std::fs::write(&amp;amp;output_path, &amp;amp;reconstructed).expect(&amp;#34;write output&amp;#34;);&lt;br/&gt;        let written = std::fs::read(&amp;amp;output_path).expect(&amp;#34;read output&amp;#34;);&lt;br/&gt;        assert_eq!(written, payload);&lt;br/&gt;&lt;br/&gt;        eprintln!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip live relay round trip&amp;#34;,&lt;br/&gt;                &amp;#34;relay&amp;#34;: relay.as_str(),&lt;br/&gt;                &amp;#34;manifest_event&amp;#34;: manifest_event.id.to_string(),&lt;br/&gt;                &amp;#34;packets&amp;#34;: packet_ids.len(),&lt;br/&gt;                &amp;#34;bytes&amp;#34;: written.len(),&lt;br/&gt;                &amp;#34;out&amp;#34;: output_path.display().to_string(),&lt;br/&gt;                &amp;#34;matches&amp;#34;: written == payload,&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        client.shutdown().await;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn dump_full_protocol_for_nocapture() {&lt;br/&gt;        use hashes::Hash;&lt;br/&gt;        use hashes::sha1::Hash as Sha1Hash;&lt;br/&gt;&lt;br/&gt;        fn reconstruct_payload_for_test(packets: &amp;amp;[ProtocolSlice]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;            let mut leaves: Vec&amp;lt;_&amp;gt; = packets.iter().filter(|packet| !packet.is_parity).collect();&lt;br/&gt;            leaves.sort_by_key(|packet| packet.header.seq_num);&lt;br/&gt;            leaves&lt;br/&gt;                .into_iter()&lt;br/&gt;                .flat_map(|packet| packet.data.clone())&lt;br/&gt;                .collect()&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        #[derive(Default)]&lt;br/&gt;        struct MockRelay {&lt;br/&gt;            online: bool,&lt;br/&gt;            dropped: usize,&lt;br/&gt;            events: Vec&amp;lt;UnsignedEvent&amp;gt;,&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        impl MockRelay {&lt;br/&gt;            fn set_online(&amp;amp;mut self, online: bool) {&lt;br/&gt;                self.online = online;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            fn publish_if_online(&amp;amp;mut self, event: UnsignedEvent) -&amp;gt; bool {&lt;br/&gt;                if self.online {&lt;br/&gt;                    self.events.push(event);&lt;br/&gt;                    true&lt;br/&gt;                } else {&lt;br/&gt;                    self.dropped &#43;= 1;&lt;br/&gt;                    false&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            fn packet_slices(&amp;amp;self) -&amp;gt; Vec&amp;lt;ProtocolSlice&amp;gt; {&lt;br/&gt;                self.events&lt;br/&gt;                    .iter()&lt;br/&gt;                    .filter(|event| event.kind == PERFECT_IP_KIND)&lt;br/&gt;                    .map(|event| {&lt;br/&gt;                        unsigned_event_to_slice(event)&lt;br/&gt;                            .expect(&amp;#34;decode packet event&amp;#34;)&lt;br/&gt;                            .expect(&amp;#34;packet event&amp;#34;)&lt;br/&gt;                    })&lt;br/&gt;                    .filter(|slice| !slice.is_parity)&lt;br/&gt;                    .collect()&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            fn packet_records(&amp;amp;self) -&amp;gt; Vec&amp;lt;ProtocolSlice&amp;gt; {&lt;br/&gt;                self.events&lt;br/&gt;                    .iter()&lt;br/&gt;                    .filter(|event| event.kind == PERFECT_IP_KIND)&lt;br/&gt;                    .map(|event| {&lt;br/&gt;                        unsigned_event_to_slice(event)&lt;br/&gt;                            .expect(&amp;#34;decode packet event&amp;#34;)&lt;br/&gt;                            .expect(&amp;#34;packet event&amp;#34;)&lt;br/&gt;                    })&lt;br/&gt;                    .collect()&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            fn publish(&amp;amp;mut self, event: UnsignedEvent) {&lt;br/&gt;                self.events.push(event);&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            fn clone_snapshot(&amp;amp;self) -&amp;gt; Vec&amp;lt;ProtocolSlice&amp;gt; {&lt;br/&gt;                let mut slices: Vec&amp;lt;ProtocolSlice&amp;gt; = self.packet_slices();&lt;br/&gt;                slices.sort_by_key(|slice| slice.header.seq_num);&lt;br/&gt;                slices&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            fn clone_snapshot_from_template(&lt;br/&gt;                &amp;amp;self,&lt;br/&gt;                template: &amp;amp;[ProtocolSlice],&lt;br/&gt;            ) -&amp;gt; Vec&amp;lt;ProtocolSlice&amp;gt; {&lt;br/&gt;                let slices = self.packet_slices();&lt;br/&gt;                let map: std::collections::HashMap&amp;lt;String, ProtocolSlice&amp;gt; = slices&lt;br/&gt;                    .into_iter()&lt;br/&gt;                    .map(|slice| (slice.id.clone(), slice))&lt;br/&gt;                    .collect();&lt;br/&gt;&lt;br/&gt;                template&lt;br/&gt;                    .iter()&lt;br/&gt;                    .filter(|packet| !packet.is_parity)&lt;br/&gt;                    .filter_map(|packet| map.get(&amp;amp;packet.id).cloned())&lt;br/&gt;                    .collect()&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let payload = vec![0xAB; 3000];&lt;br/&gt;        let batch = packetize(&amp;#34;ROOT&amp;#34;.to_string(), payload.clone());&lt;br/&gt;        let manifest = generate_manifest(&amp;#34;ROOT&amp;#34;.to_string(), payload.len());&lt;br/&gt;        let mut manager = IntegrityManager::new(manifest.clone());&lt;br/&gt;        let demo_keys = Keys::new(&lt;br/&gt;            SecretKey::from_hex(&amp;#34;0000000000000000000000000000000000000000000000000000000000000001&amp;#34;)&lt;br/&gt;                .expect(&amp;#34;secret key&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;perfect_ip manifest ({} ids):&amp;#34;, manifest.len());&lt;br/&gt;        for id in &amp;amp;manifest {&lt;br/&gt;            println!(&amp;#34;  {id}&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip packet inventory ({} packets):&amp;#34;,&lt;br/&gt;            batch.total_packets&lt;br/&gt;        );&lt;br/&gt;        for line in summarize_packets(&amp;amp;batch.packets) {&lt;br/&gt;            println!(&amp;#34;{line}&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;perfect_ip packet batch json:&amp;#34;);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::to_string(&amp;amp;batch).expect(&amp;#34;serialize packet batch&amp;#34;)&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let event_builders = batch_to_events(&amp;amp;batch).expect(&amp;#34;build nostr events&amp;#34;);&lt;br/&gt;        println!(&amp;#34;perfect_ip nostr event count: {}&amp;#34;, event_builders.len());&lt;br/&gt;        let preview_event = event_builders&lt;br/&gt;            .first()&lt;br/&gt;            .expect(&amp;#34;first nostr event&amp;#34;)&lt;br/&gt;            .clone()&lt;br/&gt;            .finalize_unsigned(demo_keys.public_key());&lt;br/&gt;        println!(&amp;#34;perfect_ip nostr preview kind: {:?}&amp;#34;, preview_event.kind);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip nostr preview content: {}&amp;#34;,&lt;br/&gt;            preview_event.content&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip nostr preview tags: {:?}&amp;#34;,&lt;br/&gt;            preview_event&lt;br/&gt;                .tags&lt;br/&gt;                .iter()&lt;br/&gt;                .map(|tag| tag.as_slice().to_vec())&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip nostr preview decoded: {:?}&amp;#34;,&lt;br/&gt;            unsigned_event_to_slice(&amp;amp;preview_event).expect(&amp;#34;decode preview event&amp;#34;)&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;perfect_ip first packet json:&amp;#34;);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::to_string(&amp;amp;batch.packets.first().expect(&amp;#34;first packet&amp;#34;))&lt;br/&gt;                .expect(&amp;#34;serialize first packet&amp;#34;)&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;perfect_ip last packet json:&amp;#34;);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::to_string(&amp;amp;batch.packets.last().expect(&amp;#34;last packet&amp;#34;))&lt;br/&gt;                .expect(&amp;#34;serialize last packet&amp;#34;)&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        for slice in batch.packets.clone() {&lt;br/&gt;            manager.record_slice(slice);&lt;br/&gt;        }&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip missing nodes: {:?}&amp;#34;,&lt;br/&gt;            manager.get_missing_nodes()&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip integrity verified: {}&amp;#34;,&lt;br/&gt;            manager.verify_integrity()&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let reconstructed = reconstruct_payload_for_test(&amp;amp;batch.packets);&lt;br/&gt;        println!(&amp;#34;perfect_ip reconstructed bytes: {}&amp;#34;, reconstructed.len());&lt;br/&gt;        println!(&amp;#34;perfect_ip payload matches: {}&amp;#34;, reconstructed == payload);&lt;br/&gt;        assert_eq!(reconstructed, payload);&lt;br/&gt;&lt;br/&gt;        let mut relay = MockRelay::default();&lt;br/&gt;        let repo_id = &amp;#34;perfect-ip-demo&amp;#34;;&lt;br/&gt;        let owner_public_key = demo_keys.public_key();&lt;br/&gt;        let repo_announcement = GitRepositoryAnnouncement {&lt;br/&gt;            id: repo_id.to_string(),&lt;br/&gt;            name: Some(&amp;#34;perfect_ip demo repo&amp;#34;.to_string()),&lt;br/&gt;            description: Some(&amp;#34;Mock NIP-34 repo push carried over perfect_ip&amp;#34;.to_string()),&lt;br/&gt;            web: vec![Url::parse(&amp;#34;&lt;a href=&#34;https://example.com/perfect-ip-demo&amp;#34&#34;&gt;https://example.com/perfect-ip-demo&amp;#34&lt;/a&gt;;).expect(&amp;#34;demo web url&amp;#34;)],&lt;br/&gt;            clone: vec![&lt;br/&gt;                Url::parse(&amp;#34;&lt;a href=&#34;https://example.com/perfect-ip-demo.git&amp;#34&#34;&gt;https://example.com/perfect-ip-demo.git&amp;#34&lt;/a&gt;;).expect(&amp;#34;demo clone url&amp;#34;),&lt;br/&gt;            ],&lt;br/&gt;            relays: vec![RelayUrl::parse(&amp;#34;wss://relay.damus.io&amp;#34;).expect(&amp;#34;demo relay url&amp;#34;)],&lt;br/&gt;            euc: Some(Sha1Hash::hash(payload.as_slice())),&lt;br/&gt;            maintainers: vec![owner_public_key],&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let repo_event = EventBuilder::git_repository_announcement(repo_announcement.clone())&lt;br/&gt;            .finalize_unsigned(owner_public_key);&lt;br/&gt;        relay.publish(repo_event.clone());&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip git repo announcement kind: {:?}&amp;#34;,&lt;br/&gt;            repo_event.kind&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip git repo announcement tags: {:?}&amp;#34;,&lt;br/&gt;            repo_event&lt;br/&gt;                .tags&lt;br/&gt;                .iter()&lt;br/&gt;                .map(|tag| tag.as_slice().to_vec())&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let repository =&lt;br/&gt;            Coordinate::new(Kind::GitRepoAnnouncement, owner_public_key).identifier(repo_id);&lt;br/&gt;        let snapshot_hash = Sha1Hash::hash(payload.as_slice());&lt;br/&gt;        let patch = GitPatch {&lt;br/&gt;            repository,&lt;br/&gt;            content: GitPatchContent::CoverLetter {&lt;br/&gt;                title: &amp;#34;perfect_ip repo snapshot&amp;#34;.to_string(),&lt;br/&gt;                description: format!(&lt;br/&gt;                    &amp;#34;Mock relay upload: {} bytes split into {} perfect_ip packets&amp;#34;,&lt;br/&gt;                    payload.len(),&lt;br/&gt;                    batch.total_packets&lt;br/&gt;                ),&lt;br/&gt;                last_commit: snapshot_hash,&lt;br/&gt;                commits_len: batch.total_packets as usize,&lt;br/&gt;            },&lt;br/&gt;            euc: snapshot_hash,&lt;br/&gt;            labels: vec![&amp;#34;perfect_ip&amp;#34;.to_string(), &amp;#34;mock-relay&amp;#34;.to_string()],&lt;br/&gt;        };&lt;br/&gt;        let patch_event = EventBuilder::git_patch(patch)&lt;br/&gt;            .expect(&amp;#34;build git patch&amp;#34;)&lt;br/&gt;            .finalize_unsigned(owner_public_key);&lt;br/&gt;        relay.publish(patch_event.clone());&lt;br/&gt;        println!(&amp;#34;perfect_ip git patch kind: {:?}&amp;#34;, patch_event.kind);&lt;br/&gt;        println!(&amp;#34;perfect_ip git patch content: {}&amp;#34;, patch_event.content);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip git patch tags: {:?}&amp;#34;,&lt;br/&gt;            patch_event&lt;br/&gt;                .tags&lt;br/&gt;                .iter()&lt;br/&gt;                .map(|tag| tag.as_slice().to_vec())&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip git clone source: {}&amp;#34;,&lt;br/&gt;            repo_announcement.clone.first().expect(&amp;#34;clone url&amp;#34;)&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let packet_events: Vec&amp;lt;UnsignedEvent&amp;gt; = batch_to_events(&amp;amp;batch)&lt;br/&gt;            .expect(&amp;#34;build packet events&amp;#34;)&lt;br/&gt;            .into_iter()&lt;br/&gt;            .map(|builder| builder.finalize_unsigned(owner_public_key))&lt;br/&gt;            .collect();&lt;br/&gt;        relay.events.extend(packet_events.clone());&lt;br/&gt;        println!(&amp;#34;perfect_ip git relay event count: {}&amp;#34;, relay.events.len());&lt;br/&gt;&lt;br/&gt;        let cloned_snapshot: Vec&amp;lt;u8&amp;gt; = relay&lt;br/&gt;            .clone_snapshot()&lt;br/&gt;            .into_iter()&lt;br/&gt;            .flat_map(|slice| slice.data)&lt;br/&gt;            .collect();&lt;br/&gt;        println!(&amp;#34;perfect_ip git clone bytes: {}&amp;#34;, cloned_snapshot.len());&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip git clone matches: {}&amp;#34;,&lt;br/&gt;            cloned_snapshot == payload&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(cloned_snapshot, payload);&lt;br/&gt;&lt;br/&gt;        let mut repair_relay = MockRelay::default();&lt;br/&gt;        repair_relay.set_online(true);&lt;br/&gt;        let repair_target_id = &amp;#34;ROOT.0.1.1&amp;#34;;&lt;br/&gt;        let repair_target = batch&lt;br/&gt;            .packets&lt;br/&gt;            .iter()&lt;br/&gt;            .find(|packet| packet.id == repair_target_id)&lt;br/&gt;            .expect(&amp;#34;repair target&amp;#34;);&lt;br/&gt;        let repair_sibling = batch&lt;br/&gt;            .packets&lt;br/&gt;            .iter()&lt;br/&gt;            .find(|packet| packet.id == &amp;#34;ROOT.0.1.0&amp;#34;)&lt;br/&gt;            .expect(&amp;#34;repair sibling&amp;#34;);&lt;br/&gt;        let repair_parity = batch&lt;br/&gt;            .packets&lt;br/&gt;            .iter()&lt;br/&gt;            .find(|packet| packet.id == &amp;#34;ROOT.0.1.P&amp;#34;)&lt;br/&gt;            .expect(&amp;#34;repair parity&amp;#34;);&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;perfect_ip repair target: {}&amp;#34;, repair_target_id);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip repair target&amp;#34;,&lt;br/&gt;                &amp;#34;target&amp;#34;: repair_target_id,&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;        for (packet, builder) in batch&lt;br/&gt;            .packets&lt;br/&gt;            .iter()&lt;br/&gt;            .zip(batch_to_events(&amp;amp;batch).expect(&amp;#34;event builders&amp;#34;))&lt;br/&gt;        {&lt;br/&gt;            let event = builder.finalize_unsigned(owner_public_key);&lt;br/&gt;            if packet.id == repair_target_id {&lt;br/&gt;                repair_relay.set_online(false);&lt;br/&gt;                let accepted = repair_relay.publish_if_online(event);&lt;br/&gt;                println!(&lt;br/&gt;                    &amp;#34;perfect_ip relay outage dropped {}: {}&amp;#34;,&lt;br/&gt;                    repair_target_id, !accepted&lt;br/&gt;                );&lt;br/&gt;                println!(&lt;br/&gt;                    &amp;#34;{}&amp;#34;,&lt;br/&gt;                    serde_json::json!({&lt;br/&gt;                        &amp;#34;event&amp;#34;: &amp;#34;perfect_ip relay outage dropped&amp;#34;,&lt;br/&gt;                        &amp;#34;target&amp;#34;: repair_target_id,&lt;br/&gt;                        &amp;#34;dropped&amp;#34;: !accepted,&lt;br/&gt;                    })&lt;br/&gt;                );&lt;br/&gt;                repair_relay.set_online(true);&lt;br/&gt;                continue;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let _ = repair_relay.publish_if_online(event);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let partial_manager = {&lt;br/&gt;            let mut manager = IntegrityManager::new(manifest.clone());&lt;br/&gt;            for slice in repair_relay.packet_records() {&lt;br/&gt;                manager.record_slice(slice);&lt;br/&gt;            }&lt;br/&gt;            manager&lt;br/&gt;        };&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip partial missing nodes: {:?}&amp;#34;,&lt;br/&gt;            partial_manager.get_missing_nodes()&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip partial missing nodes&amp;#34;,&lt;br/&gt;                &amp;#34;nodes&amp;#34;: partial_manager.get_missing_nodes(),&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let mut repair_seq = repair_target.header.seq_num;&lt;br/&gt;        let repaired_slice = recover_missing_slice(&lt;br/&gt;            repair_target.id.clone(),&lt;br/&gt;            repair_target.data.len(),&lt;br/&gt;            repair_sibling,&lt;br/&gt;            repair_parity,&lt;br/&gt;            &amp;amp;mut repair_seq,&lt;br/&gt;        );&lt;br/&gt;        let repair_event = slice_to_event(&amp;amp;repaired_slice)&lt;br/&gt;            .expect(&amp;#34;serialize repair slice&amp;#34;)&lt;br/&gt;            .finalize_unsigned(owner_public_key);&lt;br/&gt;        repair_relay.publish(repair_event);&lt;br/&gt;        println!(&amp;#34;perfect_ip repair event published: {}&amp;#34;, repaired_slice.id);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip repair event published&amp;#34;,&lt;br/&gt;                &amp;#34;target&amp;#34;: repaired_slice.id,&lt;br/&gt;                &amp;#34;seq_num&amp;#34;: repaired_slice.header.seq_num,&lt;br/&gt;                &amp;#34;total_packets&amp;#34;: repaired_slice.header.total_packets,&lt;br/&gt;                &amp;#34;is_parity&amp;#34;: repaired_slice.is_parity,&lt;br/&gt;                &amp;#34;bytes&amp;#34;: repaired_slice.data.len(),&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let repaired_snapshot = repair_relay.clone_snapshot_from_template(&amp;amp;batch.packets);&lt;br/&gt;        let repaired_payload: Vec&amp;lt;u8&amp;gt; = repaired_snapshot&lt;br/&gt;            .iter()&lt;br/&gt;            .flat_map(|slice| slice.data.clone())&lt;br/&gt;            .collect();&lt;br/&gt;        let repaired_matches = repaired_payload == payload;&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip repaired relay dropped events: {}&amp;#34;,&lt;br/&gt;            repair_relay.dropped&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip repaired relay dropped events&amp;#34;,&lt;br/&gt;                &amp;#34;dropped_events&amp;#34;: repair_relay.dropped,&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip repaired relay bytes: {}&amp;#34;,&lt;br/&gt;            repaired_payload.len()&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip repaired relay bytes&amp;#34;,&lt;br/&gt;                &amp;#34;bytes&amp;#34;: repaired_payload.len(),&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;        println!(&amp;#34;perfect_ip repaired relay matches: {}&amp;#34;, repaired_matches);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip repaired relay matches&amp;#34;,&lt;br/&gt;                &amp;#34;matches&amp;#34;: repaired_matches,&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip repaired relay summary&amp;#34;,&lt;br/&gt;                &amp;#34;dropped_events&amp;#34;: repair_relay.dropped,&lt;br/&gt;                &amp;#34;bytes&amp;#34;: repaired_payload.len(),&lt;br/&gt;                &amp;#34;matches&amp;#34;: repaired_matches,&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(repaired_payload, payload);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn perfect_ip_event_round_trips_through_nostr_content() {&lt;br/&gt;        let slice = ProtocolSlice {&lt;br/&gt;            id: &amp;#34;ROOT.0&amp;#34;.to_string(),&lt;br/&gt;            header: Header {&lt;br/&gt;                seq_num: 1,&lt;br/&gt;                total_packets: 3,&lt;br/&gt;            },&lt;br/&gt;            data: vec![0xDE, 0xAD, 0xBE, 0xEF],&lt;br/&gt;            is_parity: false,&lt;br/&gt;        };&lt;br/&gt;        let keys = Keys::new(&lt;br/&gt;            SecretKey::from_hex(&amp;#34;0000000000000000000000000000000000000000000000000000000000000001&amp;#34;)&lt;br/&gt;                .expect(&amp;#34;secret key&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let event = slice_to_event(&amp;amp;slice)&lt;br/&gt;            .expect(&amp;#34;serialize slice&amp;#34;)&lt;br/&gt;            .finalize_unsigned(keys.public_key());&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;perfect_ip nostr event kind: {:?}&amp;#34;, event.kind);&lt;br/&gt;        println!(&amp;#34;perfect_ip nostr event content: {}&amp;#34;, event.content);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip nostr event tags: {:?}&amp;#34;,&lt;br/&gt;            event&lt;br/&gt;                .tags&lt;br/&gt;                .iter()&lt;br/&gt;                .map(|tag| tag.as_slice().to_vec())&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let decoded = unsigned_event_to_slice(&amp;amp;event)&lt;br/&gt;            .expect(&amp;#34;decode event&amp;#34;)&lt;br/&gt;            .expect(&amp;#34;perfect_ip event&amp;#34;);&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;perfect_ip decoded slice id: {}&amp;#34;, decoded.id);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip decoded slice json: {}&amp;#34;,&lt;br/&gt;            serde_json::to_string(&amp;amp;decoded).expect(&amp;#34;serialize decoded slice&amp;#34;)&lt;br/&gt;        );&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;perfect_ip nostr filter: {}&amp;#34;,&lt;br/&gt;            serde_json::to_string(&amp;amp;perfect_ip_filter(Some(&amp;#34;ROOT.0&amp;#34;))).expect(&amp;#34;serialize filter&amp;#34;)&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        assert_eq!(decoded, slice);&lt;br/&gt;        assert_eq!(event.kind, PERFECT_IP_KIND);&lt;br/&gt;        assert!(&lt;br/&gt;            event&lt;br/&gt;                .tags&lt;br/&gt;                .iter()&lt;br/&gt;                .any(|tag| tag.kind().as_str() == &amp;#34;d&amp;#34; &amp;amp;&amp;amp; tag.content() == Some(&amp;#34;ROOT.0&amp;#34;))&lt;br/&gt;        );&lt;br/&gt;        assert!(&lt;br/&gt;            event&lt;br/&gt;                .tags&lt;br/&gt;                .iter()&lt;br/&gt;                .any(|tag| tag.kind().as_str() == &amp;#34;t&amp;#34; &amp;amp;&amp;amp; tag.content() == Some(PERFECT_IP_HASHTAG))&lt;br/&gt;        );&lt;br/&gt;        assert!(&lt;br/&gt;            perfect_ip_filter(Some(&amp;#34;ROOT.0&amp;#34;))&lt;br/&gt;                .kinds&lt;br/&gt;                .unwrap()&lt;br/&gt;                .contains(&amp;amp;PERFECT_IP_KIND)&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:52Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs2qav0m0cr9jz6pp6yx8dksjy3fxxy5ryx3480jsaqcsenezxq7sqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jfhcwjf</id>
    
      <title type="html">Relay URL,Latitude,Longitude wot.nostr.party,36.1627,-86.7816 ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs2qav0m0cr9jz6pp6yx8dksjy3fxxy5ryx3480jsaqcsenezxq7sqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jfhcwjf" />
    <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-10-09T01:54:52Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqszkkeqjuc287f7qte4hdaagxxe24z6cytcdt34eyuwm7s7ls3urwqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jqf2d9h</id>
    
      <title type="html">//! A crate providing the `get_file_hash!` procedural macro. //! ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqszkkeqjuc287f7qte4hdaagxxe24z6cytcdt34eyuwm7s7ls3urwqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jqf2d9h" />
    <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;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;use nostr_sdk::{Event, EventBuilder, EventId, Keys, Tag, UnsignedEvent};&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;const DEFAULT_MAX_POW_ATTEMPTS: u128 = 100_000_000;&lt;br/&gt;&lt;br/&gt;/// Mine a Nostr event whose ID starts with the given lowercase-hex prefix.&lt;br/&gt;///&lt;br/&gt;/// The SDK&amp;#39;s [`EventBuilder::pow`](nostr_sdk::EventBuilder::pow) only supports&lt;br/&gt;/// NIP-13 leading-zero-bits difficulty. This helper does custom-prefix mining&lt;br/&gt;/// by brute-forcing a `[&amp;#34;nonce&amp;#34;, &amp;#34;&amp;lt;nonce&amp;gt;&amp;#34;, &amp;#34;0&amp;#34;]` tag until the computed&lt;br/&gt;/// event ID matches the desired prefix.&lt;br/&gt;///&lt;br/&gt;/// # Arguments&lt;br/&gt;///&lt;br/&gt;/// * `keys` - The keypair used to sign the event.&lt;br/&gt;/// * `builder` - A pre-configured event builder (content, tags, kind, ...).&lt;br/&gt;/// * `prefix` - The desired hex prefix (case-insensitive). Each extra hex&lt;br/&gt;///   character increases the expected search time by a factor of 16.&lt;br/&gt;/// * `max_attempts` - Safety cap. Returns `None` if no matching nonce is found.&lt;br/&gt;///&lt;br/&gt;/// # Example&lt;br/&gt;///&lt;br/&gt;/// ```no_run&lt;br/&gt;/// use get_file_hash::mine_event_with_prefix;&lt;br/&gt;/// use nostr_sdk::{EventBuilder, Keys, Tag};&lt;br/&gt;///&lt;br/&gt;/// let keys = Keys::generate(); ## padded commit hash?&lt;br/&gt;/// let builder = EventBuilder::text_note(&amp;#34;hello&amp;#34;)&lt;br/&gt;///     .tag(Tag::parse([&amp;#34;example&amp;#34;, &amp;#34;pow&amp;#34;]).unwrap());&lt;br/&gt;///&lt;br/&gt;/// if let Some(event) = mine_event_with_prefix(&amp;amp;keys, builder, &amp;#34;0000&amp;#34;, 10_000_000) {&lt;br/&gt;///     println!(&amp;#34;mined event id: {}&amp;#34;, event.id);&lt;br/&gt;/// }&lt;br/&gt;/// ```&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;pub fn mine_event_with_prefix(&lt;br/&gt;    keys: &amp;amp;Keys,&lt;br/&gt;    builder: EventBuilder,&lt;br/&gt;    prefix: &amp;amp;str,&lt;br/&gt;    max_attempts: u128,&lt;br/&gt;) -&amp;gt; Option&amp;lt;Event&amp;gt; {&lt;br/&gt;    let pubkey = keys.public_key();&lt;br/&gt;    let prefix = prefix.to_ascii_lowercase();&lt;br/&gt;    let mut nonce: u128 = 0;&lt;br/&gt;&lt;br/&gt;    loop {&lt;br/&gt;        let mut attempt = builder.clone();&lt;br/&gt;        attempt = attempt.tag(Tag::pow(nonce, 0));&lt;br/&gt;&lt;br/&gt;        let mut unsigned: UnsignedEvent = attempt.build(pubkey);&lt;br/&gt;        let id: EventId = unsigned.id();&lt;br/&gt;&lt;br/&gt;        if id.to_hex().starts_with(&amp;amp;prefix) {&lt;br/&gt;            return unsigned.sign_with_keys(keys).ok();&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        nonce &#43;= 1;&lt;br/&gt;        if nonce &amp;gt; max_attempts {&lt;br/&gt;            return None;&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Convenience wrapper around [`mine_event_with_prefix`] with a default attempt&lt;br/&gt;/// cap of 100,000,000.&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;pub fn mine_event_with_prefix_or_default(&lt;br/&gt;    keys: &amp;amp;Keys,&lt;br/&gt;    builder: EventBuilder,&lt;br/&gt;    prefix: &amp;amp;str,&lt;br/&gt;) -&amp;gt; Option&amp;lt;Event&amp;gt; {&lt;br/&gt;    mine_event_with_prefix(keys, builder, prefix, DEFAULT_MAX_POW_ATTEMPTS)&lt;br/&gt;}&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-10-09T01:54:52Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs2my2lshe9ctx740ypkt05x5gl5pp5w9c5z5r5s3y0yjpt39lgs4szyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8x9fe2</id>
    
      <title type="html">[package] name = &amp;#34;get_file_hash_core&amp;#34; version = { ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs2my2lshe9ctx740ypkt05x5gl5pp5w9c5z5r5s3y0yjpt39lgs4szyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8x9fe2" />
    <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&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&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 = { 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 = { 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-10-09T01:54:51Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsf8zqmpmffx7kdtfuslh3jtkswz0hf9kr3wdqppkqu72k5fjqxewszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6js3g9yx</id>
    
      <title type="html">#![cfg(feature = &amp;#34;nostr&amp;#34;)] use frost_secp256k1_tr as ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsf8zqmpmffx7kdtfuslh3jtkswz0hf9kr3wdqppkqu72k5fjqxewszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6js3g9yx" />
    <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-10-09T01:54:51Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsysp65xetgeg6y6ydyz7ls76zc0y85jwtvuqzd2wwgp9dj3fk4mfqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jc3qe0u</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsysp65xetgeg6y6ydyz7ls76zc0y85jwtvuqzd2wwgp9dj3fk4mfqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jc3qe0u" />
    <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-10-09T01:54:51Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsyduj07ez2du7fzrcgr5n2mf0nctxu88fv3f6vgaylmm53t3pcx2czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jv465t4</id>
    
      <title type="html">// Copyright (c) 2023-2025 Rust Nostr Developers // Distributed ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsyduj07ez2du7fzrcgr5n2mf0nctxu88fv3f6vgaylmm53t3pcx2czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jv465t4" />
    <content type="html">
      // Copyright (c) 2023-2025 Rust Nostr Developers&lt;br/&gt;// Distributed under the MIT software license&lt;br/&gt;&lt;br/&gt;use std::error::Error;&lt;br/&gt;use std::ffi::OsStr;&lt;br/&gt;use std::path::{Path, PathBuf};&lt;br/&gt;use std::{env, fs};&lt;br/&gt;&lt;br/&gt;use nostr::hashes::{Hash, sha256};&lt;br/&gt;use get_file_hash_core::pip::{&lt;br/&gt;    MTU_PAYLOAD, PIP_MANIFEST_KIND, PIP_SLICE_KIND, PacketManifest, ProtocolSlice,&lt;br/&gt;    manifest_packet_event_ids, packet_manifest_to_event, packet_slice_from_event,&lt;br/&gt;    packet_slice_to_event, packetize,&lt;br/&gt;};&lt;br/&gt;use nostr::prelude::*;&lt;br/&gt;use nostr_sdk::prelude::Client;&lt;br/&gt;&lt;br/&gt;const SECRET_KEY_HEX: &amp;amp;str = &amp;#34;0000000000000000000000000000000000000000000000000000000000000001&amp;#34;;&lt;br/&gt;&lt;br/&gt;type CliResult&amp;lt;T&amp;gt; = std::result::Result&amp;lt;T, Box&amp;lt;dyn Error &#43; Send &#43; Sync&amp;gt;&amp;gt;;&lt;br/&gt;&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;struct Options {&lt;br/&gt;    relay: RelayUrl,&lt;br/&gt;    mode: Mode,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;enum Input {&lt;br/&gt;    File(PathBuf),&lt;br/&gt;    Directory(PathBuf),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;enum Mode {&lt;br/&gt;    Publish(Input),&lt;br/&gt;    Reconstruct { manifest: EventId, out: PathBuf },&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() -&amp;gt; CliResult&amp;lt;()&amp;gt; {&lt;br/&gt;    let options = parse_args()?;&lt;br/&gt;    let keys = Keys::new(SecretKey::from_hex(SECRET_KEY_HEX)?);&lt;br/&gt;&lt;br/&gt;    let client = Client::default();&lt;br/&gt;    client.add_relay(&amp;amp;options.relay).and_connect().await?;&lt;br/&gt;&lt;br/&gt;    match options.mode {&lt;br/&gt;        Mode::Publish(Input::File(path)) =&amp;gt; {&lt;br/&gt;            let base = path.parent().unwrap_or_else(|| Path::new(&amp;#34;.&amp;#34;));&lt;br/&gt;            push_file(&amp;amp;client, &amp;amp;options.relay, &amp;amp;keys, base, &amp;amp;path).await?&lt;br/&gt;        }&lt;br/&gt;        Mode::Publish(Input::Directory(path)) =&amp;gt; {&lt;br/&gt;            push_directory(&amp;amp;client, &amp;amp;options.relay, &amp;amp;keys, &amp;amp;path).await?&lt;br/&gt;        }&lt;br/&gt;        Mode::Reconstruct { manifest, out } =&amp;gt; {&lt;br/&gt;            reconstruct_from_manifest(&amp;amp;client, &amp;amp;options.relay, manifest, &amp;amp;out).await?&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    client.shutdown().await;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn parse_args() -&amp;gt; CliResult&amp;lt;Options&amp;gt; {&lt;br/&gt;    let mut relay: Option&amp;lt;RelayUrl&amp;gt; = None;&lt;br/&gt;    let mut file: Option&amp;lt;PathBuf&amp;gt; = None;&lt;br/&gt;    let mut directory: Option&amp;lt;PathBuf&amp;gt; = None;&lt;br/&gt;    let mut manifest: Option&amp;lt;EventId&amp;gt; = None;&lt;br/&gt;    let mut out: Option&amp;lt;PathBuf&amp;gt; = None;&lt;br/&gt;&lt;br/&gt;    let mut args = env::args().skip(1);&lt;br/&gt;    while let Some(arg) = args.next() {&lt;br/&gt;        match arg.as_str() {&lt;br/&gt;            &amp;#34;--help&amp;#34; | &amp;#34;-h&amp;#34; =&amp;gt; {&lt;br/&gt;                print_usage();&lt;br/&gt;                std::process::exit(0);&lt;br/&gt;            }&lt;br/&gt;            &amp;#34;--relay&amp;#34; =&amp;gt; {&lt;br/&gt;                let value = args&lt;br/&gt;                    .next()&lt;br/&gt;                    .ok_or_else(|| usage_error(&amp;#34;--relay requires a value&amp;#34;))?;&lt;br/&gt;                relay = Some(RelayUrl::parse(&amp;amp;value)?);&lt;br/&gt;            }&lt;br/&gt;            &amp;#34;--file&amp;#34; =&amp;gt; {&lt;br/&gt;                let value = args&lt;br/&gt;                    .next()&lt;br/&gt;                    .ok_or_else(|| usage_error(&amp;#34;--file requires a value&amp;#34;))?;&lt;br/&gt;                file = Some(PathBuf::from(value));&lt;br/&gt;            }&lt;br/&gt;            &amp;#34;--directory&amp;#34; | &amp;#34;--folder&amp;#34; =&amp;gt; {&lt;br/&gt;                let value = args&lt;br/&gt;                    .next()&lt;br/&gt;                    .ok_or_else(|| usage_error(&amp;#34;--directory requires a value&amp;#34;))?;&lt;br/&gt;                directory = Some(PathBuf::from(value));&lt;br/&gt;            }&lt;br/&gt;            &amp;#34;--manifest&amp;#34; =&amp;gt; {&lt;br/&gt;                let value = args&lt;br/&gt;                    .next()&lt;br/&gt;                    .ok_or_else(|| usage_error(&amp;#34;--manifest requires a value&amp;#34;))?;&lt;br/&gt;                manifest = Some(EventId::parse(&amp;amp;value)?);&lt;br/&gt;            }&lt;br/&gt;            &amp;#34;--out&amp;#34; =&amp;gt; {&lt;br/&gt;                let value = args&lt;br/&gt;                    .next()&lt;br/&gt;                    .ok_or_else(|| usage_error(&amp;#34;--out requires a value&amp;#34;))?;&lt;br/&gt;                out = Some(PathBuf::from(value));&lt;br/&gt;            }&lt;br/&gt;            other =&amp;gt; {&lt;br/&gt;                return Err(usage_error(format!(&amp;#34;unknown argument: {other}&amp;#34;)));&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let relay = relay.ok_or_else(|| usage_error(&amp;#34;--relay is required&amp;#34;))?;&lt;br/&gt;&lt;br/&gt;    let mode = match (file, directory, manifest, out) {&lt;br/&gt;        (Some(file), None, None, None) =&amp;gt; Mode::Publish(Input::File(file)),&lt;br/&gt;        (None, Some(directory), None, None) =&amp;gt; Mode::Publish(Input::Directory(directory)),&lt;br/&gt;        (None, None, Some(manifest), Some(out)) =&amp;gt; Mode::Reconstruct { manifest, out },&lt;br/&gt;        (Some(_), Some(_), _, _) =&amp;gt; {&lt;br/&gt;            return Err(usage_error(&amp;#34;use either --file or --directory/--folder&amp;#34;));&lt;br/&gt;        }&lt;br/&gt;        (_, _, Some(_), None) =&amp;gt; return Err(usage_error(&amp;#34;--out is required with --manifest&amp;#34;)),&lt;br/&gt;        (_, _, None, Some(_)) =&amp;gt; return Err(usage_error(&amp;#34;--manifest is required with --out&amp;#34;)),&lt;br/&gt;        _ =&amp;gt; {&lt;br/&gt;            return Err(usage_error(&lt;br/&gt;                &amp;#34;provide either --file/--directory/--folder or --manifest/--out&amp;#34;,&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    Ok(Options { relay, mode })&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn print_usage() {&lt;br/&gt;    eprintln!(&lt;br/&gt;        &amp;#34;Usage:\n  cargo run -p nostr --example nip-pip -- --file &amp;lt;path&amp;gt; --relay &amp;lt;ws(s)://relay&amp;gt;\n  cargo run -p nostr --example nip-pip -- --directory &amp;lt;path&amp;gt; --relay &amp;lt;ws(s)://relay&amp;gt;\n  cargo run -p nostr --example nip-pip -- --folder &amp;lt;path&amp;gt; --relay &amp;lt;ws(s)://relay&amp;gt;\n  cargo run -p nostr --example nip-pip -- --manifest &amp;lt;event-id&amp;gt; --out &amp;lt;path&amp;gt; --relay &amp;lt;ws(s)://relay&amp;gt;&amp;#34;&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn usage_error(message: impl Into&amp;lt;String&amp;gt;) -&amp;gt; Box&amp;lt;dyn Error &#43; Send &#43; Sync&amp;gt; {&lt;br/&gt;    Box::new(std::io::Error::new(&lt;br/&gt;        std::io::ErrorKind::InvalidInput,&lt;br/&gt;        message.into(),&lt;br/&gt;    ))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn push_directory(&lt;br/&gt;    client: &amp;amp;Client,&lt;br/&gt;    relay: &amp;amp;RelayUrl,&lt;br/&gt;    keys: &amp;amp;Keys,&lt;br/&gt;    directory: &amp;amp;Path,&lt;br/&gt;) -&amp;gt; CliResult&amp;lt;()&amp;gt; {&lt;br/&gt;    let mut files = Vec::new();&lt;br/&gt;    collect_files(directory, &amp;amp;mut files)?;&lt;br/&gt;    files.sort();&lt;br/&gt;&lt;br/&gt;    for file in files {&lt;br/&gt;        push_file(client, relay, keys, directory, &amp;amp;file).await?;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn push_file(&lt;br/&gt;    client: &amp;amp;Client,&lt;br/&gt;    relay: &amp;amp;RelayUrl,&lt;br/&gt;    keys: &amp;amp;Keys,&lt;br/&gt;    base: &amp;amp;Path,&lt;br/&gt;    file: &amp;amp;Path,&lt;br/&gt;) -&amp;gt; CliResult&amp;lt;()&amp;gt; {&lt;br/&gt;    let bytes = fs::read(file)?;&lt;br/&gt;    let relative = file&lt;br/&gt;        .strip_prefix(base)&lt;br/&gt;        .unwrap_or(file)&lt;br/&gt;        .to_string_lossy()&lt;br/&gt;        .replace(&amp;#39;\\&amp;#39;, &amp;#34;/&amp;#34;);&lt;br/&gt;    let root_id = packet_root_id(&amp;amp;relative);&lt;br/&gt;    let batch = packetize(root_id.clone(), bytes.clone());&lt;br/&gt;    let mut packet_events = Vec::with_capacity(batch.packets.len());&lt;br/&gt;    let mut packet_event_ids = Vec::with_capacity(batch.total_packets as usize);&lt;br/&gt;    let manifest = PacketManifest {&lt;br/&gt;        root: root_id,&lt;br/&gt;        sha256: sha256::Hash::hash(&amp;amp;bytes).to_string(),&lt;br/&gt;        size: bytes.len() as u64,&lt;br/&gt;        packets: batch.total_packets as u64,&lt;br/&gt;        depth: packet_depth(&amp;amp;batch.packets),&lt;br/&gt;        mtu: MTU_PAYLOAD as u64,&lt;br/&gt;        encoding: String::from(&amp;#34;json&amp;#34;),&lt;br/&gt;        path: relative.clone(),&lt;br/&gt;    };&lt;br/&gt;    println!(&lt;br/&gt;        &amp;#34;{}&amp;#34;,&lt;br/&gt;        serde_json::json!({&lt;br/&gt;            &amp;#34;event&amp;#34;: &amp;#34;perfect_ip file push start&amp;#34;,&lt;br/&gt;            &amp;#34;file&amp;#34;: relative,&lt;br/&gt;            &amp;#34;relay&amp;#34;: relay.as_str(),&lt;br/&gt;            &amp;#34;bytes&amp;#34;: bytes.len(),&lt;br/&gt;            &amp;#34;packets&amp;#34;: batch.total_packets,&lt;br/&gt;        })&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    for slice in batch.packets {&lt;br/&gt;        let event = packet_slice_to_event(&amp;amp;slice)?.finalize(keys)?;&lt;br/&gt;        let event_id = event.id.to_string();&lt;br/&gt;&lt;br/&gt;        let output = client.send_event(&amp;amp;event).to([relay]).await?;&lt;br/&gt;        packet_event_ids.push(event.id);&lt;br/&gt;        packet_events.push(event);&lt;br/&gt;        println!(&lt;br/&gt;            &amp;#34;{}&amp;#34;,&lt;br/&gt;            serde_json::json!({&lt;br/&gt;                &amp;#34;event&amp;#34;: &amp;#34;perfect_ip packet pushed&amp;#34;,&lt;br/&gt;                &amp;#34;file&amp;#34;: relative,&lt;br/&gt;                &amp;#34;packet&amp;#34;: slice.id,&lt;br/&gt;                &amp;#34;event_id&amp;#34;: event_id,&lt;br/&gt;                &amp;#34;relay&amp;#34;: relay.as_str(),&lt;br/&gt;                &amp;#34;ok_relays&amp;#34;: output.success.len(),&lt;br/&gt;                &amp;#34;failed_relays&amp;#34;: output.failed.len(),&lt;br/&gt;            })&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let manifest_event = packet_manifest_to_event(&amp;amp;manifest, &amp;amp;packet_event_ids)?.finalize(keys)?;&lt;br/&gt;    println!(&amp;#34;{}&amp;#34;, manifest_event.as_json());&lt;br/&gt;    let manifest_output = client.send_event(&amp;amp;manifest_event).to([relay]).await?;&lt;br/&gt;    println!(&lt;br/&gt;        &amp;#34;{}&amp;#34;,&lt;br/&gt;        serde_json::json!({&lt;br/&gt;            &amp;#34;event&amp;#34;: &amp;#34;perfect_ip manifest pushed&amp;#34;,&lt;br/&gt;            &amp;#34;file&amp;#34;: relative,&lt;br/&gt;            &amp;#34;event_id&amp;#34;: manifest_event.id.to_string(),&lt;br/&gt;            &amp;#34;relay&amp;#34;: relay.as_str(),&lt;br/&gt;            &amp;#34;ok_relays&amp;#34;: manifest_output.success.len(),&lt;br/&gt;            &amp;#34;failed_relays&amp;#34;: manifest_output.failed.len(),&lt;br/&gt;        })&lt;br/&gt;    );&lt;br/&gt;    let reconstructed = reconstruct_payload_from_fetched_events(&amp;amp;packet_events)?;&lt;br/&gt;    let reconstructed_hash = sha256::Hash::hash(&amp;amp;reconstructed).to_string();&lt;br/&gt;    println!(&lt;br/&gt;        &amp;#34;{}&amp;#34;,&lt;br/&gt;        serde_json::json!({&lt;br/&gt;            &amp;#34;event&amp;#34;: &amp;#34;perfect_ip file reconstructed&amp;#34;,&lt;br/&gt;            &amp;#34;file&amp;#34;: relative,&lt;br/&gt;            &amp;#34;bytes&amp;#34;: reconstructed.len(),&lt;br/&gt;            &amp;#34;matches&amp;#34;: reconstructed == bytes,&lt;br/&gt;            &amp;#34;sha256_matches&amp;#34;: reconstructed_hash == manifest.sha256,&lt;br/&gt;        })&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    println!(&lt;br/&gt;        &amp;#34;{}&amp;#34;,&lt;br/&gt;        serde_json::json!({&lt;br/&gt;            &amp;#34;event&amp;#34;: &amp;#34;perfect_ip file push complete&amp;#34;,&lt;br/&gt;            &amp;#34;file&amp;#34;: relative,&lt;br/&gt;            &amp;#34;relay&amp;#34;: relay.as_str(),&lt;br/&gt;        })&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn reconstruct_from_manifest(&lt;br/&gt;    client: &amp;amp;Client,&lt;br/&gt;    relay: &amp;amp;RelayUrl,&lt;br/&gt;    manifest_id: EventId,&lt;br/&gt;    out: &amp;amp;Path,&lt;br/&gt;) -&amp;gt; CliResult&amp;lt;()&amp;gt; {&lt;br/&gt;    let manifest_filter = Filter::new().kind(PIP_MANIFEST_KIND).ids([manifest_id]);&lt;br/&gt;    let manifest_events = client.fetch_events(manifest_filter).await?;&lt;br/&gt;    let manifest_event = manifest_events&lt;br/&gt;        .first()&lt;br/&gt;        .cloned()&lt;br/&gt;        .ok_or_else(|| usage_error(&amp;#34;manifest event not found&amp;#34;))?;&lt;br/&gt;    let manifest: PacketManifest = serde_json::from_str(&amp;amp;manifest_event.content)?;&lt;br/&gt;    let packet_event_ids = manifest_packet_event_ids(&amp;amp;manifest_event);&lt;br/&gt;    let packet_event_count = packet_event_ids.len();&lt;br/&gt;    if packet_event_ids.is_empty() {&lt;br/&gt;        return Err(usage_error(&lt;br/&gt;            &amp;#34;manifest event does not reference any packet events&amp;#34;,&lt;br/&gt;        ));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let packet_filter = Filter::new()&lt;br/&gt;        .kind(PIP_SLICE_KIND)&lt;br/&gt;        .ids(packet_event_ids.clone());&lt;br/&gt;    let packet_events = client.fetch_events(packet_filter).await?;&lt;br/&gt;    let packet_events = packet_events.into_iter().collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;();&lt;br/&gt;    let reconstructed = reconstruct_payload_from_fetched_events(&amp;amp;packet_events)?;&lt;br/&gt;    let reconstructed_hash = sha256::Hash::hash(&amp;amp;reconstructed).to_string();&lt;br/&gt;    let output_path = resolve_output_path(out, &amp;amp;manifest);&lt;br/&gt;&lt;br/&gt;    if let Some(parent) = output_path.parent() {&lt;br/&gt;        fs::create_dir_all(parent)?;&lt;br/&gt;    }&lt;br/&gt;    fs::write(&amp;amp;output_path, &amp;amp;reconstructed)?;&lt;br/&gt;&lt;br/&gt;    println!(&lt;br/&gt;        &amp;#34;{}&amp;#34;,&lt;br/&gt;        serde_json::json!({&lt;br/&gt;            &amp;#34;event&amp;#34;: &amp;#34;perfect_ip manifest fetched&amp;#34;,&lt;br/&gt;            &amp;#34;relay&amp;#34;: relay.as_str(),&lt;br/&gt;            &amp;#34;manifest&amp;#34;: manifest_id.to_string(),&lt;br/&gt;            &amp;#34;manifest_event&amp;#34;: manifest_event.id.to_string(),&lt;br/&gt;            &amp;#34;packet_events&amp;#34;: packet_event_count,&lt;br/&gt;            &amp;#34;out&amp;#34;: output_path.display().to_string(),&lt;br/&gt;            &amp;#34;bytes&amp;#34;: reconstructed.len(),&lt;br/&gt;            &amp;#34;matches&amp;#34;: reconstructed_hash == manifest.sha256,&lt;br/&gt;        })&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn reconstruct_payload_from_fetched_events(events: &amp;amp;[Event]) -&amp;gt; CliResult&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt; {&lt;br/&gt;    let mut slices: Vec&amp;lt;ProtocolSlice&amp;gt; = Vec::new();&lt;br/&gt;&lt;br/&gt;    for event in events {&lt;br/&gt;        if let Some(slice) = packet_slice_from_event(event)? {&lt;br/&gt;            if !slice.is_parity {&lt;br/&gt;                slices.push(slice);&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    slices.sort_by_key(|slice| slice.header.seq_num);&lt;br/&gt;&lt;br/&gt;    Ok(slices.into_iter().flat_map(|slice| slice.data).collect())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn resolve_output_path(out: &amp;amp;Path, manifest: &amp;amp;PacketManifest) -&amp;gt; PathBuf {&lt;br/&gt;    let manifest_name = Path::new(&amp;amp;manifest.path)&lt;br/&gt;        .file_name()&lt;br/&gt;        .unwrap_or_else(|| OsStr::new(&amp;amp;manifest.path));&lt;br/&gt;&lt;br/&gt;    if out.is_dir() || out.extension().is_none() {&lt;br/&gt;        out.join(manifest_name)&lt;br/&gt;    } else {&lt;br/&gt;        out.to_path_buf()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn packet_depth(packets: &amp;amp;[ProtocolSlice]) -&amp;gt; u32 {&lt;br/&gt;    packets&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|slice| slice.id.matches(&amp;#39;.&amp;#39;).count() as u32)&lt;br/&gt;        .max()&lt;br/&gt;        .unwrap_or(0)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn collect_files(directory: &amp;amp;Path, files: &amp;amp;mut Vec&amp;lt;PathBuf&amp;gt;) -&amp;gt; CliResult&amp;lt;()&amp;gt; {&lt;br/&gt;    for entry in fs::read_dir(directory)? {&lt;br/&gt;        let entry = entry?;&lt;br/&gt;        let path = entry.path();&lt;br/&gt;&lt;br/&gt;        if path.is_dir() {&lt;br/&gt;            collect_files(&amp;amp;path, files)?;&lt;br/&gt;        } else if path.is_file() {&lt;br/&gt;            files.push(path);&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn packet_root_id(relative: &amp;amp;str) -&amp;gt; String {&lt;br/&gt;    let mut hash: u64 = 0xcbf29ce484222325;&lt;br/&gt;&lt;br/&gt;    for byte in relative.as_bytes() {&lt;br/&gt;        hash ^= u64::from(*byte);&lt;br/&gt;        hash = hash.wrapping_mul(0x100000001b3);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    format!(&amp;#34;ROOT.{hash:016x}&amp;#34;)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:51Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqszy9ve7uctrvuj7utsnyyu88j0ygud4n6wg9lrm7hyvk6xg3vzvjqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jvqegdq</id>
    
      <title type="html">#[cfg(all(not(debug_assertions), feature = &amp;#34;nostr&amp;#34;))] ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqszy9ve7uctrvuj7utsnyyu88j0ygud4n6wg9lrm7hyvk6xg3vzvjqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jvqegdq" />
    <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-10-09T01:54:51Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspuglmeq3gv7z72f33zfzpdeqm33kar5kwvl8797jtxqmayuznldqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jd6ep3r</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspuglmeq3gv7z72f33zfzpdeqm33kar5kwvl8797jtxqmayuznldqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jd6ep3r" />
    <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-10-09T01:54:51Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsqfq9rm230ql64uzkrrudtzaxjejcn8dd2y7lqdhr7ujfj4jet3kszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j6l3ycw</id>
    
      <title type="html">//! A simple command-line tool that calculates and displays the ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsqfq9rm230ql64uzkrrudtzaxjejcn8dd2y7lqdhr7ujfj4jet3kszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j6l3ycw" />
    <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-10-09T01:54:51Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsyxlpcy6np6r0fwjj5dnr56ew6uqvhy3qz03puvthvu3ceneaj8xqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4u8vzr</id>
    
      <title type="html">//! A simple command-line tool that calculates and displays the ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsyxlpcy6np6r0fwjj5dnr56ew6uqvhy3qz03puvthvu3ceneaj8xqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4u8vzr" />
    <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-10-09T01:54:50Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsve37cjq03pl2fm700zvyl9lkdfjwh97jk8n9v30awucfmhephf6qzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jg6n3ae</id>
    
      <title type="html">&amp;lt;?xml version=&amp;#39;1.0&amp;#39; ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsve37cjq03pl2fm700zvyl9lkdfjwh97jk8n9v30awucfmhephf6qzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jg6n3ae" />
    <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-10-09T01:54:49Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs9v656ed7gnde2ln5adm26juny6jz3f386p526k3aeefee8m7cl8czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j2xh2he</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs9v656ed7gnde2ln5adm26juny6jz3f386p526k3aeefee8m7cl8czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j2xh2he" />
    <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-10-09T01:54:49Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsycn5sn6327dw99v06uym38sr6p5k7unpuhgj3jawgc3s560quhcqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j3j77a9</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsycn5sn6327dw99v06uym38sr6p5k7unpuhgj3jawgc3s560quhcqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j3j77a9" />
    <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-10-09T01:54:49Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs02gmpmqyq27trypnfr4zcgcgn822f778k6vjp4uj96axc96n2s4gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6ju9guen</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs02gmpmqyq27trypnfr4zcgcgn822f778k6vjp4uj96axc96n2s4gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6ju9guen" />
    <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-10-09T01:54:43Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs0m7rtlal83efud3qd7ewtdfj6va5n33g9akmxv6wlc659cvuvqlqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jujkuq2</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs0m7rtlal83efud3qd7ewtdfj6va5n33g9akmxv6wlc659cvuvqlqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jujkuq2" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqstde0cffse532qqqjq8yakhtmgwcpv6vskvpxz73tc9vvpe0pf27szyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jfh03k4</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqstde0cffse532qqqjq8yakhtmgwcpv6vskvpxz73tc9vvpe0pf27szyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jfh03k4" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqst8pq6ez7nx30fly6f7vq9z0hv80g0synphg6jn046wsxl4jgmkggzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jh3skxz</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqst8pq6ez7nx30fly6f7vq9z0hv80g0synphg6jn046wsxl4jgmkggzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jh3skxz" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsf2zepja5pxaeswp4uzh4vtvv3hnnjuxna89utz3crzccumcfdmkgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8c4q9c</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsf2zepja5pxaeswp4uzh4vtvv3hnnjuxna89utz3crzccumcfdmkgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8c4q9c" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs889kr28tl6jf7g4ku2trlxrwvexefejraxw9kp2gzenl6q0w8a3gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jglzq0r</id>
    
      <title type="html">#![allow(unused)] // n34 - A CLI to interact with NIP-34 and ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs889kr28tl6jf7g4ku2trlxrwvexefejraxw9kp2gzenl6q0w8a3gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jglzq0r" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsxc3nw7vapmunx9g0uxh42gungcxa9ahjwr5z4420l5h6rrdjxzjczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jrkshfc</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsxc3nw7vapmunx9g0uxh42gungcxa9ahjwr5z4420l5h6rrdjxzjczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jrkshfc" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsy40u7x6eqclcucu0g56vgv7majwtcak73jr2wpghh9j77zx9v5uqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8uq0k2</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsy40u7x6eqclcucu0g56vgv7majwtcak73jr2wpghh9j77zx9v5uqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8uq0k2" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqszjj980zmfq27z5wzsthsqdj74fr6u46n7lgpt80h5ckkehfzm2qgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jt2gnjv</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqszjj980zmfq27z5wzsthsqdj74fr6u46n7lgpt80h5ckkehfzm2qgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jt2gnjv" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqszx6mast5zs6plrngec3skglknw9pt77vapz6fqjx6wwsxnpsehzgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jpfelp8</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqszx6mast5zs6plrngec3skglknw9pt77vapz6fqjx6wwsxnpsehzgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jpfelp8" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspk3wpvp59cfa8vz5wlufgcgq6xp7p3w02cjaa6pl4qy8hk2grxfqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jrddwdz</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspk3wpvp59cfa8vz5wlufgcgq6xp7p3w02cjaa6pl4qy8hk2grxfqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jrddwdz" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspp2wyv0luc6a649jdpnnhaxjcnymnwy6t6zkdc93mwuzkscvn2hszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jj8xyct</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspp2wyv0luc6a649jdpnnhaxjcnymnwy6t6zkdc93mwuzkscvn2hszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jj8xyct" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsq9dtqvdk3vv07k2w4uumwnj6ejuzk8p09whky8l347926tn8yewqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jchawca</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsq9dtqvdk3vv07k2w4uumwnj6ejuzk8p09whky8l347926tn8yewqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jchawca" />
    <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-10-09T01:54:39Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsgkjgktvsdnq6yamrcuv9y8ke0zgekgh7thackn7vj3w8cpexvrxczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jaldzqp</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsgkjgktvsdnq6yamrcuv9y8ke0zgekgh7thackn7vj3w8cpexvrxczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jaldzqp" />
    <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-10-09T01:54:38Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs8mtd8dgcqttkgz09phv8mrdx3rg6m5upf2s4ug7wl3sph3vs2kagzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jlc6hu2</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs8mtd8dgcqttkgz09phv8mrdx3rg6m5upf2s4ug7wl3sph3vs2kagzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jlc6hu2" />
    <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-10-09T01:54:38Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsr4s9hzs4swds6vut3q4f6hewm76l4zky43wa7yfvlftpa7wz9k2czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0sc9cx</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsr4s9hzs4swds6vut3q4f6hewm76l4zky43wa7yfvlftpa7wz9k2czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0sc9cx" />
    <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-10-09T01:54:37Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsvhsk8nuahn4mmjx48avg525lhffjeu9yst07c4f2gd0w2arnfzyqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jfxfmcf</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsvhsk8nuahn4mmjx48avg525lhffjeu9yst07c4f2gd0w2arnfzyqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jfxfmcf" />
    <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-10-09T01:54:32Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs0h0hnhm2cth5hmsmxy36ztnf0jv29zyk809axq6flfv5wy7h84kczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jpvde3z</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs0h0hnhm2cth5hmsmxy36ztnf0jv29zyk809axq6flfv5wy7h84kczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jpvde3z" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsv5crd7ylyqhf2y2aazhdkq7l83lmqt9uq6wld2gwxvc9g3tajxjgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4grc5d</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsv5crd7ylyqhf2y2aazhdkq7l83lmqt9uq6wld2gwxvc9g3tajxjgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4grc5d" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsvzhelz26sfryeh889usw38ran00dz2t44heejclclkwww50t4sjszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j7a70a3</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsvzhelz26sfryeh889usw38ran00dz2t44heejclclkwww50t4sjszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j7a70a3" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs2zeet7tcx85z2kuaxug4qn5k9wvg7j65wqqt52un7z620lrwv7yszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jdyxwrz</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs2zeet7tcx85z2kuaxug4qn5k9wvg7j65wqqt52un7z620lrwv7yszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jdyxwrz" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs2pu8gv208a6hd720hyt2fsw3sx2dcry52xvn8wehke2nh4kyhlwqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jxhwwnc</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs2pu8gv208a6hd720hyt2fsw3sx2dcry52xvn8wehke2nh4kyhlwqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jxhwwnc" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsfsh57hxg69t3hcqpat36as25al6cusv9gaqym7fnvjr75hfcxxzszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4gamuc</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsfsh57hxg69t3hcqpat36as25al6cusv9gaqym7fnvjr75hfcxxzszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4gamuc" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsg67c7tlkmjdv9jv9pzy5h4n4uh7ydj8dcsskcwz4sdtvmzcx769czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jmef6au</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsg67c7tlkmjdv9jv9pzy5h4n4uh7ydj8dcsskcwz4sdtvmzcx769czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jmef6au" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs890qczhhnlrjl5rd4jgr3m0vd9uzzu9rjarv5dhx7dz35y9gu8pgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8qdxxh</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs890qczhhnlrjl5rd4jgr3m0vd9uzzu9rjarv5dhx7dz35y9gu8pgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8qdxxh" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsxa59wzeuapl7hpgv32e6akjtsndxuhzkk7az8fm6fsqglcz25wkgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6ja7sthm</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsxa59wzeuapl7hpgv32e6akjtsndxuhzkk7az8fm6fsqglcz25wkgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6ja7sthm" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs9saz366e9qaeg3xadftr2t06x625e3fz93pxhgpduqrmcvn6dzrqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8pyc99</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs9saz366e9qaeg3xadftr2t06x625e3fz93pxhgpduqrmcvn6dzrqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8pyc99" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsr7v5wuxg2ya8z3v58xavkjq0vu8nvctsyjlqnj98j4cp3mhw3hrqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j6qa7ag</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsr7v5wuxg2ya8z3v58xavkjq0vu8nvctsyjlqnj98j4cp3mhw3hrqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j6qa7ag" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsru3533592mxn5p6yfuvqmujlfneh5yll0nnecmx2lkqty6x42l0czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jd0fsmd</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsru3533592mxn5p6yfuvqmujlfneh5yll0nnecmx2lkqty6x42l0czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jd0fsmd" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsr9jfzwrkmp6p4t5scrvhv6a0fqefq5dnuc94rtxleht4g2netr4szyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jucm3n7</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsr9jfzwrkmp6p4t5scrvhv6a0fqefq5dnuc94rtxleht4g2netr4szyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jucm3n7" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsrprxgp5km5u2gwh6sy70z5unwr4jldd89mmacj6tr5k7n2dde8yszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jsxdup6</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsrprxgp5km5u2gwh6sy70z5unwr4jldd89mmacj6tr5k7n2dde8yszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jsxdup6" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqszn0l0jg7ysun5hlxyfm98k26h7rfux5d26ahs58k6y5gwpy5qekqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4twtdz</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqszn0l0jg7ysun5hlxyfm98k26h7rfux5d26ahs58k6y5gwpy5qekqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4twtdz" />
    <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-10-09T01:54:27Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspyds2exgundyluluqsa5ru4xfxegjdm4l08kmy372e747h0e567gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jzxnkzz</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspyds2exgundyluluqsa5ru4xfxegjdm4l08kmy372e747h0e567gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jzxnkzz" />
    <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-10-09T01:54:13Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs02fw793n0ys90cxjrysq7x2kzvvktf79hk5mq6elgs30e45rqnuszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jy3qv2v</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs02fw793n0ys90cxjrysq7x2kzvvktf79hk5mq6elgs30e45rqnuszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jy3qv2v" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsd0wt05ls5nja243q2ykn7uvhzm9cqy3h8m8nknxaaa2lhqllx89gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jkj8rgx</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsd0wt05ls5nja243q2ykn7uvhzm9cqy3h8m8nknxaaa2lhqllx89gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jkj8rgx" />
    <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::SocketAddr;&lt;br/&gt;&lt;br/&gt;use clap::{ArgGroup, Args};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, options_state::DEFAULT_NIP07_PROXY_ADDR, traits::CommandRunner},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;#[clap(&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;options&amp;#34;)&lt;br/&gt;            .required(true)&lt;br/&gt;    )&lt;br/&gt;)]&lt;br/&gt;pub struct Nip07Args {&lt;br/&gt;    /// Enable NIP-07 as the default signer.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    enable:  bool,&lt;br/&gt;    /// Disable NIP-07 as the default signer.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;, group = &amp;#34;disable_options&amp;#34;)]&lt;br/&gt;    disable: bool,&lt;br/&gt;    /// Set the default `ip:port` for the browser signer proxy.&lt;br/&gt;    #[arg(long, group = &amp;#34;disable_options&amp;#34;)]&lt;br/&gt;    addr:    Option&amp;lt;SocketAddr&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for Nip07Args {&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.enable {&lt;br/&gt;            let addr = self.addr.unwrap_or(DEFAULT_NIP07_PROXY_ADDR);&lt;br/&gt;            options.config.nip07 = Some(addr)&lt;br/&gt;        } else {&lt;br/&gt;            options.config.nip07 = None&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsdq3ya3lt8v8knfefy83j8mryv6vgxzc3c4hyuwses32z9924cjxczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jgysu6g</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsdq3ya3lt8v8knfefy83j8mryv6vgxzc3c4hyuwses32z9924cjxczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jgysu6g" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsvcphlqlggq2tgkg5mwla2p530szeyfy7c0ep0wlnuxxp96pwx8cczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jnq283n</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsvcphlqlggq2tgkg5mwla2p530szeyfy7c0ep0wlnuxxp96pwx8cczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jnq283n" />
    <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 bunker` subcommand&lt;br/&gt;mod bunker;&lt;br/&gt;/// `config keyring` subcommand&lt;br/&gt;mod keyring;&lt;br/&gt;/// `config nip07` subcommand&lt;br/&gt;mod nip07;&lt;br/&gt;/// `config pow` subcommand&lt;br/&gt;mod pow;&lt;br/&gt;/// `config relays` subcommand&lt;br/&gt;mod relays;&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;&lt;br/&gt;use self::bunker::BunkerArgs;&lt;br/&gt;use self::keyring::KeyringArgs;&lt;br/&gt;use self::nip07::Nip07Args;&lt;br/&gt;use self::pow::PowArgs;&lt;br/&gt;use self::relays::RelaysArgs;&lt;br/&gt;use super::CliOptions;&lt;br/&gt;use crate::{cli::traits::CommandRunner, error::N34Result};&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum ConfigSubcommands {&lt;br/&gt;    /// Sets the default PoW difficulty (0 if not specified)&lt;br/&gt;    Pow(PowArgs),&lt;br/&gt;    /// Sets the default fallback relays if none provided. Use this relays for&lt;br/&gt;    /// read and write.&lt;br/&gt;    Relays(RelaysArgs),&lt;br/&gt;    /// Sets a URL of NIP-46 bunker server used for signing events.&lt;br/&gt;    Bunker(BunkerArgs),&lt;br/&gt;    /// Managing the secret key keyring, including enabling, disabling, or&lt;br/&gt;    /// resetting it.&lt;br/&gt;    Keyring(KeyringArgs),&lt;br/&gt;    /// Controls the NIP-07 browser signer proxy, turning it on or off, and&lt;br/&gt;    /// configures the `ip:port` address.&lt;br/&gt;    Nip07(Nip07Args),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ConfigSubcommands {&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; Pow Relays Bunker Keyring Nip07)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsfsgn9kvz27q2mttl68hwp8z025ya5tkffv26x8mzl7h3jqd7c5wgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0p32md</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsfsgn9kvz27q2mttl68hwp8z025ya5tkffv26x8mzl7h3jqd7c5wgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0p32md" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsg3mkv6ht6ku4tq2hmnkmaqeuuhnkf5en6yz0lmqxs9a708gfrrrgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jwxhcen</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsg3mkv6ht6ku4tq2hmnkmaqeuuhnkf5en6yz0lmqxs9a708gfrrrgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jwxhcen" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs8ue9l0fjmjsylsxu3dvvvqaf7py8le30yvzu9ugymcrgk6sx32wszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6je8uzcq</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs8ue9l0fjmjsylsxu3dvvvqaf7py8le30yvzu9ugymcrgk6sx32wszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6je8uzcq" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs82h8tqfy5eje2xg8hh354she7xwu87lvu4cf22ayerwf9a4ec69qzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jpw9hfw</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs82h8tqfy5eje2xg8hh354she7xwu87lvu4cf22ayerwf9a4ec69qzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jpw9hfw" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs95s4xrynyug2hc9y7uz2mrs3zxwlqh5wnhs6lzdgecct0rczrnggzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jaxs0m6</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs95s4xrynyug2hc9y7uz2mrs3zxwlqh5wnhs6lzdgecct0rczrnggzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jaxs0m6" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsrz52r7sa95d9h3yklgwunhcgr70zee3dntvwfffgxtyqyzrgt45qzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jghapj8</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsrz52r7sa95d9h3yklgwunhcgr70zee3dntvwfffgxtyqyzrgt45qzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jghapj8" />
    <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::nip46::NostrConnectURI;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, traits::CommandRunner},&lt;br/&gt;    error::{N34Error, N34Result},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct BunkerArgs {&lt;br/&gt;    /// Nostr Connect URL for the bunker. Omit this to remove the current bunker&lt;br/&gt;    /// URL.&lt;br/&gt;    bunker_url: Option&amp;lt;NostrConnectURI&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;impl CommandRunner for BunkerArgs {&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 let Some(ref bunker_url) = self.bunker_url&lt;br/&gt;            &amp;amp;&amp;amp; !bunker_url.is_bunker()&lt;br/&gt;        {&lt;br/&gt;            return Err(N34Error::NotBunkerUrl);&lt;br/&gt;        }&lt;br/&gt;        options.config.bunker_url = self.bunker_url;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsp7v77fnprvnr95mu6340mprkwclumk6vjkl3hrrk5c4k5z8tdxeszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jrpt49z</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsp7v77fnprvnr95mu6340mprkwclumk6vjkl3hrrk5c4k5z8tdxeszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jrpt49z" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspzps3g03hs6e6kpw0jqswspqa83vntyk4v4k48yv9mnmjzlrvj0gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4enc7w</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspzps3g03hs6e6kpw0jqswspqa83vntyk4v4k48yv9mnmjzlrvj0gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j4enc7w" />
    <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::{ArgGroup, Args};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{Cli, CliOptions, traits::CommandRunner},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;#[clap(&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;options&amp;#34;)&lt;br/&gt;            .required(true)&lt;br/&gt;    )&lt;br/&gt;)]&lt;br/&gt;pub struct KeyringArgs {&lt;br/&gt;    /// Turns on secret key keyring. Requires entering the key once when&lt;br/&gt;    /// enabled.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    enable:  bool,&lt;br/&gt;    /// Turns off secret key keyring. Removes any existing key and prevents&lt;br/&gt;    /// storing new ones.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    disable: bool,&lt;br/&gt;    /// Deletes current key and stores the next provided key.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    reset:   bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for KeyringArgs {&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 keyring = nostr_keyring::NostrKeyring::new(Cli::N34_KEYRING_SERVICE_NAME);&lt;br/&gt;&lt;br/&gt;        if self.enable {&lt;br/&gt;            options.config.keyring_secret_key = true;&lt;br/&gt;        } else if self.disable {&lt;br/&gt;            options.config.keyring_secret_key = false;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        if self.reset || self.disable {&lt;br/&gt;            let _ = keyring.delete(Cli::USER_KEY_PAIR_ENTRY);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsq985xhgq9c6p8yuwvpeqhcjazphgrtw9vtlm6ffyt0608wzy36rqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jhp83xp</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsq985xhgq9c6p8yuwvpeqhcjazphgrtw9vtlm6ffyt0608wzy36rqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jhp83xp" />
    <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-10-09T01:54:06Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs2krgu0aszenjmdgn24wm7nruwk5wqlujqf0sy645c0rxy88sn3eczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jyh4ke9</id>
    
      <title type="html">#!/usr/bin/env sh cargo run --bin n34 -- repo view ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs2krgu0aszenjmdgn24wm7nruwk5wqlujqf0sy645c0rxy88sn3eczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jyh4ke9" />
    <content type="html">
      #!/usr/bin/env sh&lt;br/&gt;cargo run --bin n34 -- repo view naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpp4mhxue69uhkummn9ekx7mqppamhxue69uhkummnw3ezumt0d5q3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qg5waehxw309ahx7um5wghx77r5wghxgetkxpx8xj&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:05Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs2rgkasffk63tfyvp948a8tc2ruzp3xykf38z686tgn66d9l7gd6czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jgrtkk0</id>
    
      <title type="html">unstable_features = true style_edition = &amp;#34;2024&amp;#34; ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs2rgkasffk63tfyvp948a8tc2ruzp3xykf38z686tgn66d9l7gd6czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jgrtkk0" />
    <content type="html">
      unstable_features = true&lt;br/&gt;style_edition     = &amp;#34;2024&amp;#34;&lt;br/&gt;&lt;br/&gt;blank_lines_upper_bound        = 2&lt;br/&gt;combine_control_expr           = false&lt;br/&gt;wrap_comments                  = true&lt;br/&gt;condense_wildcard_suffixes     = true&lt;br/&gt;edition                        = &amp;#34;2024&amp;#34;&lt;br/&gt;enum_discrim_align_threshold   = 20&lt;br/&gt;force_multiline_blocks         = true&lt;br/&gt;format_code_in_doc_comments    = true&lt;br/&gt;format_generated_files         = false&lt;br/&gt;format_macro_matchers          = true&lt;br/&gt;format_strings                 = true&lt;br/&gt;imports_layout                 = &amp;#34;HorizontalVertical&amp;#34;&lt;br/&gt;newline_style                  = &amp;#34;Unix&amp;#34;&lt;br/&gt;normalize_comments             = true&lt;br/&gt;reorder_impl_items             = true&lt;br/&gt;group_imports                  = &amp;#34;StdExternalCrate&amp;#34;&lt;br/&gt;single_line_let_else_max_width = 0&lt;br/&gt;struct_field_align_threshold   = 20&lt;br/&gt;use_try_shorthand              = true&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:02Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs9kymh3ehcejzqu8m3vutt6tsk0yksxlv0j9qv9ekdklr30m3u8kqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jaax3vh</id>
    
      <title type="html"># This file contains NIP-19 `naddr` entities for repositories ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs9kymh3ehcejzqu8m3vutt6tsk0yksxlv0j9qv9ekdklr30m3u8kqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jaax3vh" />
    <content type="html">
      # 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;naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpp4mhxue69uhkummn9ekx7mqppamhxue69uhkummnw3ezumt0d5q3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qg5waehxw309ahx7um5wghx77r5wghxgetkxpx8xj&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:54:02Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsdj4e0pmk9hcjldpupxc38542pay4t766z4fhhuhr2rg5k52j9xuqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jd6c0es</id>
    
      <title type="html"># This justfile is for the contrbutors of this project, not for ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsdj4e0pmk9hcjldpupxc38542pay4t766z4fhhuhr2rg5k52j9xuqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jd6c0es" />
    <content type="html">
      # This justfile is for the contrbutors of this project, not for the end user.&lt;br/&gt;#&lt;br/&gt;# Requirements for this justfile:&lt;br/&gt;# - Linux distribution&lt;br/&gt;# - just (Of course) &amp;lt;&lt;a href=&#34;https://github.com/casey/just&amp;gt&#34;&gt;https://github.com/casey/just&amp;gt&lt;/a&gt;;&lt;br/&gt;# - cargo (For the build and tests) &amp;lt;&lt;a href=&#34;https://doc.rust-lang.org/cargo/getting-started/installation.html&amp;gt&#34;&gt;https://doc.rust-lang.org/cargo/getting-started/installation.html&amp;gt&lt;/a&gt;;&lt;br/&gt;# - mdbook (&amp;lt;&lt;a href=&#34;https://rust-lang.github.io/mdBook&amp;gt&#34;&gt;https://rust-lang.github.io/mdBook&amp;gt&lt;/a&gt;;)&lt;br/&gt;# - git-cliff (&amp;lt;&lt;a href=&#34;https://git-cliff.org&amp;gt&#34;&gt;https://git-cliff.org&amp;gt&lt;/a&gt;;)&lt;br/&gt;# - taplo (&amp;lt;&lt;a href=&#34;https://taplo.tamasfe.dev/&amp;gt&#34;&gt;https://taplo.tamasfe.dev/&amp;gt&lt;/a&gt;;)&lt;br/&gt;# - cargo-msrv (&amp;lt;&lt;a href=&#34;https://github.com/foresterre/cargo-msrv&amp;gt&#34;&gt;https://github.com/foresterre/cargo-msrv&amp;gt&lt;/a&gt;;)&lt;br/&gt;# - nushell (&amp;lt;&lt;a href=&#34;https://nushell.sh&amp;gt&#34;&gt;https://nushell.sh&amp;gt&lt;/a&gt;;)&lt;br/&gt;&lt;br/&gt;set quiet&lt;br/&gt;set unstable&lt;br/&gt;set shell := [&amp;#34;/usr/bin/env&amp;#34;, &amp;#34;bash&amp;#34;, &amp;#34;-c&amp;#34;]&lt;br/&gt;set script-interpreter := [&amp;#34;/usr/bin/env&amp;#34;, &amp;#34;nu&amp;#34;]&lt;br/&gt;&lt;br/&gt;JUST_EXECUTABLE := &amp;#34;just -u -f &amp;#34; &#43; justfile()&lt;br/&gt;header := &amp;#34;Available tasks:\n&amp;#34;&lt;br/&gt;BOOK_DEST_DIR := &amp;#34;dest&amp;#34;&lt;br/&gt;tag_change_body := &amp;#39;&amp;#39;&amp;#39;{% for group, commits in commits | group_by(attribute=&amp;#34;group&amp;#34;) %}&lt;br/&gt;&lt;br/&gt;{{ group | upper_first }}&lt;br/&gt;&lt;br/&gt;{% for commit in commits %}&lt;br/&gt;- {{ commit.message | split(pat=&amp;#34;\n&amp;#34;) | first | split(pat=&amp;#34;:&amp;#34;) | slice(start=1) | join(sep=&amp;#34;:&amp;#34;) | upper_first | trim }} - by {{ commit.author.name}}{% endfor %}{% endfor %}&lt;br/&gt;&amp;#39;&amp;#39;&amp;#39;&lt;br/&gt;&lt;br/&gt;export TZ := &amp;#34;UTC&amp;#34;&lt;br/&gt;&lt;br/&gt;_default:&lt;br/&gt;    @{{JUST_EXECUTABLE}} --list-heading &amp;#34;{{header}}&amp;#34; --list&lt;br/&gt;&lt;br/&gt;# Run the CI&lt;br/&gt;ci: &amp;amp;&amp;amp; msrv _done_ci&lt;br/&gt;    echo &amp;#34;🔨 Building n34...&amp;#34;&lt;br/&gt;    cargo build -q&lt;br/&gt;    echo &amp;#34;🔍 Checking code formatting...&amp;#34;&lt;br/&gt;    cargo fmt -q -- --check&lt;br/&gt;    RUST_LOG=none taplo fmt --check --config &amp;#34;./.taplo.toml&amp;#34; || (echo &amp;#34;❌Toml files is not properly formatted&amp;#34; &amp;amp;&amp;amp; exit 1)&lt;br/&gt;    echo &amp;#34;🧹 Running linter checks...&amp;#34;&lt;br/&gt;    cargo clippy -q -- -D warnings&lt;br/&gt;    echo &amp;#34;🧪 Running tests...&amp;#34;&lt;br/&gt;    cargo test -q&lt;br/&gt;&lt;br/&gt;# Check that the current MSRV is correct&lt;br/&gt;msrv:&lt;br/&gt;    echo &amp;#34;🔧 Verifying MSRV...&amp;#34;&lt;br/&gt;    cargo-msrv verify&lt;br/&gt;    echo &amp;#34;✅ MSRV verification passed&amp;#34;&lt;br/&gt;&lt;br/&gt;_done_ci:&lt;br/&gt;    echo &amp;#34;🎉 CI pipeline completed successfully&amp;#34;&lt;br/&gt;&lt;br/&gt;# Update the changelog&lt;br/&gt;[script]&lt;br/&gt;changelog:&lt;br/&gt;    def get_hash [] { open &amp;#34;./CHANGELOG.md&amp;#34; | hash sha256 }&lt;br/&gt;&lt;br/&gt;    let old_hash = get_hash&lt;br/&gt;    git-cliff out&amp;gt; &amp;#34;CHANGELOG.md&amp;#34;&lt;br/&gt;&lt;br/&gt;    if old_hash != get_hash {&lt;br/&gt;        git add &amp;#34;CHANGELOG.md&amp;#34;&lt;br/&gt;        git commit -m &amp;#34;chore(changelog): Update the changelog&amp;#34;&lt;br/&gt;        print &amp;#34;The changes have been added to the changelog file and committed&amp;#34;&lt;br/&gt;    } else {&lt;br/&gt;        print &amp;#34;No changes have been added to the changelog&amp;#34;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;# Releases a new version of n34. Requires a clean file tree with no uncommitted changes.&lt;br/&gt;[script]&lt;br/&gt;release version:&lt;br/&gt;    let tag_msg = &amp;#34;Version {{ version }}&amp;#34; &#43; (git-cliff --strip all --unreleased --body &amp;#39;{{ tag_change_body }}&amp;#39;)&lt;br/&gt;    mut cargo_file = open &amp;#34;Cargo.toml&amp;#34;&lt;br/&gt;&lt;br/&gt;    $cargo_file.package.version = &amp;#34;{{ version }}&amp;#34;&lt;br/&gt;    $cargo_file | save -f &amp;#34;Cargo.toml&amp;#34;&lt;br/&gt;&lt;br/&gt;    RUST_LOG=none taplo fmt --config &amp;#34;./.taplo.toml&amp;#34;&lt;br/&gt;    {{ JUST_EXECUTABLE }} ci&lt;br/&gt;    git-cliff -t &amp;#34;v{{ version }}&amp;#34; out&amp;gt; &amp;#34;./CHANGELOG.md&amp;#34;&lt;br/&gt;    git add .&lt;br/&gt;    git commit -m &amp;#34;chore: Bump the version to `v{{ version }}`&amp;#34;&lt;br/&gt;    git tag -s -m $tag_msg &amp;#34;v{{ version }}&amp;#34;&lt;br/&gt;    git push origin master --tags&lt;br/&gt;    cargo publish&lt;br/&gt;&lt;br/&gt;# Deploy the book to Github Pages&lt;br/&gt;deploy:&lt;br/&gt;    mdbook build --dest-dir {{ BOOK_DEST_DIR }}&lt;br/&gt;    cd {{ BOOK_DEST_DIR }}&lt;br/&gt;    git init .&lt;br/&gt;    git checkout -B gh-pages&lt;br/&gt;    touch &amp;#34;.nojekyll&amp;#34;&lt;br/&gt;    echo &amp;#34;n34.dev&amp;#34; &amp;gt; &amp;#34;CNAME&amp;#34;&lt;br/&gt;&lt;br/&gt;    git add .&lt;br/&gt;    git commit -m &amp;#34;Deploy the book to github pages&amp;#34;&lt;br/&gt;    git remote add origin &amp;#34;git@github.com:TheAwiteb/n34-book&amp;#34;&lt;br/&gt;    git push origin gh-pages -f&lt;br/&gt;    cd ..&lt;br/&gt;    rm -fr {{ BOOK_DEST_DIR }}&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:55Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs0ly8k73l8h47jqmhuc2rplm4yklxyc4pwfqjteptlghudt3vjt5czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8e822u</id>
    
      <title type="html">{ &amp;#34;nodes&amp;#34;: { &amp;#34;flake-utils&amp;#34;: { &amp;#34;inputs&amp;#34;: { ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs0ly8k73l8h47jqmhuc2rplm4yklxyc4pwfqjteptlghudt3vjt5czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j8e822u" />
    <content type="html">
      {&lt;br/&gt;  &amp;#34;nodes&amp;#34;: {&lt;br/&gt;    &amp;#34;flake-utils&amp;#34;: {&lt;br/&gt;      &amp;#34;inputs&amp;#34;: {&lt;br/&gt;        &amp;#34;systems&amp;#34;: &amp;#34;systems&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1731533236,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-l0KFg5HjrsfsO/JpG&#43;r7fRrqm12kzFHyUHqHCVpMMbI=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;numtide&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;flake-utils&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;11707dc2f618dd54ca8739b309ec4fc024de578b&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;numtide&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;flake-utils&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;nixpkgs&amp;#34;: {&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1754498491,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;NixOS&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;c2ae88e026f9525daf89587f3cbee584b92b6134&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;NixOS&amp;#34;,&lt;br/&gt;        &amp;#34;ref&amp;#34;: &amp;#34;nixos-unstable&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;nixpkgs_2&amp;#34;: {&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1744536153,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;NixOS&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;18dd725c29603f582cf1900e0d25f9f1063dbf11&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;NixOS&amp;#34;,&lt;br/&gt;        &amp;#34;ref&amp;#34;: &amp;#34;nixpkgs-unstable&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;root&amp;#34;: {&lt;br/&gt;      &amp;#34;inputs&amp;#34;: {&lt;br/&gt;        &amp;#34;flake-utils&amp;#34;: &amp;#34;flake-utils&amp;#34;,&lt;br/&gt;        &amp;#34;nixpkgs&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;rust-overlay&amp;#34;: &amp;#34;rust-overlay&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;rust-overlay&amp;#34;: {&lt;br/&gt;      &amp;#34;inputs&amp;#34;: {&lt;br/&gt;        &amp;#34;nixpkgs&amp;#34;: &amp;#34;nixpkgs_2&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1754575663,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;oxalica&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;rust-overlay&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;oxalica&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;rust-overlay&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;systems&amp;#34;: {&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1681028828,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;nix-systems&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;default&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;da67096a3b9bf56a91d16901293e51ba5b49a27e&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;nix-systems&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;default&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    }&lt;br/&gt;  },&lt;br/&gt;  &amp;#34;root&amp;#34;: &amp;#34;root&amp;#34;,&lt;br/&gt;  &amp;#34;version&amp;#34;: 7&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:54Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqswqh9zllmdrxe6lk2smnc4c672wl75zea9u09m8suc5g7z0psgywqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jtjtpkc</id>
    
      <title type="html"># Show a Set &amp;gt; `n34 sets show` command **Usage:** ``` Show a ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqswqh9zllmdrxe6lk2smnc4c672wl75zea9u09m8suc5g7z0psgywqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jtjtpkc" />
    <content type="html">
      # Show a Set&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 sets show` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Show a single set or all the stored sets&lt;br/&gt;&lt;br/&gt;Usage: n34 sets show [NAME]&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [NAME]  Name of the set to display. If not provided, lists all available sets&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Prints all set names to standard output, each followed by its associated repos&lt;br/&gt;and relays. To view the details for a specific set, provide its name as an&lt;br/&gt;argument.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:54Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs9tsghxh2vtkdetr03paspntem2r8jpg9h7n4se0xmc56x86n0g6gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jz43t5k</id>
    
      <title type="html">{ inputs = { nixpkgs.url = ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs9tsghxh2vtkdetr03paspntem2r8jpg9h7n4se0xmc56x86n0g6gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jz43t5k" />
    <content type="html">
      {&lt;br/&gt;  inputs = {&lt;br/&gt;    nixpkgs.url = &amp;#34;github:NixOS/nixpkgs/nixos-unstable&amp;#34;;&lt;br/&gt;    rust-overlay.url = &amp;#34;github:oxalica/rust-overlay&amp;#34;;&lt;br/&gt;    flake-utils.url = &amp;#34;github:numtide/flake-utils&amp;#34;;&lt;br/&gt;  };&lt;br/&gt;&lt;br/&gt;  outputs =&lt;br/&gt;    {&lt;br/&gt;      nixpkgs,&lt;br/&gt;      rust-overlay,&lt;br/&gt;      flake-utils,&lt;br/&gt;      ...&lt;br/&gt;    }:&lt;br/&gt;    flake-utils.lib.eachDefaultSystem (&lt;br/&gt;      system:&lt;br/&gt;      let&lt;br/&gt;        overlays = [ (import rust-overlay) ];&lt;br/&gt;        pkgs = import nixpkgs { inherit system overlays; };&lt;br/&gt;      in&lt;br/&gt;      with pkgs;&lt;br/&gt;      {&lt;br/&gt;        devShells.default = mkShell {&lt;br/&gt;          packages = [&lt;br/&gt;            cargo-msrv&lt;br/&gt;            dbus&lt;br/&gt;            git-cliff&lt;br/&gt;            just&lt;br/&gt;            mdbook&lt;br/&gt;            nushell&lt;br/&gt;            pkg-config&lt;br/&gt;            taplo&lt;br/&gt;          ];&lt;br/&gt;&lt;br/&gt;          nativeBuildInputs = [&lt;br/&gt;            (lib.hiPrio rust-bin.nightly.&amp;#34;2025-08-07&amp;#34;.rustfmt)&lt;br/&gt;            rust-bin.stable.latest.default&lt;br/&gt;            rust-analyzer&lt;br/&gt;          ];&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        packages.default =&lt;br/&gt;          let&lt;br/&gt;            manifest = (pkgs.lib.importTOML ./Cargo.toml).package;&lt;br/&gt;          in&lt;br/&gt;          with pkgs;&lt;br/&gt;          rustPlatform.buildRustPackage {&lt;br/&gt;            pname = manifest.name;&lt;br/&gt;            version = manifest.version;&lt;br/&gt;            cargoLock.lockFile = ./Cargo.lock;&lt;br/&gt;            src = lib.cleanSource ./.;&lt;br/&gt;&lt;br/&gt;            nativeBuildInputs = [&lt;br/&gt;              pkg-config&lt;br/&gt;            ];&lt;br/&gt;&lt;br/&gt;            buildInputs = [&lt;br/&gt;              dbus&lt;br/&gt;            ];&lt;br/&gt;&lt;br/&gt;            meta = {&lt;br/&gt;              inherit (manifest) description homepage;&lt;br/&gt;              license = lib.licenses.gpl3Plus;&lt;br/&gt;            };&lt;br/&gt;          };&lt;br/&gt;      }&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:54Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqszpldy9nzpg53de57wjycsmgr7nj0vmtjtk54zfyuy4yqnlx774uczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j9gg3j3</id>
    
      <title type="html"># Create a Set &amp;gt; `n34 sets new` command **Usage:** ``` Create ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqszpldy9nzpg53de57wjycsmgr7nj0vmtjtk54zfyuy4yqnlx774uczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j9gg3j3" />
    <content type="html">
      # Create a Set&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 sets new` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Create a new set&lt;br/&gt;&lt;br/&gt;Usage: n34 sets new [OPTIONS] &amp;lt;NAME&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;NAME&amp;gt;  Unique name for the set&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --set-relay &amp;lt;RELAYS&amp;gt;         Optional relay to add it to the set, either as URL or set name to extract its relays. [aliases: `--sr`]&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Each set requires a unique name, provided as the final argument to the command.&lt;br/&gt;Use the `--set-relays`/`--sr` option to specify the relays for the new set;&lt;br/&gt;this can be a relay URL or the name of an existing set whose relays you wish to&lt;br/&gt;use. To add repositories, use the `--repo` option. Check [passing repositories]&lt;br/&gt;format.&lt;br/&gt;&lt;br/&gt;[passing repositories]: /commands.html#passing-repositories&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:54Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsphcxumjlpq0yhz0k2la32jfztnc0qrff3vh6e49j6pm6sqgk5zcqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j422lvg</id>
    
      <title type="html"># Remove a Set &amp;gt; `n34 sets remove` command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsphcxumjlpq0yhz0k2la32jfztnc0qrff3vh6e49j6pm6sqgk5zcqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j422lvg" />
    <content type="html">
      # Remove a Set&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 sets remove` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Remove a set, or specific repos and relays within it&lt;br/&gt;&lt;br/&gt;Usage: n34 sets remove [OPTIONS] &amp;lt;NAME&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;NAME&amp;gt;  Set name to delete&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --set-relay &amp;lt;RELAYS&amp;gt;         Specific relay to remove it from the set, either as URL or set name to extract its relays. [aliases: `--sr`]&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Removes an entire set, or specific repositories and relays from it.&lt;br/&gt;Without options, this command deletes the entire set.&lt;br/&gt;&lt;br/&gt;See the [passing repositories] section for more details on supported formats.&lt;br/&gt;&lt;br/&gt;[passing repositories]: /commands.html#passing-repositories&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:54Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsqmn6wndp7rfgwqggk5kka0tvyvygy8znne0l73zcaddjksknvl6gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0j5aan</id>
    
      <title type="html"># Modify a Set &amp;gt; `n34 sets update` command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsqmn6wndp7rfgwqggk5kka0tvyvygy8znne0l73zcaddjksknvl6gzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0j5aan" />
    <content type="html">
      # Modify a Set&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 sets update` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Modify an existing set&lt;br/&gt;&lt;br/&gt;Usage: n34 sets update [OPTIONS] &amp;lt;NAME&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;NAME&amp;gt;  Name of the set to update&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --set-relay &amp;lt;RELAYS&amp;gt;         Add relay to the set, either as URL or set name to extract its relays. [aliases: `--sr`]&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;      --override                   Replace existing relays/repositories instead of adding to them&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Use this command to update an existing set by its name. By default, providing&lt;br/&gt;relays via `--set-relay` or repositories via `--repo` will add them to the set&amp;#39;s&lt;br/&gt;existing entries. To replace the current relays and repositories with the new&lt;br/&gt;values, use the `--override` flag.&lt;br/&gt;&lt;br/&gt;[passing repositories]: /commands.html#passing-repositories&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:54Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsw9kxhgk3q7kx2selq84p3nz8ydaxfmn9n4z6pstxygct3sew95tszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jt3p4rf</id>
    
      <title type="html"># Merge an Open Patch &amp;gt; `n34 patch merge` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsw9kxhgk3q7kx2selq84p3nz8ydaxfmn9n4z6pstxygct3sew95tszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jt3p4rf" />
    <content type="html">
      # Merge an Open Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch merge` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Set an open patch status to merged&lt;br/&gt;&lt;br/&gt;Usage: n34 patch merge [OPTIONS] &amp;lt;PATCH_ID&amp;gt; &amp;lt;MERGE_COMMIT&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;      The open patch id to merge it. Must be orignal root patch or revision root&lt;br/&gt;  &amp;lt;MERGE_COMMIT&amp;gt;  The merge commit id&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;      --patches &amp;lt;PATCH-EVENT-ID&amp;gt;   Patches that have been merged. Use this when only some patches have been merged, not all&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Creates a kind `1631` event (Applied/Merged status) for the specified patch. The&lt;br/&gt;patch must be in open status.&lt;br/&gt;&lt;br/&gt;You can specify either an original patch or revision patch ID, but the status&lt;br/&gt;event will only reference the original patch. Revision patches will be mentioned&lt;br/&gt;in the event.&lt;br/&gt;&lt;br/&gt;You can get the `MERGE_COMMIT` commit using `git rev-parse HEAD` command if&lt;br/&gt;the merge commit in the `HEAD` or use `HEAD~n` where the `n` is the number of&lt;br/&gt;commits the merge commit before the HEAD&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:53Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs2ddmn9pske5hdl5l7p2wfvyqqjqlzjystcl2w865j9zwdv7s8nxczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jea6ty7</id>
    
      <title type="html"># Broadcast and Update a Git Repository &amp;gt; `n34 repo announce` ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs2ddmn9pske5hdl5l7p2wfvyqqjqlzjystcl2w865j9zwdv7s8nxczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jea6ty7" />
    <content type="html">
      # Broadcast and Update a Git Repository&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 repo announce` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Broadcast and update a git repository&lt;br/&gt;&lt;br/&gt;Usage: n34 repo announce [OPTIONS] --id &amp;lt;REPO_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --id &amp;lt;REPO_ID&amp;gt;               Unique identifier for the repository in kebab-case&lt;br/&gt;  -n, --name &amp;lt;NAME&amp;gt;                A name for the repository&lt;br/&gt;  -d, --description &amp;lt;DESCRIPTION&amp;gt;  A description for the repository&lt;br/&gt;  -w, --web &amp;lt;WEB&amp;gt;                  Webpage URLs for the repository (if provided by the git server)&lt;br/&gt;  -c, --clone &amp;lt;CLONE&amp;gt;              URLs for cloning the repository&lt;br/&gt;  -m, --maintainers &amp;lt;MAINTAINERS&amp;gt;  Additional maintainers of the repository (besides yourself)&lt;br/&gt;  -l, --label &amp;lt;LABEL&amp;gt;              Labels to categorize the repository. Can be specified multiple times&lt;br/&gt;      --force-id                   Skip kebab-case validation for the repository ID&lt;br/&gt;      --address-file               If set, creates a `nostr-address` file to enable automatic address discovery by n34&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command generates an announcement event to publish your project. It can be&lt;br/&gt;used to announce a new repository or update an existing one.&lt;br/&gt;&lt;br/&gt;When updating, you must resubmit all repository fields, not just the fields&lt;br/&gt;you wish to change. The command uses this information to build and publish a&lt;br/&gt;completely new announcement event that will replace the old one.&lt;br/&gt;&lt;br/&gt;It is recommended to use the `--address-file` flag. This option creates&lt;br/&gt;a `nostr-address` file that enables `n34` to automatically discover the&lt;br/&gt;repository&amp;#39;s address, simplifying the workflow for contributors.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:53Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs8zmyl7zqkezw64jywx337yr9crddr6kmnkg4f0lrxxatgcsxrmmqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jx9rcmf</id>
    
      <title type="html"># Send Patches to a Repository &amp;gt; `n34 patch send` command ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs8zmyl7zqkezw64jywx337yr9crddr6kmnkg4f0lrxxatgcsxrmmqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jx9rcmf" />
    <content type="html">
      # Send Patches to a Repository&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch send` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Send one or more patches to a repository&lt;br/&gt;&lt;br/&gt;Usage: n34 patch send [OPTIONS] &amp;lt;PATCH-PATH&amp;gt;...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH-PATH&amp;gt;...  List of patch files to send (space separated)&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;      --original-patch &amp;lt;EVENT-ID&amp;gt;  Original patch ID if this is a revision of it&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Send your generated patches to the repositories specified using the `--repo`&lt;br/&gt;option or retrieved from the `nostr-address` file. When submitting a revision&lt;br/&gt;of an existing patch, include the original patch ID to ensure it’s correctly&lt;br/&gt;referenced in your revision patch event.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:53Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs99e8f9kfvchg4r24epxe883dss83gc63dqfgwzffjqlpfpu22acgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j9rvzvw</id>
    
      <title type="html"># Reply to Issues and Patches &amp;gt; `n34 reply` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs99e8f9kfvchg4r24epxe883dss83gc63dqfgwzffjqlpfpu22acgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j9rvzvw" />
    <content type="html">
      # Reply to Issues and Patches&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 reply` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Reply to issues and patches&lt;br/&gt;&lt;br/&gt;Usage: n34 reply [OPTIONS] &amp;lt;--comment &amp;lt;COMMENT&amp;gt;|--editor&amp;gt; &amp;lt;nevent1-or-note1&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;nevent1-or-note1&amp;gt;  The issue, patch, or comment to reply to&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --quote-to                   Quote the replied-to event in the editor&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;  -c, --comment &amp;lt;COMMENT&amp;gt;          The comment (cannot be used with --editor)&lt;br/&gt;  -e, --editor                     Open editor to write comment (cannot be used with --content)&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Craft replies ([NIP-22] Comment) to issues, patches, or comments with ease&lt;br/&gt;using the `n34 reply` command. You can either input your reply directly with&lt;br/&gt;the `--comment` option or open an editor for a more detailed response using&lt;br/&gt;`--editor`. Additionally, when using `--editor`, the `--quote-to` option&lt;br/&gt;allows you to include the original content in your editor, enabling precise and&lt;br/&gt;context-aware replies.&lt;br/&gt;&lt;br/&gt;[NIP-22]: &lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/22.md&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/22.md&lt;/a&gt;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:53Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsrh864mt8fan6j4mgtj3s48g0m54rrm27ytkz6g4at3jdzu3d5vtszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jgc6kqs</id>
    
      <title type="html"># View Git Repository Details &amp;gt; `n34 repo view` command ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsrh864mt8fan6j4mgtj3s48g0m54rrm27ytkz6g4at3jdzu3d5vtszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jgc6kqs" />
    <content type="html">
      # View Git Repository Details&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 repo view` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;View details of a nostr git repository&lt;br/&gt;&lt;br/&gt;Usage: n34 repo view [NADDR-NIP05-OR-SET]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [NADDR-NIP05-OR-SET]...  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command prints repository details to standard output. If no arguments&lt;br/&gt;are provided, it looks for a `nostr-address` file in the current directory&lt;br/&gt;and displays the details for the address specified within it. See [passing&lt;br/&gt;repositories] for details on accepted formats.&lt;br/&gt;&lt;br/&gt;[passing repositories]: /commands.html#passing-repositories&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:53Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsrnj78xkltasmmslpjphk8wqmnehzfn3ugyygqyzkgnpu2e502qtczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jpgvhwx</id>
    
      <title type="html"># Managing Repository and Relay Sets Sets are a convenience ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsrnj78xkltasmmslpjphk8wqmnehzfn3ugyygqyzkgnpu2e502qtczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jpgvhwx" />
    <content type="html">
      # Managing Repository and Relay Sets&lt;br/&gt;&lt;br/&gt;Sets are a convenience feature for contributing to projects that do not have a&lt;br/&gt;`nostr-address` file. Instead of manually specifying the project&amp;#39;s repositories&lt;br/&gt;and relays for every command, you can define them once as a named &amp;#34;set&amp;#34;. You can&lt;br/&gt;then reference this set by its name in commands. This allows you to use the set&lt;br/&gt;as a shortcut for a list of relays (`--relays &amp;lt;set_name&amp;gt;`) or as the project&amp;#39;s&lt;br/&gt;address in commands like `issue` and `patch`.&lt;br/&gt;&lt;br/&gt;Sets are defined in your configuration file. To use a specific configuration&lt;br/&gt;file, pass its path using the `--config` option.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:53Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspl7u45shu280tt92w75wssd057cu5wh0mtde3d9n6vkksvc4xd7czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jju2lth</id>
    
      <title type="html"># Reopens a Closed or Drafted Patch &amp;gt; `n34 patch reopen` ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspl7u45shu280tt92w75wssd057cu5wh0mtde3d9n6vkksvc4xd7czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jju2lth" />
    <content type="html">
      # Reopens a Closed or Drafted Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch reopen` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Reopens a closed or drafted patch&lt;br/&gt;&lt;br/&gt;Usage: n34 patch reopen [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The closed/drafted patch id to reopen it. Must be orignal root patch&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1632` (Close status) for the specified patch. The patch have to&lt;br/&gt;be closed or drafted.&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:53Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsp0lplrz3je43algaenru8y3s9r7y2vgxpq7f5kz7f29dawpv5qyqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jujnsav</id>
    
      <title type="html"># Manage Repositories In `n34` you can manage your repositories. ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsp0lplrz3je43algaenru8y3s9r7y2vgxpq7f5kz7f29dawpv5qyqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jujnsav" />
    <content type="html">
      # Manage Repositories&lt;br/&gt;&lt;br/&gt;In `n34` you can manage your repositories. This includes announcing new ones,&lt;br/&gt;viewing existing ones, and announcing state updates (coming soon).&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:53Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsy96ql7x7wn0qlz4a0t78lhs6vgm28jg4lhg50y68znrs0h39fymqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j5aazg8</id>
    
      <title type="html"># List Repositories Patches &amp;gt; `n34 patch list` command ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsy96ql7x7wn0qlz4a0t78lhs6vgm28jg4lhg50y68znrs0h39fymqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j5aazg8" />
    <content type="html">
      # List Repositories Patches&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch list` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;List the repositories patches&lt;br/&gt;&lt;br/&gt;Usage: n34 patch list [OPTIONS] [NADDR-NIP05-OR-SET]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [NADDR-NIP05-OR-SET]...  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --limit &amp;lt;LIMIT&amp;gt;  Maximum number of patches to list [default: 15]&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;List the repositories patches. By default `n34` will look for `nostr-address`&lt;br/&gt;file and extract the repositories from it.&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:48Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqs04f3qjkvttth7328ek86h2v6xlhvjysc5pjvu9nxhpvurvzqg5cgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jyu4ga0</id>
    
      <title type="html"># Fetch a Patch By ID &amp;gt; `n34 patch fetch` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqs04f3qjkvttth7328ek86h2v6xlhvjysc5pjvu9nxhpvurvzqg5cgzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jyu4ga0" />
    <content type="html">
      # Fetch a Patch By ID&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch fetch` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Fetches a patch by its id&lt;br/&gt;&lt;br/&gt;Usage: n34 patch fetch [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The patch id to fetch it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;  -o, --output &amp;lt;PATH&amp;gt;              Output directory for the patches. Default to the current directory&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Fetches patches using their original patch ID. All fetched patches will be saved&lt;br/&gt;to the specified output directory (current directory by default). You can then&lt;br/&gt;apply or merge these patches into your branch as needed.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsw705gvzjxlh55z4u2tkjd4pfwqyf8kjf8kjeypm0v8y5lydxcscszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jv49g76</id>
    
      <title type="html"># List Repositories Issues &amp;gt; `n34 issue list` command ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsw705gvzjxlh55z4u2tkjd4pfwqyf8kjf8kjeypm0v8y5lydxcscszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jv49g76" />
    <content type="html">
      # List Repositories Issues&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue list` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;List the repositories issues&lt;br/&gt;&lt;br/&gt;Usage: n34 issue list [OPTIONS] [NADDR-NIP05-OR-SET]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [NADDR-NIP05-OR-SET]...  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --limit &amp;lt;LIMIT&amp;gt;  Maximum number of issues to list [default: 15]&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;List the repositories issues. By default `n34` will look for `nostr-address`&lt;br/&gt;file and extract the repositories from it.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsdasamrwm32khsep2ep6xx2e4s520gsudrw4xxt3222crfps8w3gqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j7a0x5r</id>
    
      <title type="html"># Draft an Open Patch &amp;gt; `n34 patch draft` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsdasamrwm32khsep2ep6xx2e4s520gsudrw4xxt3222crfps8w3gqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j7a0x5r" />
    <content type="html">
      # Draft an Open Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch draft` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Converts an open patch to draft state&lt;br/&gt;&lt;br/&gt;Usage: n34 patch draft [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The open patch id to draft it. Must be orignal root patch&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1633` (Draft status) for the specified patch. The patch have to&lt;br/&gt;be open.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqst3f8488usayjar9qwumerla0tga600544hvhletxtpqgkdkaer2qzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j05k75h</id>
    
      <title type="html"># Resolves an Issue &amp;gt; `n34 issue resolve` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqst3f8488usayjar9qwumerla0tga600544hvhletxtpqgkdkaer2qzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j05k75h" />
    <content type="html">
      # Resolves an Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue resolve` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Resolves an issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue resolve [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The issue id to resolve it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1631` (Resolved status) event for the specified issue.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqst8fy4r20zcvvfplvk8nguey9euhygl5f64xdlpw09lp4mu6qhczqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0vm863</id>
    
      <title type="html"># NIP-07 Browser Signer Proxy &amp;gt; `n34 config nip07` **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqst8fy4r20zcvvfplvk8nguey9euhygl5f64xdlpw09lp4mu6qhczqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0vm863" />
    <content type="html">
      # NIP-07 Browser Signer Proxy&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 config nip07`&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Manage the NIP-07 browser signer proxy by enabling or disabling it and configuring the `ip:port` address.&lt;br/&gt;&lt;br/&gt;Usage: n34 config nip07 [OPTIONS] &amp;lt;--enable|--disable&amp;gt;&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --enable       Enable NIP-07 as the default signer&lt;br/&gt;      --disable      Disable NIP-07 as the default signer&lt;br/&gt;      --addr &amp;lt;ADDR&amp;gt;  Set the `ip:port` for the browser signer proxy (default: 127.0.0.1:51034)&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Use [NIP-07] (Browser Extension Signer) as your default signer. This is achieved&lt;br/&gt;by running a proxy at the specified `ADDR`, which defaults to `127.0.0.1:51034`.&lt;br/&gt;The proxy forwards `n34` requests to the browser signer and relays the responses&lt;br/&gt;back.&lt;br/&gt;&lt;br/&gt;[NIP-07]: &lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/07.md&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/07.md&lt;/a&gt;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsfpkhsxal5lzrv7uj59jcj78pvrx87eggce54kv30jum2kqdnq8uszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jjh8qsd</id>
    
      <title type="html"># Closes an Open Issue &amp;gt; `n34 issue close` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsfpkhsxal5lzrv7uj59jcj78pvrx87eggce54kv30jum2kqdnq8uszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jjh8qsd" />
    <content type="html">
      # Closes an Open Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue close` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Closes an open issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue close [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The open issue id to close it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1632` (Close status) for the specified issue. The issue have to&lt;br/&gt;be open.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsgmkjvccwjkhl2an2redvrvpm9k9vak95h0y4cafvsvma6u2rxpygzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0y9we5</id>
    
      <title type="html"># View an Issue By ID &amp;gt; `n34 issue view` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsgmkjvccwjkhl2an2redvrvpm9k9vak95h0y4cafvsvma6u2rxpygzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j0y9we5" />
    <content type="html">
      # View an Issue By ID&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue view` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;View an issue by its ID&lt;br/&gt;&lt;br/&gt;Usage: n34 issue view [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The issue id to view it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Simply provide the issue ID in `note` or `nevent` format to retrieve and display&lt;br/&gt;the issue details.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsxmwrezxrqd8hpdpvl5u0qjx4ezysrxqc2ff6d5dm8ylwsps65vpczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jdkcc6k</id>
    
      <title type="html"># Default PoW Difficulty &amp;gt; `n34 config pow` **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsxmwrezxrqd8hpdpvl5u0qjx4ezysrxqc2ff6d5dm8ylwsps65vpczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jdkcc6k" />
    <content type="html">
      # Default PoW Difficulty&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 config pow`&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Sets the default PoW difficulty (0 if not specified)&lt;br/&gt;&lt;br/&gt;Usage: n34 config pow &amp;lt;DIFFICULTY&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;DIFFICULTY&amp;gt;  The new default PoW difficulty&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command configures the default Proof of Work (PoW) difficulty for newly&lt;br/&gt;created events. This setting is applied to most generated events, but it&lt;br/&gt;intentionally skips patch events. Because patches can be numerous, calculating&lt;br/&gt;PoW for each one would significantly slow down operations.&lt;br/&gt;&lt;br/&gt;If you want to disable the PoW just make it 0.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsxywcaplz6r0z629putjjl5cf2e6nyguvspcrh879twngyvh549qqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jlym0wp</id>
    
      <title type="html"># Apply an Open Patch &amp;gt; `n34 patch apply` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsxywcaplz6r0z629putjjl5cf2e6nyguvspcrh879twngyvh549qqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jlym0wp" />
    <content type="html">
      # Apply an Open Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch apply` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Set an open patch status to applied&lt;br/&gt;&lt;br/&gt;Usage: n34 patch apply [OPTIONS] &amp;lt;PATCH_ID&amp;gt; [APPLIED_COMMITS]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;            The open patch id to apply it. Must be orignal root patch or revision root&lt;br/&gt;  [APPLIED_COMMITS]...  The applied commits&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;      --patches &amp;lt;PATCH-EVENT-ID&amp;gt;   Patches that have been applied. Use this when only some patches have been applied, not all&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Creates a kind `1631` event (Applied/Merged status) for the specified patch. The&lt;br/&gt;patch must be in open status.&lt;br/&gt;&lt;br/&gt;You can specify either an original patch or revision patch ID, but the status&lt;br/&gt;event will only reference the original patch. Revision patches will be mentioned&lt;br/&gt;in the event.&lt;br/&gt;&lt;br/&gt;The `APPLIED_COMMITS` field serves to inform clients about the status of&lt;br/&gt;specific commits, whether they have been applied or not. If you need to retrieve&lt;br/&gt;the list of commits from a specific point (such as the tip of the master branch)&lt;br/&gt;up to the `HEAD`, you can use the following Git command: `git log --pretty=%H&lt;br/&gt;&amp;#39;origin/master..HEAD&amp;#39;`.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsyhn3f6xlk9xpefxgrw8lqa7v0udw87lw5czfc2ke37qqmffaj29szyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6je8fuzv</id>
    
      <title type="html"># Fallback Relays &amp;gt; `n34 config relays` command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsyhn3f6xlk9xpefxgrw8lqa7v0udw87lw5czfc2ke37qqmffaj29szyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6je8fuzv" />
    <content type="html">
      # Fallback Relays&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 config relays` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Sets the default fallback relays if none provided. Use this relays for read and write&lt;br/&gt;&lt;br/&gt;Usage: n34 config relays [OPTIONS] [RELAYS]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [RELAYS]...  List of relay URLs to append to fallback relays. If empty, removes all fallback relays&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --override  Replace existing fallback relays instead of appending new ones&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command configures the default fallback relays, which `n34` uses to read&lt;br/&gt;from and write to. To add relays, provide their URLs as arguments to append&lt;br/&gt;them to the current list. Use the `--override` flag to replace the existing list&lt;br/&gt;entirely. To clear all fallback relays, run the command without any arguments.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsykd6rl23t6nkgvjz2qf9urk4nxnfjlpxy7uftul9m67asu7qutaszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j3z25aj</id>
    
      <title type="html">&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt; &amp;lt;head&amp;gt; ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsykd6rl23t6nkgvjz2qf9urk4nxnfjlpxy7uftul9m67asu7qutaszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j3z25aj" />
    <content type="html">
      &amp;lt;!DOCTYPE html&amp;gt;&lt;br/&gt;&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;&lt;br/&gt;  &amp;lt;head&amp;gt;&lt;br/&gt;    &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34; /&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;meta property=&amp;#34;og:title&amp;#34; content=&amp;#34;n34 - CLI for NIP-34 and Nostr Code Collaboration&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta property=&amp;#34;og:description&amp;#34; content=&amp;#34;An open source CLI for sending and receiving Git issues, patches and comments over the Nostr protocol.&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta property=&amp;#34;og:url&amp;#34; content=&amp;#34;&lt;a href=&#34;https://n34.dev&amp;#34;&amp;gt&#34;&gt;https://n34.dev&amp;#34;&amp;gt&lt;/a&gt;;&lt;br/&gt;    &amp;lt;meta property=&amp;#34;og:type&amp;#34; content=&amp;#34;website&amp;#34;&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;twitter:card&amp;#34; content=&amp;#34;summary&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;twitter:title&amp;#34; content=&amp;#34;n34 - CLI for NIP-34 and Nostr Code Collaboration&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;twitter:description&amp;#34; content=&amp;#34;An open source CLI for sending and receiving Git issues, patches and comments over the Nostr protocol.&amp;#34;&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34; /&amp;gt;&lt;br/&gt;    &amp;lt;title&amp;gt;n34 - CLI for NIP-34 and Nostr Code Collaboration&amp;lt;/title&amp;gt;&lt;br/&gt;    &amp;lt;style&amp;gt;&lt;br/&gt;      * {&lt;br/&gt;        margin: 0;&lt;br/&gt;        padding: 0;&lt;br/&gt;        box-sizing: border-box;&lt;br/&gt;      }&lt;br/&gt;      body {&lt;br/&gt;        font-family: &amp;#34;Segoe UI&amp;#34;, Tahoma, Geneva, Verdana, sans-serif;&lt;br/&gt;        background: radial-gradient(125% 125% at 50% 90%, #000000 40%, #072607 100%);&lt;br/&gt;        color: #ffffff;&lt;br/&gt;        line-height: 1.6;&lt;br/&gt;        min-height: 100vh;&lt;br/&gt;      }&lt;br/&gt;      .container {&lt;br/&gt;        max-width: 800px;&lt;br/&gt;        margin: 0 auto;&lt;br/&gt;        padding: 60px 20px;&lt;br/&gt;      }&lt;br/&gt;      .hero {&lt;br/&gt;        text-align: center;&lt;br/&gt;        margin-bottom: 80px;&lt;br/&gt;      }&lt;br/&gt;      .hero h1 {&lt;br/&gt;        font-size: 4rem;&lt;br/&gt;        font-weight: 300;&lt;br/&gt;        margin-bottom: 20px;&lt;br/&gt;        letter-spacing: 2px;&lt;br/&gt;        background: linear-gradient(135deg, #ffffff, #cccccc);&lt;br/&gt;        -webkit-background-clip: text;&lt;br/&gt;        -webkit-text-fill-color: transparent;&lt;br/&gt;        background-clip: text;&lt;br/&gt;      }&lt;br/&gt;      .hero p {&lt;br/&gt;        font-size: 1.2rem;&lt;br/&gt;        color: #cccccc;&lt;br/&gt;        margin-bottom: 40px;&lt;br/&gt;        max-width: 600px;&lt;br/&gt;        margin-left: auto;&lt;br/&gt;        margin-right: auto;&lt;br/&gt;      }&lt;br/&gt;      .buttons {&lt;br/&gt;        display: flex;&lt;br/&gt;        gap: 20px;&lt;br/&gt;        justify-content: center;&lt;br/&gt;        flex-wrap: wrap;&lt;br/&gt;      }&lt;br/&gt;      .btn {&lt;br/&gt;        display: inline-block;&lt;br/&gt;        padding: 14px 28px;&lt;br/&gt;        background: transparent;&lt;br/&gt;        border: 2px solid #ffffff;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;        text-decoration: none;&lt;br/&gt;        border-radius: 8px;&lt;br/&gt;        font-weight: 500;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        transition: all 0.3s ease;&lt;br/&gt;        min-width: 120px;&lt;br/&gt;        text-align: center;&lt;br/&gt;      }&lt;br/&gt;      .btn:hover {&lt;br/&gt;        background: #ffffff;&lt;br/&gt;        color: #0a0a0a;&lt;br/&gt;        transform: translateY(-2px);&lt;br/&gt;        box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);&lt;br/&gt;      }&lt;br/&gt;      .section {&lt;br/&gt;        margin-bottom: 60px;&lt;br/&gt;      }&lt;br/&gt;      .section h2 {&lt;br/&gt;        font-size: 2.2rem;&lt;br/&gt;        margin-bottom: 30px;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;        font-weight: 400;&lt;br/&gt;      }&lt;br/&gt;      .section p {&lt;br/&gt;        font-size: 1.1rem;&lt;br/&gt;        color: #e0e0e0;&lt;br/&gt;        margin-bottom: 20px;&lt;br/&gt;        text-align: justify;&lt;br/&gt;      }&lt;br/&gt;      a {&lt;br/&gt;        color: #cccccc;&lt;br/&gt;        text-decoration: underline;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        transition: color 0.3s ease;&lt;br/&gt;      }&lt;br/&gt;      a:hover {&lt;br/&gt;        color: #ffffff;&lt;br/&gt;      }&lt;br/&gt;      .features {&lt;br/&gt;        display: grid;&lt;br/&gt;        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));&lt;br/&gt;        gap: 30px;&lt;br/&gt;        margin-bottom: 60px;&lt;br/&gt;      }&lt;br/&gt;      .feature {&lt;br/&gt;        background: #1a1a1a;&lt;br/&gt;        padding: 30px;&lt;br/&gt;        border-radius: 12px;&lt;br/&gt;        border: 1px solid #333;&lt;br/&gt;        transition: transform 0.3s ease, border-color 0.3s ease;&lt;br/&gt;      }&lt;br/&gt;      .feature:hover {&lt;br/&gt;        transform: translateY(-5px);&lt;br/&gt;        border-color: #555;&lt;br/&gt;      }&lt;br/&gt;      .feature h3 {&lt;br/&gt;        font-size: 1.3rem;&lt;br/&gt;        margin-bottom: 15px;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;        display: flex;&lt;br/&gt;        align-items: center;&lt;br/&gt;        gap: 10px;&lt;br/&gt;      }&lt;br/&gt;      .feature-icon {&lt;br/&gt;        font-size: 1.5rem;&lt;br/&gt;      }&lt;br/&gt;      .feature p {&lt;br/&gt;        color: #cccccc;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        text-align: left;&lt;br/&gt;      }&lt;br/&gt;      .feature-list {&lt;br/&gt;        background: #111;&lt;br/&gt;        padding: 40px;&lt;br/&gt;        border-radius: 12px;&lt;br/&gt;        border: 1px solid #333;&lt;br/&gt;        margin-bottom: 60px;&lt;br/&gt;      }&lt;br/&gt;      .feature-grid {&lt;br/&gt;        display: grid;&lt;br/&gt;        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));&lt;br/&gt;        gap: 20px;&lt;br/&gt;        margin-top: 20px;&lt;br/&gt;      }&lt;br/&gt;      .feature-column ul {&lt;br/&gt;        list-style: none;&lt;br/&gt;        padding: 0;&lt;br/&gt;      }&lt;br/&gt;      .feature-column li {&lt;br/&gt;        display: flex;&lt;br/&gt;        align-items: center;&lt;br/&gt;        margin-bottom: 12px;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        padding: 8px 0;&lt;br/&gt;      }&lt;br/&gt;      .feature-check {&lt;br/&gt;        display: inline-block;&lt;br/&gt;        width: 20px;&lt;br/&gt;        height: 20px;&lt;br/&gt;        margin-right: 12px;&lt;br/&gt;        border-radius: 3px;&lt;br/&gt;        text-align: center;&lt;br/&gt;        line-height: 20px;&lt;br/&gt;        font-size: 12px;&lt;br/&gt;        font-weight: bold;&lt;br/&gt;        flex-shrink: 0;&lt;br/&gt;      }&lt;br/&gt;      .feature-check.completed {&lt;br/&gt;        background: #22c55e;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;      }&lt;br/&gt;      .feature-check.pending {&lt;br/&gt;        background: #374151;&lt;br/&gt;        color: #9ca3af;&lt;br/&gt;        border: 1px solid #4b5563;&lt;br/&gt;      }&lt;br/&gt;      .feature-check.completed::after {&lt;br/&gt;        content: &amp;#34;✓&amp;#34;;&lt;br/&gt;      }&lt;br/&gt;      .feature-check.pending::after {&lt;br/&gt;        content: &amp;#34;○&amp;#34;;&lt;br/&gt;      }&lt;br/&gt;      .feature-text {&lt;br/&gt;        color: #e0e0e0;&lt;br/&gt;      }&lt;br/&gt;      .feature-text.pending {&lt;br/&gt;        color: #9ca3af;&lt;br/&gt;      }&lt;br/&gt;      .install-section {&lt;br/&gt;        background: #111;&lt;br/&gt;        padding: 40px;&lt;br/&gt;        border-radius: 12px;&lt;br/&gt;        border: 1px solid #333;&lt;br/&gt;        margin-bottom: 60px;&lt;br/&gt;      }&lt;br/&gt;      .install-section h2 {&lt;br/&gt;        margin-bottom: 20px;&lt;br/&gt;        text-align: center;&lt;br/&gt;      }&lt;br/&gt;      .code-block {&lt;br/&gt;        background: #000;&lt;br/&gt;        padding: 20px;&lt;br/&gt;        border-radius: 8px;&lt;br/&gt;        font-family: &amp;#34;Courier New&amp;#34;, monospace;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        border: 1px solid #333;&lt;br/&gt;        overflow-x: auto;&lt;br/&gt;        margin: 15px 0;&lt;br/&gt;      }&lt;br/&gt;      .code-block p {&lt;br/&gt;        color: #00ff00;&lt;br/&gt;      }&lt;br/&gt;      .code-block p::before {&lt;br/&gt;        content: &amp;#34;$ &amp;#34;;&lt;br/&gt;        color: #888;&lt;br/&gt;      }&lt;br/&gt;      .code-block span::before {&lt;br/&gt;        content: &amp;#34;$ &amp;#34;;&lt;br/&gt;        color: #888;&lt;br/&gt;      }&lt;br/&gt;      .code-block span {&lt;br/&gt;        color: #777;&lt;br/&gt;      }&lt;br/&gt;      .code-block pre {&lt;br/&gt;        color: #00ff00;&lt;br/&gt;      }&lt;br/&gt;      .links {&lt;br/&gt;        margin-top: 30px;&lt;br/&gt;      }&lt;br/&gt;      .links h3 {&lt;br/&gt;        font-size: 1.3rem;&lt;br/&gt;        margin-bottom: 15px;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;      }&lt;br/&gt;      .links ul {&lt;br/&gt;        list-style: none;&lt;br/&gt;      }&lt;br/&gt;      .links li {&lt;br/&gt;        margin-bottom: 8px;&lt;br/&gt;      }&lt;br/&gt;      .links a {&lt;br/&gt;        color: #cccccc;&lt;br/&gt;        text-decoration: none;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        transition: color 0.3s ease;&lt;br/&gt;      }&lt;br/&gt;      .links a:hover {&lt;br/&gt;        color: #ffffff;&lt;br/&gt;      }&lt;br/&gt;      .links a:before {&lt;br/&gt;        content: &amp;#34;→ &amp;#34;;&lt;br/&gt;        margin-right: 8px;&lt;br/&gt;      }&lt;br/&gt;      .status-badge {&lt;br/&gt;        display: inline-block;&lt;br/&gt;        padding: 4px 12px;&lt;br/&gt;        background: #1a4d1a;&lt;br/&gt;        color: #4ade80;&lt;br/&gt;        border-radius: 16px;&lt;br/&gt;        font-size: 0.8rem;&lt;br/&gt;        font-weight: 500;&lt;br/&gt;        margin-bottom: 20px;&lt;br/&gt;      }&lt;br/&gt;      .footer {&lt;br/&gt;        text-align: center;&lt;br/&gt;        padding: 40px 0;&lt;br/&gt;        border-top: 1px solid #333;&lt;br/&gt;        margin-top: 80px;&lt;br/&gt;        color: #888;&lt;br/&gt;      }&lt;br/&gt;      @media (max-width: 768px) {&lt;br/&gt;        .hero h1 {&lt;br/&gt;          font-size: 2.5rem;&lt;br/&gt;        }&lt;br/&gt;        .hero p {&lt;br/&gt;          font-size: 1.1rem;&lt;br/&gt;        }&lt;br/&gt;        .buttons {&lt;br/&gt;          flex-direction: column;&lt;br/&gt;          align-items: center;&lt;br/&gt;        }&lt;br/&gt;        .btn {&lt;br/&gt;          width: 200px;&lt;br/&gt;        }&lt;br/&gt;        .section h2 {&lt;br/&gt;          font-size: 1.8rem;&lt;br/&gt;        }&lt;br/&gt;        .features {&lt;br/&gt;          grid-template-columns: 1fr;&lt;br/&gt;        }&lt;br/&gt;        .feature-grid {&lt;br/&gt;          grid-template-columns: 1fr;&lt;br/&gt;        }&lt;br/&gt;        .install-section,&lt;br/&gt;        .feature-list {&lt;br/&gt;          padding: 20px;&lt;br/&gt;        }&lt;br/&gt;      }&lt;br/&gt;    &amp;lt;/style&amp;gt;&lt;br/&gt;  &amp;lt;/head&amp;gt;&lt;br/&gt;  &amp;lt;body&amp;gt;&lt;br/&gt;    &amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;hero&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;status-badge&amp;#34;&amp;gt;✨ Looking for feedback&amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;h1&amp;gt;n34&amp;lt;/h1&amp;gt;&lt;br/&gt;        &amp;lt;p&amp;gt;&lt;br/&gt;          n34 is an open source command-line interface (CLI) tool for&lt;br/&gt;          sending and receiving Git issues, patches and comments over the&lt;br/&gt;          Nostr protocol. It supports creating, replying to, and managing&lt;br/&gt;          issues and patches, making Git collaboration decentralized and&lt;br/&gt;          censorship-resistant.&lt;br/&gt;        &amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;buttons&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;a href=&amp;#34;&lt;a href=&#34;https://n34.dev/commands.html&amp;#34&#34;&gt;https://n34.dev/commands.html&amp;#34&lt;/a&gt;; class=&amp;#34;btn&amp;#34;&amp;gt;Documentation&amp;lt;/a&amp;gt;&lt;br/&gt;          &amp;lt;a href=&amp;#34;&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;; class=&amp;#34;btn&amp;#34;&amp;gt;Git Repository&amp;lt;/a&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;section&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;h2&amp;gt;Key Features&amp;lt;/h2&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;features&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h3&amp;gt;&amp;lt;span class=&amp;#34;feature-icon&amp;#34;&amp;gt;🔄&amp;lt;/span&amp;gt;Complete Git Workflow&amp;lt;/h3&amp;gt;&lt;br/&gt;            &amp;lt;p&amp;gt;Handle the full development lifecycle with patches, issues, replies, and status tracking, all through the decentralized Nostr protocol.&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h3&amp;gt;&amp;lt;span class=&amp;#34;feature-icon&amp;#34;&amp;gt;🔗&amp;lt;/span&amp;gt;&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/34.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/34.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-34&amp;lt;/a&amp;gt; Compliant&amp;lt;/h3&amp;gt;&lt;br/&gt;            &amp;lt;p&amp;gt;Fully implements the &amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/34.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/34.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-34&amp;lt;/a&amp;gt; specification for Git repositories on Nostr, ensuring compatibility with the decentralized ecosystem.&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h3&amp;gt;&amp;lt;span class=&amp;#34;feature-icon&amp;#34;&amp;gt;🛠️&amp;lt;/span&amp;gt;Developer Friendly&amp;lt;/h3&amp;gt;&lt;br/&gt;            &amp;lt;p&amp;gt;Intuitive CLI interface designed for developers who want to integrate Git workflows with Nostr seamlessly.&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h3&amp;gt;&amp;lt;span class=&amp;#34;feature-icon&amp;#34;&amp;gt;🔐&amp;lt;/span&amp;gt;Self-Sovereign&amp;lt;/h3&amp;gt;&lt;br/&gt;            &amp;lt;p&amp;gt;No accounts, no passwords, no centralized servers. You control your identity and data through cryptographic keys.&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;feature-list&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;h2&amp;gt;Feature Roadmap&amp;lt;/h2&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-bottom: 30px; color: #cccccc;&amp;#34;&amp;gt;Current implementation status and upcoming features&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;feature-grid&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature-column&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;ul&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Repository announcements&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check pending&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text pending&amp;#34;&amp;gt;Repository state announcements&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Patches (Send, fetch and list)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Issues (Send, view and list)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Replies&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Issues and patches status&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check pending&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text pending&amp;#34;&amp;gt;Pull requests (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/pull/1966&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/pull/1966&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;nostr-protocol/nips#1966&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;            &amp;lt;/ul&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature-column&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;ul&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Gossip Model (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/65.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/65.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-65&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Proof of Work (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/13.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/13.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-13&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;nostr: URI scheme (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/21.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/21.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-21&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Signing using bunker (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/46.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/46.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-46&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text pending&amp;#34;&amp;gt;Signing using &amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/07.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/07.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-07&amp;lt;/a&amp;gt; proxy (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://crates.io/crates/nostr-browser-signer-proxy&amp;#34&#34;&gt;https://crates.io/crates/nostr-browser-signer-proxy&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;nostr-browser-signer-proxy&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Secret key keyring&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check pending&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text pending&amp;#34;&amp;gt;Code Snippets (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/C0.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/C0.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-C0&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;In device relays and repos bookmark&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;            &amp;lt;/ul&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;install-section&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;h2&amp;gt;Quick Start&amp;lt;/h2&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-bottom: 20px; color: #cccccc;&amp;#34;&amp;gt;Get started with n34 in seconds&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;p&amp;gt;cargo install n34&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;span&amp;gt;# The execautable will be in ~/.cargo/bin/n34&amp;lt;/span&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin: 20px 0; color: #888;&amp;#34;&amp;gt;or&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;p&amp;gt;git clone &lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git&amp;lt;/p&amp;gt&#34;&gt;https://git.4rs.nl/awiteb/n34.git&amp;lt;/p&amp;gt&lt;/a&gt;;&lt;br/&gt;          &amp;lt;p&amp;gt;cd n34 &amp;amp;&amp;amp; cargo build --release&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;span&amp;gt;# The execautable will be in target/release/n34&amp;lt;/span&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin: 20px 0; color: #888;&amp;#34;&amp;gt;NixOS (Version 0.4.0 or later)&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;p&amp;gt;git clone &lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git&amp;lt;/p&amp;gt&#34;&gt;https://git.4rs.nl/awiteb/n34.git&amp;lt;/p&amp;gt&lt;/a&gt;;&lt;br/&gt;          &amp;lt;p&amp;gt;cd n34 &amp;amp;&amp;amp; nix build&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;span&amp;gt;# The execautable will be in result/bin/n34&amp;lt;/span&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin: 20px 0; color: #888;&amp;#34;&amp;gt;home-manager&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-bottom: 20px; color: #cccccc;&amp;#34;&amp;gt;Add this to your &amp;lt;code&amp;gt;flake.nix&amp;lt;/code&amp;gt; inputs&lt;br/&gt;        &amp;lt;br&amp;gt;&lt;br/&gt;Specify the version you want to install, or remove &amp;lt;code&amp;gt;?ref&amp;lt;/code&amp;gt; for the unreleased version. You can also use any mirror; it doesn&amp;#39;t have to be &amp;lt;code&amp;gt;git.4rs.nl&amp;lt;/code&amp;gt;&lt;br/&gt;        &amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;pre&amp;gt;&lt;br/&gt;inputs = {&lt;br/&gt;  n34.url = &amp;#34;git&#43;&lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git?ref=refs/tags/vx.y.x&amp;#34&#34;&gt;https://git.4rs.nl/awiteb/n34.git?ref=refs/tags/vx.y.x&amp;#34&lt;/a&gt;;;&lt;br/&gt;};&amp;lt;/pre&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-bottom: 20px; color: #cccccc;&amp;#34;&amp;gt;And this in your home packages&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;pre&amp;gt;packages = [ inputs.n34.packages.&amp;#34;${pkgs.system}&amp;#34;.default ];&amp;lt;/pre&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-top: 20px; color: #cccccc;&amp;#34;&amp;gt;Once installed, run &amp;lt;code style=&amp;#34;background: #333; padding: 2px 6px; border-radius: 4px;&amp;#34;&amp;gt;n34 --help&amp;lt;/code&amp;gt; to see available commands.&amp;lt;/p&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;section&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;h2&amp;gt;Why Nostr?&amp;lt;/h2&amp;gt;&lt;br/&gt;        &amp;lt;p&amp;gt;&lt;br/&gt;          Nostr is fundamentally different from traditional platforms because it&amp;#39;s not an application or service, it&amp;#39;s a decentralized protocol. This means any tool or app can integrate with it, enabling open, permissionless collaboration&lt;br/&gt;          without relying on centralized gatekeepers. Unlike proprietary systems, Nostr doesn&amp;#39;t require emails, passwords, or accounts. You interact directly through relays, whether you self-host your own or use public ones, ensuring no&lt;br/&gt;          single point of failure or control.&lt;br/&gt;        &amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;p&amp;gt;&lt;br/&gt;          What makes Nostr uniquely resilient is its design, the protocol itself is just a set of rules, not a company or product that can disappear. Your Git issues, patches, and comments persist as long as relays choose to store them,&lt;br/&gt;          immune to the whims of corporate shutdowns or policy changes. Nostr is infrastructure in its purest form, an idea that outlives any temporary implementation. n34 taps into a future-proof foundation for decentralized collaboration.&lt;br/&gt;        &amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;links&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;h3&amp;gt;More about Nostr&amp;lt;/h3&amp;gt;&lt;br/&gt;          &amp;lt;ul&amp;gt;&lt;br/&gt;            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://nostr.com&amp;#34;&amp;gt;nostr.com&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&#34;&gt;https://nostr.com&amp;#34;&amp;gt;nostr.com&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&lt;/a&gt;;&lt;br/&gt;            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://nostr.org&amp;#34;&amp;gt;nostr.org&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&#34;&gt;https://nostr.org&amp;#34;&amp;gt;nostr.org&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&lt;/a&gt;;&lt;br/&gt;            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://nostr.how/en/what-is-nostr&amp;#34;&amp;gt;nostr.how/en/what-is-nostr&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&#34;&gt;https://nostr.how/en/what-is-nostr&amp;#34;&amp;gt;nostr.how/en/what-is-nostr&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&lt;/a&gt;;&lt;br/&gt;          &amp;lt;/ul&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;footer&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;p&amp;gt;Built with ❤️ for the decentralized future&amp;lt;/p&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;    &amp;lt;/div&amp;gt;&lt;br/&gt;  &amp;lt;/body&amp;gt;&lt;br/&gt;&amp;lt;/html&amp;gt;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspm87lmzjun2hclppdjav36u9lyjv4zl73fkvr0xd6zwsxmac8luqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jefmrns</id>
    
      <title type="html"># Closes an Open or Drafted Patch &amp;gt; `n34 patch close` command ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspm87lmzjun2hclppdjav36u9lyjv4zl73fkvr0xd6zwsxmac8luqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jefmrns" />
    <content type="html">
      # Closes an Open or Drafted Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch close` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Closes an open or drafted patch&lt;br/&gt;&lt;br/&gt;Usage: n34 patch close [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The open/drafted patch id to close it. Must be orignal root patch&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1632` (Close status) for the specified patch. The patch have to&lt;br/&gt;be open or drafted.&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspf7wjwgdj82vflsc3ee85gjr60mzp3092u8l9s3hemsq9spkhc2czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jw8ejnn</id>
    
      <title type="html"># Patch Management In `n34`, patch management is designed to give ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspf7wjwgdj82vflsc3ee85gjr60mzp3092u8l9s3hemsq9spkhc2czyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jw8ejnn" />
    <content type="html">
      # Patch Management&lt;br/&gt;&lt;br/&gt;In `n34`, patch management is designed to give you complete control. You can&lt;br/&gt;manually generate patch files using `git-format-patch` and then broadcast them&lt;br/&gt;to Nostr relays. This ensures that you have full authority over the content&lt;br/&gt;and structure of your patches, allowing for precise customization as per your&lt;br/&gt;requirements.&lt;br/&gt;&lt;br/&gt;Similarly, when fetching patches, `n34` provides them to you without&lt;br/&gt;automatically applying, merging, or checking them. This empowers you to review&lt;br/&gt;the patches at your own pace and decide whether to merge or apply them as&lt;br/&gt;needed. You retain full control over the entire process, ensuring a tailored&lt;br/&gt;approach to patch management.&lt;br/&gt;&lt;br/&gt;## Patch Status Management&lt;br/&gt;&lt;br/&gt;You can assign a status to original patches, but revision patches do not have&lt;br/&gt;a specific status assigned to them. Instead, they inherit the status of the&lt;br/&gt;original patch. However, if the original patch is marked as `Applied/Merged`,&lt;br/&gt;the revision patch must be explicitly tagged to claim the same status. If not&lt;br/&gt;tagged, the revision patch status will be `Closed`.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqspf0erpej5p6xm0q3tacnqv3jfvtrpfv5g9a7ua87n3jpnuxsgrfqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jq34aas</id>
    
      <title type="html"># Issue Management Using `n34`, you can manage Git issues stored ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqspf0erpej5p6xm0q3tacnqv3jfvtrpfv5g9a7ua87n3jpnuxsgrfqzyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jq34aas" />
    <content type="html">
      # Issue Management&lt;br/&gt;&lt;br/&gt;Using `n34`, you can manage Git issues stored in Nostr relays, adhering to&lt;br/&gt;the [NIP-34] standard. In Nostr, events are immutable, meaning their IDs are&lt;br/&gt;derived from the SHA-256 hash of their timestamp, content, author, and tags.&lt;br/&gt;As a result, issues cannot be edited directly. However, with `n34`, you can&lt;br/&gt;create new issues, view existing ones, or update their status—such as closing,&lt;br/&gt;resolving, or reopening them.&lt;br/&gt;&lt;br/&gt;[NIP-34] introduces support for drafting issues, though this feature is not&lt;br/&gt;currently implemented in `n34` due to the lack of a clear use case for drafting&lt;br/&gt;issues. The inclusion of this functionality may stem from its shared use in both&lt;br/&gt;issues and patches, suggesting it was primarily designed for patch management.&lt;br/&gt;&lt;br/&gt;[NIP-34]: &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;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsp9raedtthxexa2zrhcdn343xn08c7shehlcttyuled2tvnhcvgfszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j66vwrj</id>
    
      <title type="html"># Reopen a Closed Issue &amp;gt; `n34 issue reopen` command ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsp9raedtthxexa2zrhcdn343xn08c7shehlcttyuled2tvnhcvgfszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j66vwrj" />
    <content type="html">
      # Reopen a Closed Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue reopen` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Reopens a closed issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue reopen [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The ID of the closed issue to reopen&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1630` (Open status) for the specified issue. The issue have to&lt;br/&gt;be closed.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsqa43hp70qy5vvuwtu4w9vyx3kxj6zm0smnztazp4zs4t0n7zaqdczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j7cw0al</id>
    
      <title type="html"># Create an Issue &amp;gt; `n34 issue new` Command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsqa43hp70qy5vvuwtu4w9vyx3kxj6zm0smnztazp4zs4t0n7zaqdczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j7cw0al" />
    <content type="html">
      # Create an Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue new` Command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Create a new repository issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue new [OPTIONS] &amp;lt;--content &amp;lt;CONTENT&amp;gt;|--editor&amp;gt;&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;  -c, --content &amp;lt;CONTENT&amp;gt;          Markdown content for the issue. Cannot be used together with the `--editor` flag&lt;br/&gt;  -e, --editor                     Opens the user&amp;#39;s default editor to write issue content. The first line will be used as the issue subject&lt;br/&gt;      --subject &amp;lt;SUBJECT&amp;gt;          The issue subject. Cannot be used together with the `--editor` flag&lt;br/&gt;  -l, --label &amp;lt;LABEL&amp;gt;              Labels for the issue. Can be specified as arguments (-l bug) or hashtags in content (#bug)&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Use the `n34 issue new` command to create a new issue in a repository. This&lt;br/&gt;command supports the [NIP-21] (`nostr:` URI scheme) and hashtags within the&lt;br/&gt;issue content. When you mention public keys in the content, they will be&lt;br/&gt;included in the event tags. Additionally, using hashtags like `#bug` in the&lt;br/&gt;issue body will automatically apply them as labels.&lt;br/&gt;&lt;br/&gt;You must choose between the `--content` and `--editor` options. With&lt;br/&gt;`--content`, you provide the issue content directly in the command. With&lt;br/&gt;`--editor`, your default `$EDITOR` will open, allowing you to write the issue&lt;br/&gt;content. The first line of the editor&amp;#39;s output will be used as the issue&lt;br/&gt;subject.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:41Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsymjxz0ts2k7ks3c75j3j2ww382fn3sal4me3qvgt06gvdpu2ywfczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j85pmc6</id>
    
      <title type="html"># Secret Key Keyring &amp;gt; `n34 config keyring` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsymjxz0ts2k7ks3c75j3j2ww382fn3sal4me3qvgt06gvdpu2ywfczyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6j85pmc6" />
    <content type="html">
      # Secret Key Keyring&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 config keyring` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Manages the secret key keyring, including enabling, disabling, or resetting it.&lt;br/&gt;&lt;br/&gt;Usage: n34 config keyring &amp;lt;--enable|--disable|--reset&amp;gt;&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --enable   Enables the secret key keyring. You will be prompted for your key one last time to store it.&lt;br/&gt;      --disable  Disables the secret key keyring. This removes the stored key and prevents new ones from being saved.&lt;br/&gt;      --reset    Resets the keyring. This deletes the current key, allowing a new one to be stored on the next use.&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;To avoid entering your private key for every command, you can enable the keyring&lt;br/&gt;to store it securely. First, run `n34 config keyring --enable`. The next time&lt;br/&gt;you run an `n34` command that requires your private key, it will be saved&lt;br/&gt;to your system&amp;#39;s keyring. You will not need to enter it again for subsequent&lt;br/&gt;commands.&lt;br/&gt;&lt;br/&gt;To replace the stored key with a new one, use the `--reset` flag. To stop using&lt;br/&gt;the keyring and remove the stored key, use the `--disable` flag.&lt;br/&gt;&lt;br/&gt;`n34` uses your operating system&amp;#39;s native secret management system. For example,&lt;br/&gt;it uses `keyutils` on Linux and `Keychain` on macOS.&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:21Z</updated>
  </entry>

  <entry>
    <id>https://njump.me/nevent1qqsr6sxlhy9tm6k676sl82wvvs9kkkrmsrw52enk4aw5qrl0nhwc4uszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jug5ut6</id>
    
      <title type="html"># NIP-46 Bunker &amp;gt; `n34 config bunker` command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsr6sxlhy9tm6k676sl82wvvs9kkkrmsrw52enk4aw5qrl0nhwc4uszyp2xhcyfwmc3jdu98up2074n3zznz6ye259qzdm0pp7qwhqtg2z6jug5ut6" />
    <content type="html">
      # NIP-46 Bunker&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 config bunker` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Sets a URL of NIP-46 bunker server used for signing events&lt;br/&gt;&lt;br/&gt;Usage: n34 config bunker [BUNKER_URL]&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [BUNKER_URL]  Nostr Connect URL for the bunker. Omit this to remove the current bunker URL&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command configures `n34` to use a remote signer ([NIP-46]), known as a&lt;br/&gt;bunker, for all cryptographic operations.&lt;br/&gt;&lt;br/&gt;When `n34` communicates with the bunker, it uses a persistent, locally-generated&lt;br/&gt;keypair. You should add this keypair&amp;#39;s public key to your bunker&amp;#39;s list of&lt;br/&gt;authorized applications. This allows `n34` to operate securely without needing&lt;br/&gt;direct access to your main private key.&lt;br/&gt;&lt;br/&gt;Once configured, actions such as fetching your public key or signing events are&lt;br/&gt;delegated to the bunker. To remove the bunker configuration, run the command&lt;br/&gt;again without providing a URL.&lt;br/&gt;&lt;br/&gt;[NIP-46]: &lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/46.md&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/46.md&lt;/a&gt;&lt;br/&gt;
    </content>
    <updated>2026-10-09T01:53:18Z</updated>
  </entry>

</feed>