diff --git a/.github/workflows/gnostr-bot.yml b/.github/workflows/gnostr-bot.yml index 3052bcb3d4..2857c05596 100644 --- a/.github/workflows/gnostr-bot.yml +++ b/.github/workflows/gnostr-bot.yml @@ -6,9 +6,6 @@ on: - cron: '*/30 * * * *' # run 30th minute push: branches: - - '*' - - '*/*' - - '**' - 'ma**' workflow_dispatch: diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml index 2fb9b32dfd..5438be7fbd 100644 --- a/.github/workflows/matrix.yml +++ b/.github/workflows/matrix.yml @@ -30,7 +30,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - rustup: [1.88, nightly] + rustup: [1.88, stable, nightly] runs-on: ${{ matrix.os }} steps: - name: echo test @@ -86,8 +86,6 @@ jobs: - run: cargo search gnostr --limit 100 - run: cargo install cargo-binstall@1.9.0 || true #if: matrix.os != 'windows-latest' - - run: cargo-binstall --no-confirm mempool_space - - run: cargo-binstall --no-confirm gnostr-xq - run: cargo-binstall --no-confirm gnostr - run: brew tap gnostr-org/homebrew-gnostr-org || true if: matrix.os != 'windows-latest' @@ -95,30 +93,30 @@ jobs: if: matrix.os != 'windows-latest' - run: brew tap randymcmillan/homebrew-randymcmillan || true if: matrix.os != 'windows-latest' - - name: Save rustup - id: cache-rustup-save - uses: actions/cache/save@v3 - if: ${{ !env.ACT }} - with: - path: | - ~/.rustup - key: ${{ steps.cache-rustup-restore.outputs.cache-primary-key }} - - name: Save cargo - id: cache-cargo-save - uses: actions/cache/save@v3 - if: ${{ !env.ACT }} - with: - path: | - ~/.cargo - key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }} - - name: Save target - id: cache-target-save - uses: actions/cache/save@v3 - if: ${{ !env.ACT }} - with: - path: | - target - key: ${{ steps.cache-target-restore.outputs.cache-primary-key }} + #- name: Save rustup + # id: cache-rustup-save + # uses: actions/cache/save@v3 + # if: ${{ !env.ACT }} + # with: + # path: | + # ~/.rustup + # key: ${{ steps.cache-rustup-restore.outputs.cache-primary-key }} + #- name: Save cargo + # id: cache-cargo-save + # uses: actions/cache/save@v3 + # if: ${{ !env.ACT }} + # with: + # path: | + # ~/.cargo + # key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }} + #- name: Save target + # id: cache-target-save + # uses: actions/cache/save@v3 + # if: ${{ !env.ACT }} + # with: + # path: | + # target + # key: ${{ steps.cache-target-restore.outputs.cache-primary-key }} test: needs: setup env: @@ -184,46 +182,37 @@ jobs: fetch-tags: 'false' - run: rustup default ${{ matrix.rustup }} - - run: gnostr-fetch-by-id - - run: gnostr -V - - run: gnostr -h - - run: gnostr ngit -h - - run: gnostr ngit --help - - run: gnostr ngit help - - run: gnostr ngit fetch -h - - run: gnostr ngit fetch --help - - run: gnostr ngit fetch - run: | - cargo t -p gnostr || true - if: matrix.rustup == '1.88' + cargo t -p gnostr --bins --lib -- --no-capture + if: matrix.rustup == '1.88' && matrix.os != 'windows-latest' - run: V=1 sudo make docs || true if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true && matrix.os == 'ubuntu-matrix' - - name: Save rustup - id: cache-rustup-save - uses: actions/cache/save@v3 - if: ${{ !env.ACT }} - with: - path: | - ~/.rustup - key: ${{ steps.cache-rustup-restore.outputs.cache-primary-key }} - - name: Save cargo - id: cache-cargo-save - uses: actions/cache/save@v3 - if: ${{ !env.ACT }} - with: - path: | - ~/.cargo - key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }} - - name: Save target - id: cache-target-save - uses: actions/cache/save@v3 - if: ${{ !env.ACT }} - with: - path: | - target - key: ${{ steps.cache-target-restore.outputs.cache-primary-key }} + #- name: Save rustup + # id: cache-rustup-save + # uses: actions/cache/save@v3 + # if: ${{ !env.ACT }} + # with: + # path: | + # ~/.rustup + # key: ${{ steps.cache-rustup-restore.outputs.cache-primary-key }} + #- name: Save cargo + # id: cache-cargo-save + # uses: actions/cache/save@v3 + # if: ${{ !env.ACT }} + # with: + # path: | + # ~/.cargo + # key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }} + #- name: Save target + # id: cache-target-save + # uses: actions/cache/save@v3 + # if: ${{ !env.ACT }} + # with: + # path: | + # target + # key: ${{ steps.cache-target-restore.outputs.cache-primary-key }} install: needs: test env: @@ -291,10 +280,10 @@ jobs: - run: rustup default ${{ matrix.rustup }} - run: | cargo install --path . --force - if: matrix.os == 'windows-latest' + if: matrix.os == 'windows-latest' && matrix.rustup == 'nightly' - run: | make cargo-install || sudo make cargo-install - if: matrix.os != 'windows-latest' + if: matrix.os != 'windows-latest' && matrix.rustup == 'nightly' - run: V=1 sudo make docs || true if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true && matrix.os == 'ubuntu-matrix' diff --git a/.gitignore b/.gitignore index 8f2703c32d..3fd71d52ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +gnostr-gnit-key +gnostr-gnit-key.pub .gnostr executable test @@ -19,8 +21,6 @@ act/ aes.o base64.o bash_profile.log -bin/ -bins/ bits/ cat/ cli/ @@ -41,7 +41,6 @@ gnostr-get-relays gnostr-get-relays-test gnostr-git gnostr-gnode -gnostr-legit gnostr-pi gnostr-req gnostr-set-relays @@ -57,7 +56,6 @@ hyper-nostr/ hyper-sdk/ jj/ jq/ -legit/ lfs/ libnostr.a libsecp256k1.a diff --git a/Cargo.lock b/Cargo.lock index 57f2b4bd4d..1060bc57de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,12 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "argparse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" + [[package]] name = "arrayref" version = "0.3.9" @@ -1259,50 +1265,6 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" -[[package]] -name = "camino" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo-depgraph" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62895292d88c935f74dc3220a90cc07b7ddc05e233e2236db914369e61b65b7" -dependencies = [ - "anyhow", - "cargo_metadata", - "clap 4.5.37", - "petgraph", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" -dependencies = [ - "camino", - "cargo-platform", - "semver 1.0.26", - "serde", - "serde_json", - "thiserror 1.0.69", -] - [[package]] name = "cassowary" version = "0.3.0" @@ -3005,6 +2967,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "generic-array" version = "0.12.4" @@ -3251,9 +3219,10 @@ dependencies = [ [[package]] name = "gnostr" -version = "0.0.108" +version = "0.0.112" dependencies = [ "anyhow", + "argparse", "ascii", "async-std", "async-trait", @@ -3266,7 +3235,6 @@ dependencies = [ "bugreport", "bwrap", "bytesize", - "cargo-depgraph", "chacha20poly1305", "chrono", "clap 4.5.37", @@ -3298,6 +3266,7 @@ dependencies = [ "gnostr-bins", "gnostr-cat", "gnostr-crawler", + "gnostr-query", "gnostr-types 0.7.6", "gnostr-xq", "gnostr_qr", @@ -3327,6 +3296,7 @@ dependencies = [ "num-bigint", "num_cpus", "once_cell", + "pad", "parking_lot_core 0.9.9", "passwords", "pretty_assertions", @@ -3340,6 +3310,7 @@ dependencies = [ "rpassword", "russh", "russh-keys", + "rust-crypto", "scopeguard", "scopetime", "scrypt", @@ -3571,6 +3542,20 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "gnostr-query" +version = "0.0.9" +dependencies = [ + "clap 4.5.37", + "futures 0.3.31", + "log 0.4.27", + "serde", + "serde_json", + "tokio 1.44.2", + "tokio-tungstenite 0.13.0", + "url 2.5.4", +] + [[package]] name = "gnostr-types" version = "0.0.71" @@ -4594,6 +4579,15 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "input_buffer" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" +dependencies = [ + "bytes 1.10.1", +] + [[package]] name = "instability" version = "0.3.7" @@ -6927,6 +6921,15 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "pad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width 0.1.14", +] + [[package]] name = "parking" version = "2.2.1" @@ -7734,6 +7737,29 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.9", +] + [[package]] name = "rand" version = "0.6.5" @@ -8581,6 +8607,19 @@ dependencies = [ "yasna", ] +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +dependencies = [ + "gcc", + "libc", + "rand 0.3.23", + "rustc-serialize", + "time 0.1.45", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -8593,6 +8632,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc-serialize" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" + [[package]] name = "rustc_version" version = "0.2.3" @@ -9044,9 +9089,6 @@ name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" -dependencies = [ - "serde", -] [[package]] name = "semver-parser" @@ -9199,6 +9241,19 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.1", +] + [[package]] name = "sha1" version = "0.10.6" @@ -10603,6 +10658,21 @@ dependencies = [ "tokio-io", ] +[[package]] +name = "tokio-tungstenite" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" +dependencies = [ + "futures-util", + "log 0.4.27", + "native-tls", + "pin-project", + "tokio 1.44.2", + "tokio-native-tls", + "tungstenite 0.12.0", +] + [[package]] name = "tokio-tungstenite" version = "0.18.0" @@ -11181,6 +11251,26 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "tungstenite" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes 1.10.1", + "http 0.2.12", + "httparse", + "input_buffer", + "log 0.4.27", + "native-tls", + "rand 0.8.5", + "sha-1 0.9.8", + "url 2.5.4", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.18.0" @@ -11895,7 +11985,7 @@ dependencies = [ "futures 0.1.31", "native-tls", "rand 0.6.5", - "sha-1", + "sha-1 0.8.2", "tokio-codec", "tokio-io", "tokio-tcp", diff --git a/Cargo.toml b/Cargo.toml index 2da56d504d..94feb5220a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gnostr" -version = "0.0.108" +version = "0.0.112" edition = "2021" description = "gnostr:a git+nostr workflow utility" authors = [ @@ -53,25 +53,18 @@ regex-onig = ["syntect/regex-onig", "two-face/syntect-onig"] timing = ["scopetime/enabled"] trace-libgit = ["gnostr-asyncgit/trace-libgit"] vendor-openssl = ["gnostr-asyncgit/vendor-openssl"] -##serde_json = "1.0" -##serde = { version = "1.0.152", features = ["derive"] } -#tokio = { version = "1", features = ["full"] } -##futures = "0.3.25" [dependencies] -#notify-debouncer-mini = "0.2.0" - anyhow = "1.0.75" +argparse = "0.2.2" ascii = "1.1.0" async-std = "1.12" async-trait = "0.1.73" ##nostr -#async-trait = "0.1.73" auth-git2 = "0.5.5" backtrace = "0.3" base64 = "0.21" -#log = "0.4" bincode = "1.3.1" bitcoin_hashes = "0.14.0" bitflags = "2.6" @@ -86,29 +79,19 @@ colored = "2.0.0" colorful = "0.2" comrak = "0.18.0" console = "0.15.7" -##clap = { version = "4.5", features = ["env", "cargo"] } crossbeam-channel = "0.5" -#clap = { version = "4.3.19", features = ["derive"] } -#console = "0.15.7" crossterm = { version = "0.28.0", features = ["event-stream", "serde"] } -#clap = { version = "4.5.6", features = ["derive"] } csv = "1.3.0" -#clap = { version = "4.2", features = ["derive"] } -#serde = { version = "1.0", features = ["derive"] } ctrlc = { version = "3.2", features = ["termination"] } dialoguer = "0.10.4" directories = "5.0.1" -#crossterm = { version = "0.28", features = ["serde"] } dirs = "5.0.1" easy-cast = "0.5" -#dialoguer = "0.10.4" -#directories = "5.0.1" env_logger = "0.11" filetreelist = { path = "./filetreelist", version = "0.5" } flume = "0.10" futures = "0.3.28" -#futures = "0.3.28" futures-core = { version = "0.3", optional = false, default-features = false } futures-timer = "3.0" futures-util = "0.3" @@ -121,11 +104,9 @@ git2 = "^0.18" ## asyncgit ^0.19 #anyhow = "1.0" gnostr-asyncgit = { path = "./asyncgit", version = "0.0.4", default-features = false } gnostr-crawler = { version = "0.0.8", path = "crawler" } +gnostr-query = { version = "0.0.9", path = "query" } gnostr-types = { version = "0.7.6", path = "types" } -#gnostr-types = "0.7.6" gnostr_qr = { version = "0.0.7", path = "qr" } -#gpui = "0.1.0" -#gpui-component = "0.1.0" hex = "0.4" hostname = "0.3.1" @@ -134,8 +115,6 @@ indexmap = "2" indicatif = "0.17.7" itertools = "0.13" k256 = { version = "0.13", features = [ "schnorr", "ecdh" ] } -##gnostr-tui provides ngit -##gnostr-tui = { version = "0.0.62", path = "crates/tui" } #indicatif = "0.17.7" keyring = "2.0.5" lazy_static = "1.4" @@ -149,24 +128,29 @@ libp2p = { version = "0.54.1", features = [ log = "0.4" -nostr_0_34_1 = { package = "nostr", version = "0.34.1" } -nostr-database_0_34_0 = { package = "nostr-database", version ="0.34.0" } -nostr-sdk_0_34_0 = { package = "nostr-sdk", version = "0.34.0" } +nostr-database_0_34_0 = { package = "nostr-database", version = "0.34.0" } + nostr-sdk_0_19_1 = { package = "nostr-sdk", version = "0.19.1" } nostr-sdk_0_32_0 = { package = "nostr-sdk", version = "0.32.0" } +nostr-sdk_0_34_0 = { package = "nostr-sdk", version = "0.34.0" } + nostr-sdk_0_37_0 = { package = "nostr-sdk", version = "0.37.0" } + nostr-signer_0_34_0 = { package = "nostr-signer", version = "0.34.0" } + nostr-sqlite_0_34_0 = { package = "nostr-sqlite", version = "0.34.0" } -#log = "0.4" -##cli nostr_0_32_0 = { package = "nostr", version = "0.32.0" } + +nostr_0_34_1 = { package = "nostr", version = "0.34.1" } + notify = "6.1" notify-debouncer-mini = "0.4" num-bigint = "0.4.6" num_cpus = "1.16.0" once_cell = "1.21.3" +pad = "0.1.6" #once_cell = "1" # pin until upgrading this does not introduce a duplicate dependency parking_lot_core = "=0.9.9" @@ -183,6 +167,7 @@ ron = "0.8" rpassword = "7.2" russh = { version = "0.37.1", features = ["openssl"] } russh-keys = "0.37.1" +rust-crypto = "0.2.36" scopeguard = "1.2" scopetime = { path = "./scopetime", version = "0.1" } scrypt = "0.11.0" @@ -190,16 +175,11 @@ secp256k1 = { version = "0.31.0", features = ["hashes"] } serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.117" serde_yaml = "0.9.27" -#serde = { version = "1.0.181", features = ["derive"] } -#serde_json = "1.0.105" -#serde_yaml = "0.9.27" serial_test = "2.0.0" sha2 = "0.10.8" sha256 = "1.5.0" -#serde = "1.0" shellexpand = "3.1" shellwords = "1.1.0" -#anyhow = "1.0" simple_logger = "4.1" simplelog = { version = "0.12", default-features = false } struct-patch = "0.4" @@ -215,12 +195,11 @@ tempfile = "3.5.0" tera = "1.18.1" textwrap = "0.16.0" thiserror = "1" -time = "0.1.39" +time = "0.1.42" tokio = { version = "1.33.0", features = ["full"] } tokio-tungstenite = { version = "0.21", features = [ "connect", "handshake", "rustls-tls-webpki-roots" ] } toml = "0.7.3" -##tracing = { version = "0.1.37", features = ["std", "attributes"] } tracing = { version = "0.1.38", default-features = false } tracing-core = "0.1.33" @@ -228,7 +207,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } trust-dns-resolver = "0.23" # Use the latest version tui-input = "0.10.1" tui-textarea = "0.6" -##tokio-tungstenite = "0.20" tungstenite = { version = "0.21", features = [ "rustls-tls-webpki-roots" ] } two-face = { version = "0.4.0", default-features = false } unicode-segmentation = "1.12" @@ -247,15 +225,12 @@ sd-notify = "0.4.1" chrono = { version = "0.4", default-features = false, features = ["clock"] } [dev-dependencies] -cargo-depgraph = "1.6.0" -##nostr env_logger = "0.11" gnostr-bins = "0.0.71" gnostr-cat = "0.0.40" gnostr-xq = "0.0.3" mockall = "0.11.4" pretty_assertions = "1.4" -##examples/sniper reqwest = { version = "0.11.14", features = ["blocking", "json"] } rexpect = { git = "https://github.com/rust-cli/rexpect.git", rev = "9eb61dd" } serial_test = "2.0.0" @@ -271,7 +246,6 @@ test_utils = { path = "test_utils" } ### ### tokio = { version = "1", features = ["full"] } # Required for async operations -##trust-dns-resolver = "0.23" # Use the latest version tungstenite = { version = "0.21.0", features = ["native-tls"] } url = "2.2.2" diff --git a/app/Cargo.toml b/app/Cargo.toml index 11e44f7767..e6087e9e8d 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -32,7 +32,7 @@ actix-rt = "2.10.0" anyhow = "1.0.86" clap = { version = "4.5.16", features = ["derive"] } clio = { version = "0.3.5", features = ["clap-parse"] } -gnostr = { version = "0.0.106", path = ".." } +gnostr = { version = "*", path = ".." } gpui = { git = "https://github.com/zed-industries/zed" } indicatif = "0.17.8" nostr-db = { version = "0.4.5", path = "./db", features = ["search"] } diff --git a/asyncgit/src/gitui/git/mod.rs b/asyncgit/src/gitui/git/mod.rs index c9076fa44e..4e6f8a787f 100644 --- a/asyncgit/src/gitui/git/mod.rs +++ b/asyncgit/src/gitui/git/mod.rs @@ -235,7 +235,7 @@ pub(crate) fn get_head_name(repo: &git2::Repository) -> Res { .map_err(Error::BranchNameUtf8) } -pub(crate) fn get_current_branch(repo: &git2::Repository) -> Res { +pub(crate) fn get_current_branch(repo: &git2::Repository) -> Res> { let head = repo.head().map_err(Error::GetHead)?; if head.is_branch() { Ok(Branch::wrap(head)) diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index b1e9d393b7..f49f1003ab 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -47,7 +47,6 @@ mod diff; mod error; mod fetch_job; mod filter_commits; -//mod gitui; mod progress; mod pull; mod push; diff --git a/crawler/src/processor.rs b/crawler/src/processor.rs index b6018b3cd1..80eb0194c8 100644 --- a/crawler/src/processor.rs +++ b/crawler/src/processor.rs @@ -5,19 +5,24 @@ use crate::stats::Stats; use nostr_sdk::prelude::{Event, Kind, Tag, Timestamp}; use std::sync::LazyLock; +pub const BOOTSTRAP_RELAY0: &str = "wss://nos.lol"; pub const BOOTSTRAP_RELAY1: &str = "wss://relay.nostr.band"; -pub const BOOTSTRAP_RELAY2: &str = "wss://nos.lol"; +pub const BOOTSTRAP_RELAY2: &str = "wss://bitcoiner.social"; pub const BOOTSTRAP_RELAY3: &str = "wss://relay.damus.io"; -pub const BOOTSTRAP_RELAY4: &str = "wss://sendit.nosflare.com"; +pub const BOOTSTRAP_RELAY4: &str = "wss://purplerelay.com"; +pub const BOOTSTRAP_RELAY5: &str = "wss://nos.lol"; + pub static BOOTSTRAP_RELAYS: LazyLock> = LazyLock::new(|| { // The vec! macro and String::from calls are now inside a closure, // which is executed at runtime when the static variable is first needed. vec![ + String::from(BOOTSTRAP_RELAY0), String::from(BOOTSTRAP_RELAY1), String::from(BOOTSTRAP_RELAY2), String::from(BOOTSTRAP_RELAY3), String::from(BOOTSTRAP_RELAY4), + String::from(BOOTSTRAP_RELAY5), ] }); diff --git a/src/bin/create_giftwrap.rs b/examples/create_giftwrap.rs similarity index 100% rename from src/bin/create_giftwrap.rs rename to examples/create_giftwrap.rs diff --git a/src/bin/create_nevent.rs b/examples/create_nevent.rs similarity index 100% rename from src/bin/create_nevent.rs rename to examples/create_nevent.rs diff --git a/src/bin/decrypt_private_key.rs b/examples/decrypt_private_key.rs similarity index 100% rename from src/bin/decrypt_private_key.rs rename to examples/decrypt_private_key.rs diff --git a/src/bin/dump_relay.rs b/examples/dump_relay.rs similarity index 100% rename from src/bin/dump_relay.rs rename to examples/dump_relay.rs diff --git a/src/bin/dump_relay_with_login.rs b/examples/dump_relay_with_login.rs similarity index 100% rename from src/bin/dump_relay_with_login.rs rename to examples/dump_relay_with_login.rs diff --git a/src/bin/encrypt_private_key.rs b/examples/encrypt_private_key.rs similarity index 100% rename from src/bin/encrypt_private_key.rs rename to examples/encrypt_private_key.rs diff --git a/src/bin/fetch_by_filter.rs b/examples/fetch_by_filter.rs similarity index 100% rename from src/bin/fetch_by_filter.rs rename to examples/fetch_by_filter.rs diff --git a/src/bin/fetch_by_id_with_login.rs b/examples/fetch_by_id_with_login.rs similarity index 100% rename from src/bin/fetch_by_id_with_login.rs rename to examples/fetch_by_id_with_login.rs diff --git a/src/bin/fetch_by_kind_and_author.rs b/examples/fetch_by_kind_and_author.rs similarity index 100% rename from src/bin/fetch_by_kind_and_author.rs rename to examples/fetch_by_kind_and_author.rs diff --git a/src/bin/fetch_by_kind_and_author_limit.rs b/examples/fetch_by_kind_and_author_limit.rs similarity index 100% rename from src/bin/fetch_by_kind_and_author_limit.rs rename to examples/fetch_by_kind_and_author_limit.rs diff --git a/src/bin/fetch_by_kind_and_author_with_login.rs b/examples/fetch_by_kind_and_author_with_login.rs similarity index 100% rename from src/bin/fetch_by_kind_and_author_with_login.rs rename to examples/fetch_by_kind_and_author_with_login.rs diff --git a/src/bin/fetch_giftwraps.rs b/examples/fetch_giftwraps.rs similarity index 100% rename from src/bin/fetch_giftwraps.rs rename to examples/fetch_giftwraps.rs diff --git a/src/bin/fetch_metadata.rs b/examples/fetch_metadata.rs similarity index 100% rename from src/bin/fetch_metadata.rs rename to examples/fetch_metadata.rs diff --git a/src/bin/fetch_nip11.rs b/examples/fetch_nip11.rs similarity index 100% rename from src/bin/fetch_nip11.rs rename to examples/fetch_nip11.rs diff --git a/src/bin/fetch_relay_list.rs b/examples/fetch_relay_list.rs similarity index 100% rename from src/bin/fetch_relay_list.rs rename to examples/fetch_relay_list.rs diff --git a/src/bin/form_naddr.rs b/examples/form_naddr.rs similarity index 100% rename from src/bin/form_naddr.rs rename to examples/form_naddr.rs diff --git a/examples/git-clone.rs b/examples/git-clone.rs deleted file mode 100644 index 76f25fc3ac..0000000000 --- a/examples/git-clone.rs +++ /dev/null @@ -1,17 +0,0 @@ -use rexpect::error::Error; -use rexpect::spawn_bash; - -fn main() -> Result<(), Error> { - let mut p = spawn_bash(Some(1000))?; - p.execute("ping gitworkshop.dev", "bytes")?; - p.send_control('z')?; - p.wait_for_prompt()?; - p.execute("bg", "ping gitworkshop.dev")?; - p.wait_for_prompt()?; - p.send_line("sleep 0.5")?; - p.wait_for_prompt()?; - p.execute("fg", "ping gitworkshop.dev")?; - p.send_control('c')?; - p.exp_string("packet loss")?; - Ok(()) -} diff --git a/examples/git-log.rs b/examples/git-log.rs deleted file mode 100644 index 4cfddd6ee5..0000000000 --- a/examples/git-log.rs +++ /dev/null @@ -1,34 +0,0 @@ -use gnostr_crawler::processor::Processor; -use gnostr_crawler::processor::APP_SECRET_KEY; -use gnostr_crawler::processor::BOOTSTRAP_RELAY1; -use gnostr_crawler::processor::BOOTSTRAP_RELAY2; -use gnostr_crawler::processor::BOOTSTRAP_RELAY3; -use gnostr_crawler::CliArgs; - -use nostr_sdk_0_19_1::prelude::{FromBech32, Keys, SecretKey}; - -use clap::Parser; -//use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time}; -//use git2::{DiffFormat, Error, Pathspec}; -//use std::str; - -#[tokio::main] -async fn main() { - env_logger::init(); - let args = CliArgs::parse(); - - match gnostr_crawler::run(&args) { - Ok(()) => { - let app_secret_key = SecretKey::from_bech32(APP_SECRET_KEY); - let app_keys = Keys::new(app_secret_key.expect("REASON")); - let processor = Processor::new(); - let mut relay_manager = - gnostr_crawler::relay_manager::RelayManager::new(app_keys, processor); - let _ = relay_manager - .run(vec![BOOTSTRAP_RELAY1, BOOTSTRAP_RELAY2, BOOTSTRAP_RELAY3]) - .await; - relay_manager.processor.dump(); - } - Err(e) => println!("error: {}", e), - } -} diff --git a/examples/git2_status.rs b/examples/git2_status.rs new file mode 100644 index 0000000000..cd1ffdef24 --- /dev/null +++ b/examples/git2_status.rs @@ -0,0 +1,93 @@ +use git2::{Repository, Status, StatusOptions}; +use std::path::Path; + +fn main() -> Result<(), git2::Error> { + // Open an existing repository (replace "." with your repo path if needed) + let repo = Repository::open(".")?; + + let mut opts = StatusOptions::new(); + // You can configure what types of statuses you want to include + opts.include_untracked(true) + .recurse_untracked_dirs(true) + .exclude_submodules(true); // Or false, depending on your needs + + let statuses = repo.statuses(Some(&mut opts))?; + + for entry in statuses.iter() { + let status = entry.status(); + + // Get the path of the file + let path = if let Some(path_str) = entry.path() { + Path::new(path_str) + } else { + // This might happen for certain status types or unusual cases + continue; + }; + + // Determine the status characters (like 'M', 'A', 'D', '??') + let (index_status, workdir_status) = { + let mut i = ' '; + let mut w = ' '; + + if status.contains(Status::INDEX_NEW) { + i = 'A'; + } else if status.contains(Status::INDEX_MODIFIED) { + i = 'M'; + } else if status.contains(Status::INDEX_DELETED) { + i = 'D'; + } else if status.contains(Status::INDEX_RENAMED) { + i = 'R'; + } else if status.contains(Status::INDEX_TYPECHANGE) { + i = 'T'; + } + + if status.contains(Status::WT_NEW) { + w = '?'; + } + // Untracked + else if status.contains(Status::WT_MODIFIED) { + w = 'M'; + } else if status.contains(Status::WT_DELETED) { + w = 'D'; + } else if status.contains(Status::WT_RENAMED) { + w = 'R'; + } else if status.contains(Status::WT_TYPECHANGE) { + w = 'T'; + } + + (i, w) + }; + + // Print the status and path + println!("{}{} {}", index_status, workdir_status, path.display()); + + // You can also get more detailed information + if let Some(h2i) = entry.head_to_index() { + // Changes between HEAD and index + let old_file = h2i.old_file().path().unwrap_or(Path::new("")); + let new_file = h2i.new_file().path().unwrap_or(Path::new("")); + if old_file != new_file { + println!( + " (Indexed path changed from {} to {})", + old_file.display(), + new_file.display() + ); + } + } + + if let Some(i2w) = entry.index_to_workdir() { + // Changes between index and working directory + let old_file = i2w.old_file().path().unwrap_or(Path::new("")); + let new_file = i2w.new_file().path().unwrap_or(Path::new("")); + if old_file != new_file { + println!( + " (Workdir path changed from {} to {})", + old_file.display(), + new_file.display() + ); + } + } + } + + Ok(()) +} diff --git a/examples/git_remote_nostr/fetch.rs b/examples/git_remote_nostr/fetch.rs new file mode 100644 index 0000000000..1ccef6fdd0 --- /dev/null +++ b/examples/git_remote_nostr/fetch.rs @@ -0,0 +1,602 @@ +use core::str; +use std::{ + io::Stdin, + sync::{Arc, Mutex}, + time::Instant, +}; + +use anyhow::{anyhow, bail, Result}; +use auth_git2::GitAuthenticator; +use git2::{Progress, Repository}; +use gnostr::{ + git::{ + nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, + utils::check_ssh_keys, + Repo, RepoActions, + }, + git_events::tag_value, + login::get_curent_user, + repo_ref::RepoRef, +}; +use nostr_0_34_1::nips::nip19; +use nostr_sdk_0_34_0::ToBech32; + +use crate::utils::{ + count_lines_per_msg_vec, fetch_or_list_error_is_not_authentication_failure, + find_proposal_and_patches_by_branch_name, get_oids_from_fetch_batch, get_open_proposals, + get_read_protocols_to_try, join_with_and, set_protocol_preference, Direction, +}; + +pub async fn run_fetch( + git_repo: &Repo, + repo_ref: &RepoRef, + decoded_nostr_url: &NostrUrlDecoded, + stdin: &Stdin, + oid: &str, + refstr: &str, +) -> Result<()> { + let mut fetch_batch = get_oids_from_fetch_batch(stdin, oid, refstr)?; + + let oids_from_git_servers = fetch_batch + .iter() + .filter(|(refstr, _)| !refstr.contains("refs/heads/pr/")) + .map(|(_, oid)| oid.clone()) + .collect::>(); + + let mut errors = vec![]; + let term = console::Term::stderr(); + + for git_server_url in &repo_ref.git_server { + let term = console::Term::stderr(); + if let Err(error) = fetch_from_git_server( + git_repo, + &oids_from_git_servers, + git_server_url, + decoded_nostr_url, + &term, + ) { + errors.push(error); + } else { + break; + } + } + + if oids_from_git_servers + .iter() + .any(|oid| !git_repo.does_commit_exist(oid).unwrap()) + && !errors.is_empty() + { + bail!( + "fetch: failed to fetch objects in nostr state event from:\r\n{}", + errors + .iter() + .map(|e| format!(" - {e}")) + .collect::>() + .join("\r\n") + ); + } + + fetch_batch.retain(|refstr, _| refstr.contains("refs/heads/pr/")); + + if !fetch_batch.is_empty() { + let open_proposals = get_open_proposals(git_repo, repo_ref).await?; + + let current_user = get_curent_user(git_repo)?; + + for (refstr, oid) in fetch_batch { + if let Some((_, (_, patches))) = + find_proposal_and_patches_by_branch_name(&refstr, &open_proposals, ¤t_user) + { + if !git_repo.does_commit_exist(&oid)? { + let mut patches_ancestor_first = patches.clone(); + patches_ancestor_first.reverse(); + if git_repo.does_commit_exist(&tag_value( + patches_ancestor_first.first().unwrap(), + "parent-commit", + )?)? { + for patch in &patches_ancestor_first { + if let Err(error) = git_repo.create_commit_from_patch(patch) { + term.write_line( + format!( + "WARNING: cannot create branch for {refstr}, error: {error} for patch {}", + nip19::Nip19Event { + event_id: patch.id(), + author: Some(patch.author()), + kind: Some(patch.kind()), + relays: if let Some(relay) = repo_ref.relays.first() { + vec![relay.to_string()] + } else { vec![]}, + }.to_bech32().unwrap_or_default() + ) + .as_str(), + )?; + break; + } + } + } else { + term.write_line( + format!("WARNING: cannot find parent commit for {refstr}").as_str(), + )?; + } + } + } else { + term.write_line(format!("WARNING: cannot find proposal for {refstr}").as_str())?; + } + } + } + + term.flush()?; + println!(); + Ok(()) +} + +fn fetch_from_git_server( + git_repo: &Repo, + oids: &[String], + git_server_url: &str, + decoded_nostr_url: &NostrUrlDecoded, + term: &console::Term, +) -> Result<()> { + let server_url = git_server_url.parse::()?; + + let protocols_to_attempt = get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url); + + let mut failed_protocols = vec![]; + let mut success = false; + for protocol in &protocols_to_attempt { + term.write_line( + format!("fetching {} over {protocol}...", server_url.short_name(),).as_str(), + )?; + + let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; + let res = fetch_from_git_server_url( + &git_repo.git_repo, + oids, + &formatted_url, + [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol), + term, + ); + if let Err(error) = res { + term.write_line( + format!("fetch: {formatted_url} failed over {protocol}: {error}").as_str(), + )?; + failed_protocols.push(protocol); + if protocol == &ServerProtocol::Ssh + && fetch_or_list_error_is_not_authentication_failure(&error) + { + // authenticated by failed to complete request + break; + } + } else { + success = true; + if !failed_protocols.is_empty() { + term.write_line(format!("fetch: succeeded over {protocol}").as_str())?; + let _ = set_protocol_preference(git_repo, protocol, &server_url, &Direction::Push); + } + break; + } + } + if success { + Ok(()) + } else { + let error = anyhow!( + "{} failed over {}{}", + server_url.short_name(), + join_with_and(&failed_protocols), + if decoded_nostr_url.protocol.is_some() { + " and nostr url contains protocol override so no other protocols were attempted" + } else { + "" + }, + ); + term.write_line(format!("fetch: {error}").as_str())?; + Err(error) + } +} + +#[allow(clippy::cast_precision_loss)] +#[allow(clippy::float_cmp)] +#[allow(clippy::needless_pass_by_value)] +fn report_on_transfer_progress( + progress_stats: &Progress<'_>, + start_time: &Instant, + end_time: &Option, +) -> Vec { + let mut report = vec![]; + let total = progress_stats.total_objects() as f64; + if total == 0.0 { + return report; + } + let received = progress_stats.received_objects() as f64; + let percentage = ((received / total) * 100.0) + // always round down because 100% complete is misleading when + // its not complete + .floor(); + + let received_bytes = progress_stats.received_bytes() as f64; + + let (size, unit) = if received_bytes >= (1024.0 * 1024.0) { + (received_bytes / (1024.0 * 1024.0), "MiB") + } else { + (received_bytes / 1024.0, "KiB") + }; + + let speed = { + let duration = if let Some(end_time) = end_time { + (*end_time - *start_time).as_millis() as f64 + } else { + start_time.elapsed().as_millis() as f64 + }; + + if duration > 0.0 { + (received_bytes / (1024.0 * 1024.0)) / (duration / 1000.0) // Convert bytes to MiB and milliseconds to seconds + } else { + 0.0 + } + }; + + // Format the output for receiving objects + report.push(format!( + "Receiving objects: {percentage}% ({received}/{total}) {size:.2} {unit} | {speed:.2} MiB/s{}", + if received == total { + ", done." + } else { ""}, + )); + if received == total { + let indexed_deltas = progress_stats.indexed_deltas() as f64; + let total_deltas = progress_stats.total_deltas() as f64; + let percentage = ((indexed_deltas / total_deltas) * 100.0) + // always round down because 100% complete is misleading + // when its not complete + .floor(); + if total_deltas > 0.0 { + report.push(format!( + "Resolving deltas: {percentage}% ({indexed_deltas}/{total_deltas}){}", + if indexed_deltas == total_deltas { + ", done." + } else { + "" + }, + )); + } + } + report +} + +struct FetchReporter<'a> { + remote_msgs: Vec, + transfer_progress_msgs: Vec, + term: &'a console::Term, + start_time: Option, + end_time: Option, +} +impl<'a> FetchReporter<'a> { + fn new(term: &'a console::Term) -> Self { + Self { + remote_msgs: vec![], + transfer_progress_msgs: vec![], + term, + start_time: None, + end_time: None, + } + } + fn write_all(&self, lines_to_clear: usize) { + let _ = self.term.clear_last_lines(lines_to_clear); + for msg in &self.remote_msgs { + let _ = self.term.write_line(format!("remote: {msg}").as_str()); + } + for msg in &self.transfer_progress_msgs { + let _ = self.term.write_line(msg); + } + } + fn count_all_existing_lines(&self) -> usize { + let width = self.term.size().1; + count_lines_per_msg_vec(width, &self.remote_msgs, "remote: ".len()) + + count_lines_per_msg_vec(width, &self.transfer_progress_msgs, 0) + } + fn just_write_transfer_progress(&self, lines_to_clear: usize) { + let _ = self.term.clear_last_lines(lines_to_clear); + for msg in &self.transfer_progress_msgs { + let _ = self.term.write_line(msg); + } + } + fn just_count_transfer_progress(&self) -> usize { + let width = self.term.size().1; + count_lines_per_msg_vec(width, &self.transfer_progress_msgs, 0) + } + fn process_remote_msg(&mut self, data: &[u8]) { + if let Ok(data) = str::from_utf8(data) { + let data = data + .split(['\n', '\r']) + .map(str::trim) + .filter(|line| !line.trim().is_empty()) + .collect::>(); + for data in data { + let existing_lines = self.count_all_existing_lines(); + let msg = data.to_string(); + if let Some(last) = self.remote_msgs.last() { + // if previous line begins with x but doesnt + // finish with y then its part of the + // same msg + if (last.starts_with("Enume") && !last.ends_with(", done.")) + || ((last.starts_with("Compre") || last.starts_with("Count")) + && !last.contains(')')) + { + let last = self.remote_msgs.pop().unwrap(); + self.remote_msgs.push(format!("{last}{msg}")); + // if previous msg contains % and its not 100% + // then it should be overwritten + } else if (last.contains('%') && !last.contains("100%")) + // but also if the next message is identical with "", done." appended + || last == &msg.replace(", done.", "") + { + self.remote_msgs.pop(); + self.remote_msgs.push(msg); + } else { + self.remote_msgs.push(msg); + } + } else { + self.remote_msgs.push(msg); + } + self.write_all(existing_lines); + } + } + } + fn process_transfer_progress_update(&mut self, progress_stats: &git2::Progress<'_>) { + if self.start_time.is_none() { + self.start_time = Some(Instant::now()); + } + let existing_lines = self.just_count_transfer_progress(); + let updated = + report_on_transfer_progress(progress_stats, &self.start_time.unwrap(), &self.end_time); + if self.transfer_progress_msgs.len() <= updated.len() { + if self.end_time.is_none() && updated.first().is_some_and(|f| f.contains("100%")) { + self.end_time = Some(Instant::now()); + } + // once "Resolving Deltas" is complete, deltas get reset + // to 0 and it stops reporting on it so we want to keep + // the old report + self.transfer_progress_msgs = updated; + } + self.just_write_transfer_progress(existing_lines); + } +} + +fn fetch_from_git_server_url( + git_repo: &Repository, + oids: &[String], + git_server_url: &str, + dont_authenticate: bool, + term: &console::Term, +) -> Result<()> { + if git_server_url.parse::()?.protocol() == ServerProtocol::Ssh && !check_ssh_keys() { + bail!("no ssh keys found"); + } + let git_config = git_repo.config()?; + let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; + let auth = GitAuthenticator::default(); + let mut fetch_options = git2::FetchOptions::new(); + let mut remote_callbacks = git2::RemoteCallbacks::new(); + let fetch_reporter = Arc::new(Mutex::new(FetchReporter::new(term))); + remote_callbacks.sideband_progress({ + let fetch_reporter = Arc::clone(&fetch_reporter); + move |data| { + let mut reporter = fetch_reporter.lock().unwrap(); + reporter.process_remote_msg(data); + true + } + }); + remote_callbacks.transfer_progress({ + let fetch_reporter = Arc::clone(&fetch_reporter); + move |stats| { + let mut reporter = fetch_reporter.lock().unwrap(); + reporter.process_transfer_progress_update(&stats); + true + } + }); + + if !dont_authenticate { + remote_callbacks.credentials(auth.credentials(&git_config)); + } + fetch_options.remote_callbacks(remote_callbacks); + git_server_remote.download(oids, Some(&mut fetch_options))?; + + git_server_remote.disconnect()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + + use super::*; + + fn pass_through_fetch_reporter_proces_remote_msg(msgs: Vec<&str>) -> Vec { + let term = console::Term::stdout(); + let mut reporter = FetchReporter::new(&term); + for msg in msgs { + reporter.process_remote_msg(msg.as_bytes()); + } + reporter.remote_msgs + } + + #[test] + fn logs_single_msg() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.", + ]), + vec!["Enumerating objects: 23716, done."] + ); + } + + #[test] + fn logs_multiple_msgs() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + ] + ); + } + + mod ignores { + use super::*; + + #[test] + fn empty_msgs() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.", + "", + "Counting objects: 0% (1/2195)", + "", + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + ] + ); + } + + #[test] + fn whitespace_msgs() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.", + " ", + "Counting objects: 0% (1/2195)", + " \r\n \r", + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + ] + ); + } + } + + mod splits { + use super::*; + + #[test] + fn multiple_lines_in_single_msg() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.\r\nCounting objects: 0% (1/2195)", + "", + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + ] + ); + } + } + + mod joins_lines_sent_over_multiple_msgs { + use super::*; + + #[test] + fn enumerating() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerat", + "ing objec", + "ts: 23716, done.", + "Counting objects: 0% (1/2195)", + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + ] + ); + } + #[test] + fn counting() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.", + "Counting obj", + "ects: 0% (1/2195)", + "Count", + "ing objects: 1% (22/", + "2195)", + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 1% (22/2195)", + ] + ); + } + #[test] + fn compressing() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Compress", + "ing obj", + "ect", + "s: 0% (1/56", + "0)" + ]), + vec!["Compressing objects: 0% (1/560)"] + ); + } + } + + #[test] + fn msgs_with_pc_and_not_100pc_are_replaced() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + "Counting objects: 1% (22/2195)", + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 1% (22/2195)", + ] + ); + } + mod msgs_with_pc_100pc_are_not_replaced { + use super::*; + + #[test] + fn when_next_msg_is_not_identical_but_with_done() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + "Counting objects: 1% (22/2195)", + "Counting objects: 100% (2195/2195)", + "Compressing objects: 0% (1/560)" + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 100% (2195/2195)", + "Compressing objects: 0% (1/560)" + ] + ); + } + + #[test] + fn but_is_when_next_msg_is_identical_but_with_done_appended() { + assert_eq!( + pass_through_fetch_reporter_proces_remote_msg(vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 0% (1/2195)", + "Counting objects: 1% (22/2195)", + "Counting objects: 100% (2195/2195)", + "Counting objects: 100% (2195/2195), done.", + ]), + vec![ + "Enumerating objects: 23716, done.", + "Counting objects: 100% (2195/2195), done.", + ] + ); + } + } +} diff --git a/examples/git_remote_nostr/list.rs b/examples/git_remote_nostr/list.rs new file mode 100644 index 0000000000..f91bfde0ad --- /dev/null +++ b/examples/git_remote_nostr/list.rs @@ -0,0 +1,279 @@ +use core::str; +use std::collections::HashMap; + +use anyhow::{anyhow, Context, Result}; +use auth_git2::GitAuthenticator; +use client::get_state_from_cache; +use git::RepoActions; +use git_events::{event_to_cover_letter, get_commit_id_from_patch}; +use gnostr::{ + client, + git::{ + self, + nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, + }, + git_events, + login::get_curent_user, + repo_ref, +}; +use nostr_sdk_0_34_0::hashes::sha1::Hash as Sha1Hash; +use repo_ref::RepoRef; + +use crate::{ + git::Repo, + utils::{ + fetch_or_list_error_is_not_authentication_failure, get_open_proposals, + get_read_protocols_to_try, get_short_git_server_name, join_with_and, + set_protocol_preference, Direction, + }, +}; + +pub async fn run_list( + git_repo: &Repo, + repo_ref: &RepoRef, + decoded_nostr_url: &NostrUrlDecoded, + for_push: bool, +) -> Result>> { + let nostr_state = + if let Ok(nostr_state) = get_state_from_cache(git_repo.get_path()?, repo_ref).await { + Some(nostr_state) + } else { + None + }; + + let term = console::Term::stderr(); + + let remote_states = list_from_remotes(&term, git_repo, &repo_ref.git_server, decoded_nostr_url); + + let mut state = if let Some(nostr_state) = nostr_state { + for (name, value) in &nostr_state.state { + for (url, remote_state) in &remote_states { + let remote_name = get_short_git_server_name(git_repo, url); + if let Some(remote_value) = remote_state.get(name) { + if value.ne(remote_value) { + term.write_line( + format!( + "WARNING: {remote_name} {name} is {} nostr ", + if let Ok((ahead, behind)) = + get_ahead_behind(git_repo, value, remote_value) + { + format!("{} ahead {} behind", ahead.len(), behind.len()) + } else { + "out of sync with".to_string() + } + ) + .as_str(), + )?; + } + } else { + term.write_line( + format!("WARNING: {remote_name} {name} is missing but tracked on nostr") + .as_str(), + )?; + } + } + } + nostr_state.state + } else { + repo_ref + .git_server + .iter() + .filter_map(|server| remote_states.get(server)) + .cloned() + .collect::>>() + .first() + .context("failed to get refs from git server")? + .clone() + }; + + state.retain(|k, _| !k.starts_with("refs/heads/pr/")); + + let open_proposals = get_open_proposals(git_repo, repo_ref).await?; + let current_user = get_curent_user(git_repo)?; + for (_, (proposal, patches)) in open_proposals { + if let Ok(cl) = event_to_cover_letter(&proposal) { + if let Ok(mut branch_name) = cl.get_branch_name() { + branch_name = if let Some(public_key) = current_user { + if proposal.author().eq(&public_key) { + cl.branch_name.to_string() + } else { + branch_name + } + } else { + branch_name + }; + if let Some(patch) = patches.first() { + // TODO this isn't resilient because the commit id + // stated may not be correct we will need to + // check whether the commit id exists in the repo + // or apply the proposal and each patch to + // check + if let Ok(commit_id) = get_commit_id_from_patch(patch) { + state.insert(format!("refs/heads/{branch_name}"), commit_id); + } + } + } + } + } + + // TODO 'for push' should we check with the git servers to see if + // any of them allow push from the user? + for (name, value) in state { + if value.starts_with("ref: ") { + if !for_push { + println!("{} {name}", value.replace("ref: ", "@")); + } + } else { + println!("{value} {name}"); + } + } + + println!(); + Ok(remote_states) +} + +pub fn list_from_remotes( + term: &console::Term, + git_repo: &Repo, + git_servers: &Vec, + decoded_nostr_url: &NostrUrlDecoded, // Add this parameter +) -> HashMap> { + let mut remote_states = HashMap::new(); + let mut errors = HashMap::new(); + for url in git_servers { + match list_from_remote(term, git_repo, url, decoded_nostr_url) { + Err(error) => { + errors.insert(url, error); + } + Ok(state) => { + remote_states.insert(url.to_string(), state); + } + } + } + remote_states +} + +pub fn list_from_remote( + term: &console::Term, + git_repo: &Repo, + git_server_url: &str, + decoded_nostr_url: &NostrUrlDecoded, // Add this parameter +) -> Result> { + let server_url = git_server_url.parse::()?; + let protocols_to_attempt = get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url); + + let mut failed_protocols = vec![]; + let mut remote_state: Option> = None; + + for protocol in &protocols_to_attempt { + term.write_line( + format!( + "fetching {} ref list over {protocol}...", + server_url.short_name(), + ) + .as_str(), + )?; + + let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; + let res = list_from_remote_url( + git_repo, + &formatted_url, + [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol), + term, + ); + + match res { + Ok(state) => { + remote_state = Some(state); + term.clear_last_lines(1)?; + if !failed_protocols.is_empty() { + term.write_line( + format!( + "list: succeeded over {protocol} from {}", + server_url.short_name(), + ) + .as_str(), + )?; + let _ = + set_protocol_preference(git_repo, protocol, &server_url, &Direction::Fetch); + } + break; + } + Err(error) => { + term.clear_last_lines(1)?; + term.write_line( + format!("list: {formatted_url} failed over {protocol}: {error}").as_str(), + )?; + failed_protocols.push(protocol); + if protocol == &ServerProtocol::Ssh + && fetch_or_list_error_is_not_authentication_failure(&error) + { + // authenticated by failed to complete request + break; + } + } + } + } + if let Some(remote_state) = remote_state { + if failed_protocols.is_empty() { + term.clear_last_lines(1)?; + } + Ok(remote_state) + } else { + let error = anyhow!( + "{} failed over {}{}", + server_url.short_name(), + join_with_and(&failed_protocols), + if decoded_nostr_url.protocol.is_some() { + " and nostr url contains protocol override so no other protocols were attempted" + } else { + "" + }, + ); + term.write_line(format!("list: {error}").as_str())?; + Err(error) + } +} + +fn list_from_remote_url( + git_repo: &Repo, + git_server_remote_url: &str, + dont_authenticate: bool, + term: &console::Term, +) -> Result> { + let git_config = git_repo.git_repo.config()?; + + let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_remote_url)?; + // authentication may be required + let auth = GitAuthenticator::default(); + let mut remote_callbacks = git2::RemoteCallbacks::new(); + if !dont_authenticate { + remote_callbacks.credentials(auth.credentials(&git_config)); + } + term.write_line("list: connecting...")?; + git_server_remote.connect_auth(git2::Direction::Fetch, Some(remote_callbacks), None)?; + term.clear_last_lines(1)?; + let mut state = HashMap::new(); + for head in git_server_remote.list()? { + if let Some(symbolic_reference) = head.symref_target() { + state.insert( + head.name().to_string(), + format!("ref: {symbolic_reference}"), + ); + } else { + state.insert(head.name().to_string(), head.oid().to_string()); + } + } + git_server_remote.disconnect()?; + Ok(state) +} + +fn get_ahead_behind( + git_repo: &Repo, + base_ref_or_oid: &str, + latest_ref_or_oid: &str, +) -> Result<(Vec, Vec)> { + let base = git_repo.get_commit_or_tip_of_reference(base_ref_or_oid)?; + let latest = git_repo.get_commit_or_tip_of_reference(latest_ref_or_oid)?; + git_repo.get_commits_ahead_behind(&base, &latest) +} diff --git a/examples/git_remote_nostr/main.rs b/examples/git_remote_nostr/main.rs new file mode 100644 index 0000000000..1d840e9e27 --- /dev/null +++ b/examples/git_remote_nostr/main.rs @@ -0,0 +1,139 @@ +#![cfg_attr(not(test), warn(clippy::pedantic))] +#![allow(clippy::large_futures, clippy::module_name_repetitions)] +// better solution to dead_code error on multiple binaries than https://stackoverflow.com/a/66196291 +#![allow(dead_code)] +#![cfg_attr(not(test), warn(clippy::expect_used))] + +use core::str; +use std::{ + collections::HashSet, + env, io, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::{bail, Context, Result}; +use client::{consolidate_fetch_reports, get_repo_ref_from_cache, Connect}; +use git::{nostr_url::NostrUrlDecoded, RepoActions}; +use gnostr::{client, git}; +use nostr_0_34_1::nips::nip01::Coordinate; +use utils::read_line; + +use crate::{client::Client, git::Repo}; + +mod fetch; +mod list; +mod push; +mod utils; + +#[tokio::main] +async fn main() -> Result<()> { + let args = env::args(); + let args = args.skip(1).take(2).collect::>(); + + let ([_, nostr_remote_url] | [nostr_remote_url]) = args.as_slice() else { + bail!("invalid arguments - no url"); + }; + if env::args().nth(1).as_deref() == Some("--version") { + const VERSION: &str = env!("CARGO_PKG_VERSION"); + println!("v{VERSION}"); + return Ok(()); + } + + let git_repo = Repo::from_path(&PathBuf::from( + std::env::var("GIT_DIR").context("git should set GIT_DIR when remote helper is called")?, + ))?; + let git_repo_path = git_repo.get_path()?; + + let client = Client::default(); + + let decoded_nostr_url = + NostrUrlDecoded::from_str(nostr_remote_url).context("invalid nostr url")?; + + fetching_with_report_for_helper(git_repo_path, &client, &decoded_nostr_url.coordinates).await?; + + let repo_ref = get_repo_ref_from_cache(git_repo_path, &decoded_nostr_url.coordinates).await?; + + let stdin = io::stdin(); + let mut line = String::new(); + + let mut list_outputs = None; + loop { + let tokens = read_line(&stdin, &mut line)?; + + match tokens.as_slice() { + ["capabilities"] => { + println!("option"); + println!("push"); + println!("fetch"); + println!(); + } + ["option", "verbosity"] => { + println!("ok"); + } + ["option", ..] => { + println!("unsupported"); + } + ["fetch", oid, refstr] => { + fetch::run_fetch( + &git_repo, + &repo_ref, + &decoded_nostr_url, + &stdin, + oid, + refstr, + ) + .await?; + } + ["push", refspec] => { + push::run_push( + &git_repo, + &repo_ref, + &decoded_nostr_url, + &stdin, + refspec, + &client, + list_outputs.clone(), + ) + .await?; + } + ["list"] => { + list_outputs = + Some(list::run_list(&git_repo, &repo_ref, &decoded_nostr_url, false).await?); + } + ["list", "for-push"] => { + list_outputs = + Some(list::run_list(&git_repo, &repo_ref, &decoded_nostr_url, true).await?); + } + [] => { + return Ok(()); + } + _ => { + bail!(format!("unknown command: {}", line.trim().to_owned())); + } + } + } +} + +async fn fetching_with_report_for_helper( + git_repo_path: &Path, + client: &Client, + repo_coordinates: &HashSet, +) -> Result<()> { + let term = console::Term::stderr(); + term.write_line("nostr: fetching...")?; + let (relay_reports, progress_reporter) = client + .fetch_all(git_repo_path, repo_coordinates, &HashSet::new()) + .await?; + if !relay_reports.iter().any(std::result::Result::is_err) { + let _ = progress_reporter.clear(); + term.clear_last_lines(1)?; + } + let report = consolidate_fetch_reports(relay_reports); + if report.to_string().is_empty() { + term.write_line("nostr: no updates")?; + } else { + term.write_line(&format!("nostr updates: {report}"))?; + } + Ok(()) +} diff --git a/examples/git_remote_nostr/push.rs b/examples/git_remote_nostr/push.rs new file mode 100644 index 0000000000..87bedbd18a --- /dev/null +++ b/examples/git_remote_nostr/push.rs @@ -0,0 +1,1234 @@ +use core::str; +use std::{ + collections::{HashMap, HashSet}, + io::Stdin, + sync::{Arc, Mutex}, + time::Instant, +}; + +use anyhow::{anyhow, bail, Context, Result}; +use auth_git2::GitAuthenticator; +use client::{get_events_from_cache, get_state_from_cache, send_events, sign_event, STATE_KIND}; +use console::Term; +use git::{sha1_to_oid, RepoActions}; +use git2::{Oid, Repository}; +use git_events::{ + generate_cover_letter_and_patch_events, generate_patch_event, get_commit_id_from_patch, +}; +use gnostr::{ + client::{self, get_event_from_cache_by_id}, + git::{ + self, + nostr_url::{CloneUrl, NostrUrlDecoded}, + oid_to_shorthand_string, + }, + git_events::{self, get_event_root}, + login::{self, get_curent_user}, + repo_ref, repo_state, +}; +use nostr_0_34_1::nips::nip10::Marker; +use nostr_sdk_0_34_0::{ + hashes::sha1::Hash as Sha1Hash, Event, EventBuilder, EventId, Kind, PublicKey, Tag, +}; +use nostr_signer_0_34_0::NostrSigner; +use repo_ref::RepoRef; +use repo_state::RepoState; + +use crate::{ + client::Client, + git::Repo, + list::list_from_remotes, + utils::{ + count_lines_per_msg_vec, find_proposal_and_patches_by_branch_name, get_all_proposals, + get_remote_name_by_url, get_short_git_server_name, get_write_protocols_to_try, + join_with_and, push_error_is_not_authentication_failure, read_line, + set_protocol_preference, Direction, + }, +}; + +#[allow(clippy::too_many_lines)] +pub async fn run_push( + git_repo: &Repo, + repo_ref: &RepoRef, + decoded_nostr_url: &NostrUrlDecoded, + stdin: &Stdin, + initial_refspec: &str, + client: &Client, + list_outputs: Option>>, +) -> Result<()> { + let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?; + + let proposal_refspecs = refspecs + .iter() + .filter(|r| r.contains("refs/heads/pr/")) + .cloned() + .collect::>(); + + let mut git_server_refspecs = refspecs + .iter() + .filter(|r| !r.contains("refs/heads/pr/")) + .cloned() + .collect::>(); + + let term = console::Term::stderr(); + + let list_outputs = match list_outputs { + Some(outputs) => outputs, + _ => list_from_remotes(&term, git_repo, &repo_ref.git_server, decoded_nostr_url), + }; + + let nostr_state = get_state_from_cache(git_repo.get_path()?, repo_ref).await; + + let existing_state = { + // if no state events - create from first git server listed + if let Ok(nostr_state) = &nostr_state { + nostr_state.state.clone() + } else if let Some(url) = repo_ref + .git_server + .iter() + .find(|&url| list_outputs.contains_key(url)) + { + list_outputs.get(url).unwrap().to_owned() + } else { + bail!( + "cannot connect to git servers: {}", + repo_ref.git_server.join(" ") + ); + } + }; + + let (rejected_refspecs, remote_refspecs) = create_rejected_refspecs_and_remotes_refspecs( + &term, + git_repo, + &git_server_refspecs, + &existing_state, + &list_outputs, + )?; + + git_server_refspecs.retain(|refspec| { + if let Some(rejected) = rejected_refspecs.get(&refspec.to_string()) { + let (_, to) = refspec_to_from_to(refspec).unwrap(); + println!("error {to} {} out of sync with nostr", rejected.join(" ")); + false + } else { + true + } + }); + + let mut events = vec![]; + + if git_server_refspecs.is_empty() && proposal_refspecs.is_empty() { + // all refspecs rejected + println!(); + return Ok(()); + } + + let (signer, user_ref) = login::launch( + git_repo, + &None, + &None, + &None, + &None, + Some(client), + false, + true, + ) + .await?; + + if !repo_ref.maintainers.contains(&user_ref.public_key) { + for refspec in &git_server_refspecs { + let (_, to) = refspec_to_from_to(refspec).unwrap(); + println!( + "error {to} your nostr account {} isn't listed as a maintainer of the repo", + user_ref.metadata.name + ); + } + git_server_refspecs.clear(); + if proposal_refspecs.is_empty() { + println!(); + return Ok(()); + } + } + + if !git_server_refspecs.is_empty() { + let new_state = generate_updated_state(git_repo, &existing_state, &git_server_refspecs)?; + + let new_repo_state = + RepoState::build(repo_ref.identifier.clone(), new_state, &signer).await?; + + events.push(new_repo_state.event); + + for event in get_merged_status_events( + &term, + repo_ref, + git_repo, + &decoded_nostr_url.original_string, + &signer, + &git_server_refspecs, + ) + .await? + { + events.push(event); + } + } + + let mut rejected_proposal_refspecs = vec![]; + if !proposal_refspecs.is_empty() { + let all_proposals = get_all_proposals(git_repo, repo_ref).await?; + let current_user = get_curent_user(git_repo)?; + + for refspec in &proposal_refspecs { + let (from, to) = refspec_to_from_to(refspec).unwrap(); + let tip_of_pushed_branch = git_repo.get_commit_or_tip_of_reference(from)?; + + if let Some((_, (proposal, patches))) = + find_proposal_and_patches_by_branch_name(to, &all_proposals, ¤t_user) + { + if [repo_ref.maintainers.clone(), vec![proposal.author()]] + .concat() + .contains(&user_ref.public_key) + { + if refspec.starts_with('+') { + // force push + let (_, main_tip) = git_repo.get_main_or_master_branch()?; + let (mut ahead, _) = + git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; + ahead.reverse(); + for patch in generate_cover_letter_and_patch_events( + None, + git_repo, + &ahead, + &signer, + repo_ref, + &Some(proposal.id().to_string()), + &[], + ) + .await? + { + events.push(patch); + } + } else { + // fast forward push + let tip_patch = patches.first().unwrap(); + let tip_of_proposal = get_commit_id_from_patch(tip_patch)?; + let tip_of_proposal_commit = + git_repo.get_commit_or_tip_of_reference(&tip_of_proposal)?; + + let (mut ahead, behind) = git_repo.get_commits_ahead_behind( + &tip_of_proposal_commit, + &tip_of_pushed_branch, + )?; + if behind.is_empty() { + let thread_id = if let Ok(root_event_id) = get_event_root(tip_patch) { + root_event_id + } else { + // tip patch is the root proposal + tip_patch.id() + }; + let mut parent_patch = tip_patch.clone(); + ahead.reverse(); + for (i, commit) in ahead.iter().enumerate() { + let new_patch = generate_patch_event( + git_repo, + &git_repo.get_root_commit()?, + commit, + Some(thread_id), + &signer, + repo_ref, + Some(parent_patch.id()), + Some(( + (patches.len() + i + 1).try_into().unwrap(), + (patches.len() + ahead.len()).try_into().unwrap(), + )), + None, + &None, + &[], + ) + .await + .context("cannot make patch event from commit")?; + events.push(new_patch.clone()); + parent_patch = new_patch; + } + } else { + // we shouldn't get here + term.write_line( + format!( + "WARNING: failed to push {from} as nostr proposal. Try and force push ", + ) + .as_str(), + ) + .unwrap(); + println!( + "error {to} cannot fastforward as newer patches found on proposal" + ); + rejected_proposal_refspecs.push(refspec.to_string()); + } + } + } else { + println!( + "error {to} permission denied. you are not the proposal author or a repo maintainer" + ); + rejected_proposal_refspecs.push(refspec.to_string()); + } + } else { + // TODO new proposal / couldn't find exisiting + // proposal + let (_, main_tip) = git_repo.get_main_or_master_branch()?; + let (mut ahead, _) = + git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?; + ahead.reverse(); + for patch in generate_cover_letter_and_patch_events( + None, + git_repo, + &ahead, + &signer, + repo_ref, + &None, + &[], + ) + .await? + { + events.push(patch); + } + } + } + } + + // TODO check whether tip of each branch pushed is on at least one + // git server before broadcasting the nostr state + if !events.is_empty() { + send_events( + client, + git_repo.get_path()?, + events, + user_ref.relays.write(), + repo_ref.relays.clone(), + false, + true, + ) + .await?; + } + + for refspec in &[git_server_refspecs.clone(), proposal_refspecs.clone()].concat() { + if rejected_proposal_refspecs.contains(refspec) { + continue; + } + let (_, to) = refspec_to_from_to(refspec)?; + println!("ok {to}"); + update_remote_refs_pushed( + &git_repo.git_repo, + refspec, + &decoded_nostr_url.original_string, + ) + .context("could not update remote_ref locally")?; + } + + // TODO make async - check gitlib2 callbacks work async + + for (git_server_url, remote_refspecs) in remote_refspecs { + let remote_refspecs = remote_refspecs + .iter() + .filter(|refspec| git_server_refspecs.contains(refspec)) + .cloned() + .collect::>(); + if !refspecs.is_empty() { + let _ = push_to_remote( + git_repo, + &git_server_url, + decoded_nostr_url, + &remote_refspecs, + &term, + ); + } + } + println!(); + Ok(()) +} + +fn push_to_remote( + git_repo: &Repo, + git_server_url: &str, + decoded_nostr_url: &NostrUrlDecoded, + remote_refspecs: &[String], + term: &Term, +) -> Result<()> { + let server_url = git_server_url.parse::()?; + let protocols_to_attempt = get_write_protocols_to_try(git_repo, &server_url, decoded_nostr_url); + + let mut failed_protocols = vec![]; + let mut success = false; + + for protocol in &protocols_to_attempt { + term.write_line(format!("push: {} over {protocol}...", server_url.short_name(),).as_str())?; + + let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; + + if let Err(error) = push_to_remote_url(git_repo, &formatted_url, remote_refspecs, term) { + term.write_line( + format!("push: {formatted_url} failed over {protocol}: {error}").as_str(), + )?; + failed_protocols.push(protocol); + if push_error_is_not_authentication_failure(&error) { + break; + } + } else { + success = true; + if !failed_protocols.is_empty() { + term.write_line(format!("push: succeeded over {protocol}").as_str())?; + let _ = set_protocol_preference(git_repo, protocol, &server_url, &Direction::Push); + } + break; + } + } + if success { + Ok(()) + } else { + let error = anyhow!( + "{} failed over {}{}", + server_url.short_name(), + join_with_and(&failed_protocols), + if decoded_nostr_url.protocol.is_some() { + " and nostr url contains protocol override so no other protocols were attempted" + } else { + "" + }, + ); + term.write_line(format!("push: {error}").as_str())?; + Err(error) + } +} + +fn push_to_remote_url( + git_repo: &Repo, + git_server_url: &str, + remote_refspecs: &[String], + term: &Term, +) -> Result<()> { + let git_config = git_repo.git_repo.config()?; + let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; + let auth = GitAuthenticator::default(); + let mut push_options = git2::PushOptions::new(); + let mut remote_callbacks = git2::RemoteCallbacks::new(); + let push_reporter = Arc::new(Mutex::new(PushReporter::new(term))); + + remote_callbacks.credentials(auth.credentials(&git_config)); + + remote_callbacks.push_update_reference({ + let push_reporter = Arc::clone(&push_reporter); + move |name, error| { + let mut reporter = push_reporter.lock().unwrap(); + if let Some(error) = error { + let existing_lines = reporter.count_all_existing_lines(); + reporter.update_reference_errors.push(format!( + "WARNING: {} failed to push {name} error: {error}", + get_short_git_server_name(git_repo, git_server_url), + )); + reporter.write_all(existing_lines); + } + Ok(()) + } + }); + + remote_callbacks.push_negotiation({ + let push_reporter = Arc::clone(&push_reporter); + move |updates| { + let mut reporter = push_reporter.lock().unwrap(); + let existing_lines = reporter.count_all_existing_lines(); + + for update in updates { + let dst_refname = update + .dst_refname() + .unwrap_or("") + .replace("refs/heads/", "") + .replace("refs/tags/", "tags/"); + let msg = if update.dst().is_zero() { + format!("push: - [delete] {dst_refname}") + } else if update.src().is_zero() { + if update.dst_refname().unwrap_or("").contains("refs/tags") { + format!("push: * [new tag] {dst_refname}") + } else { + format!("push: * [new branch] {dst_refname}") + } + } else { + let force = remote_refspecs + .iter() + .any(|r| r.contains(&dst_refname) && r.contains('+')); + format!( + "push: {} {}..{} {} -> {dst_refname}", + if force { "+" } else { " " }, + oid_to_shorthand_string(update.src()).unwrap(), + oid_to_shorthand_string(update.dst()).unwrap(), + update + .src_refname() + .unwrap_or("") + .replace("refs/heads/", "") + .replace("refs/tags/", "tags/"), + ) + }; + // other possibilities will result in push to fail but + // better reporting is needed: + // deleting a non-existant branch: + // ! [remote rejected] -> + // (not found) adding a branch + // that already exists: ! [remote rejected] + // -> (already + // exists) pushing without non-fast-forward + // without force: ! [rejected] + // -> (non-fast-forward) + reporter.negotiation.push(msg); + } + reporter.write_all(existing_lines); + Ok(()) + } + }); + + remote_callbacks.push_transfer_progress({ + let push_reporter = Arc::clone(&push_reporter); + #[allow(clippy::cast_precision_loss)] + move |current, total, bytes| { + let mut reporter = push_reporter.lock().unwrap(); + reporter.process_transfer_progress_update(current, total, bytes); + } + }); + + remote_callbacks.sideband_progress({ + let push_reporter = Arc::clone(&push_reporter); + move |data| { + let mut reporter = push_reporter.lock().unwrap(); + reporter.process_remote_msg(data); + true + } + }); + push_options.remote_callbacks(remote_callbacks); + git_server_remote.push(remote_refspecs, Some(&mut push_options))?; + let _ = git_server_remote.disconnect(); + Ok(()) +} + +#[allow(clippy::cast_precision_loss)] +#[allow(clippy::float_cmp)] +#[allow(clippy::needless_pass_by_value)] +fn report_on_transfer_progress( + current: usize, + total: usize, + bytes: usize, + start_time: &Instant, + end_time: &Option, +) -> Option { + if total == 0 { + return None; + } + let percentage = ((current as f64 / total as f64) * 100.0) + // always round down because 100% complete is misleading when + // its not complete + .floor(); + let (size, unit) = if bytes as f64 >= (1024.0 * 1024.0) { + (bytes as f64 / (1024.0 * 1024.0), "MiB") + } else { + (bytes as f64 / 1024.0, "KiB") + }; + let speed = { + let duration = if let Some(end_time) = end_time { + (*end_time - *start_time).as_millis() as f64 + } else { + start_time.elapsed().as_millis() as f64 + }; + + if duration > 0.0 { + (bytes as f64 / (1024.0 * 1024.0)) / (duration / 1000.0) // Convert bytes to MiB and milliseconds to seconds + } else { + 0.0 + } + }; + + Some(format!( + "push: Writing objects: {percentage}% ({current}/{total}) {size:.2} {unit} | {speed:.2} MiB/s{}", + if current == total { ", done." } else { "" }, + )) +} + +struct PushReporter<'a> { + remote_msgs: Vec, + negotiation: Vec, + transfer_progress_msgs: Vec, + update_reference_errors: Vec, + term: &'a console::Term, + start_time: Option, + end_time: Option, +} +impl<'a> PushReporter<'a> { + fn new(term: &'a console::Term) -> Self { + Self { + remote_msgs: vec![], + negotiation: vec![], + transfer_progress_msgs: vec![], + update_reference_errors: vec![], + term, + start_time: None, + end_time: None, + } + } + fn write_all(&self, lines_to_clear: usize) { + let _ = self.term.clear_last_lines(lines_to_clear); + for msg in &self.remote_msgs { + let _ = self.term.write_line(format!("remote: {msg}").as_str()); + } + for msg in &self.negotiation { + let _ = self.term.write_line(msg); + } + for msg in &self.transfer_progress_msgs { + let _ = self.term.write_line(msg); + } + for msg in &self.update_reference_errors { + let _ = self.term.write_line(msg); + } + } + + fn count_all_existing_lines(&self) -> usize { + let width = self.term.size().1; + count_lines_per_msg_vec(width, &self.remote_msgs, "remote: ".len()) + + count_lines_per_msg_vec(width, &self.negotiation, 0) + + count_lines_per_msg_vec(width, &self.transfer_progress_msgs, 0) + + count_lines_per_msg_vec(width, &self.update_reference_errors, 0) + } + fn process_remote_msg(&mut self, data: &[u8]) { + if let Ok(data) = str::from_utf8(data) { + let data = data + .split(['\n', '\r']) + .map(str::trim) + .filter(|line| !line.trim().is_empty()) + .collect::>(); + for data in data { + let existing_lines = self.count_all_existing_lines(); + let msg = data.to_string(); + if let Some(last) = self.remote_msgs.last() { + if (last.contains('%') && !last.contains("100%")) + || last == &msg.replace(", done.", "") + { + self.remote_msgs.pop(); + self.remote_msgs.push(msg); + } else { + self.remote_msgs.push(msg); + } + } else { + self.remote_msgs.push(msg); + } + self.write_all(existing_lines); + } + } + } + fn process_transfer_progress_update(&mut self, current: usize, total: usize, bytes: usize) { + if self.start_time.is_none() { + self.start_time = Some(Instant::now()); + } + if let Some(report) = report_on_transfer_progress( + current, + total, + bytes, + &self.start_time.unwrap(), + &self.end_time, + ) { + let existing_lines = self.count_all_existing_lines(); + if report.contains("100%") { + self.end_time = Some(Instant::now()); + } + self.transfer_progress_msgs = vec![report]; + self.write_all(existing_lines); + } + } +} + +type HashMapUrlRefspecs = HashMap>; + +#[allow(clippy::too_many_lines)] +fn create_rejected_refspecs_and_remotes_refspecs( + term: &console::Term, + git_repo: &Repo, + refspecs: &Vec, + nostr_state: &HashMap, + list_outputs: &HashMap>, +) -> Result<(HashMapUrlRefspecs, HashMapUrlRefspecs)> { + let mut refspecs_for_remotes = HashMap::new(); + + let mut rejected_refspecs: HashMapUrlRefspecs = HashMap::new(); + + for (url, remote_state) in list_outputs { + let short_name = get_short_git_server_name(git_repo, url); + let mut refspecs_for_remote = vec![]; + for refspec in refspecs { + let (from, to) = refspec_to_from_to(refspec)?; + let nostr_value = nostr_state.get(to); + let remote_value = remote_state.get(to); + if from.is_empty() { + if remote_value.is_some() { + // delete remote branch + refspecs_for_remote.push(refspec.clone()); + } + continue; + } + let from_tip = git_repo.get_commit_or_tip_of_reference(from)?; + if let Some(nostr_value) = nostr_value { + if let Some(remote_value) = remote_value { + if nostr_value.eq(remote_value) { + // in sync - existing branch at same state + let is_remote_tip_ancestor_of_commit = if let Ok(remote_value_tip) = + git_repo.get_commit_or_tip_of_reference(remote_value) + { + if let Ok((_, behind)) = + git_repo.get_commits_ahead_behind(&remote_value_tip, &from_tip) + { + behind.is_empty() + } else { + false + } + } else { + false + }; + if is_remote_tip_ancestor_of_commit { + refspecs_for_remote.push(refspec.clone()); + } else { + // this is a force push so we need to + // force push to git server too + if refspec.starts_with('+') { + refspecs_for_remote.push(refspec.clone()); + } else { + refspecs_for_remote.push(format!("+{refspec}")); + } + } + } else if let Ok(remote_value_tip) = + git_repo.get_commit_or_tip_of_reference(remote_value) + { + if from_tip.eq(&remote_value_tip) { + // remote already at correct state + term.write_line( + format!("{short_name} {to} already up-to-date").as_str(), + )?; + } + let (ahead_of_local, behind_local) = + git_repo.get_commits_ahead_behind(&from_tip, &remote_value_tip)?; + if ahead_of_local.is_empty() { + // can soft push + refspecs_for_remote.push(refspec.clone()); + } else { + // cant soft push + let (ahead_of_nostr, behind_nostr) = git_repo + .get_commits_ahead_behind( + &git_repo.get_commit_or_tip_of_reference(nostr_value)?, + &remote_value_tip, + )?; + if ahead_of_nostr.is_empty() { + // ancestor of nostr and we are force + // pushing anyway... + refspecs_for_remote.push(refspec.clone()); + } else { + rejected_refspecs + .entry(refspec.to_string()) + .and_modify(|a| a.push(url.to_string())) + .or_insert(vec![url.to_string()]); + term.write_line( + format!( + "ERROR: {short_name} {to} conflicts with nostr ({} ahead {} behind) and local ({} ahead {} behind). either:\r\n 1. pull from that git server and resolve\r\n 2. force push your branch to the git server before pushing to nostr remote", + ahead_of_nostr.len(), + behind_nostr.len(), + ahead_of_local.len(), + behind_local.len(), + ).as_str(), + )?; + } + }; + } else { + // remote_value oid is not present locally + // TODO can we download the remote reference? + + // cant soft push + rejected_refspecs + .entry(refspec.to_string()) + .and_modify(|a| a.push(url.to_string())) + .or_insert(vec![url.to_string()]); + term.write_line( + format!("ERROR: {short_name} {to} conflicts with nostr and is not an ancestor of local branch. either:\r\n 1. pull from that git server and resolve\r\n 2. force push your branch to the git server before pushing to nostr remote").as_str(), + )?; + } + } else { + // existing nostr branch not on remote + // report - creating new branch + term.write_line( + format!( + "{short_name} {to} doesn't exist and will be added as a new branch" + ) + .as_str(), + )?; + refspecs_for_remote.push(refspec.clone()); + } + } else if let Some(remote_value) = remote_value { + // new to nostr but on remote + if let Ok(remote_value_tip) = git_repo.get_commit_or_tip_of_reference(remote_value) + { + let (ahead, behind) = + git_repo.get_commits_ahead_behind(&from_tip, &remote_value_tip)?; + if behind.is_empty() { + // can soft push + refspecs_for_remote.push(refspec.clone()); + } else { + // cant soft push + rejected_refspecs + .entry(refspec.to_string()) + .and_modify(|a| a.push(url.to_string())) + .or_insert(vec![url.to_string()]); + term.write_line( + format!( + "ERROR: {short_name} already contains {to} {} ahead and {} behind local branch. either:\r\n 1. pull from that git server and resolve\r\n 2. force push your branch to the git server before pushing to nostr remote", + ahead.len(), + behind.len(), + ).as_str(), + )?; + } + } else { + // havn't fetched oid from remote + // TODO fetch oid from remote + // cant soft push + rejected_refspecs + .entry(refspec.to_string()) + .and_modify(|a| a.push(url.to_string())) + .or_insert(vec![url.to_string()]); + term.write_line( + format!("ERROR: {short_name} already contains {to} at {remote_value} which is not an ancestor of local branch. either:\r\n 1. pull from that git server and resolve\r\n 2. force push your branch to the git server before pushing to nostr remote").as_str(), + )?; + } + } else { + // in sync - new branch + refspecs_for_remote.push(refspec.clone()); + } + } + if !refspecs_for_remote.is_empty() { + refspecs_for_remotes.insert(url.to_string(), refspecs_for_remote); + } + } + + // remove rejected refspecs so they dont get pushed to some + // remotes + let mut remotes_refspecs_without_rejected = HashMap::new(); + for (url, value) in &refspecs_for_remotes { + remotes_refspecs_without_rejected.insert( + url.to_string(), + value + .iter() + .filter(|refspec| !rejected_refspecs.contains_key(*refspec)) + .cloned() + .collect(), + ); + } + Ok((rejected_refspecs, remotes_refspecs_without_rejected)) +} + +fn generate_updated_state( + git_repo: &Repo, + existing_state: &HashMap, + refspecs: &Vec, +) -> Result> { + let mut new_state = existing_state.clone(); + + for refspec in refspecs { + let (from, to) = refspec_to_from_to(refspec)?; + if from.is_empty() { + // delete + new_state.remove(to); + if to.contains("refs/tags") { + new_state.remove(&format!("{to}{}", "^{}")); + } + } else if to.contains("refs/tags") { + new_state.insert( + format!("{to}{}", "^{}"), + git_repo + .get_commit_or_tip_of_reference(from) + .unwrap() + .to_string(), + ); + new_state.insert( + to.to_string(), + git_repo + .git_repo + .find_reference(to) + .unwrap() + .peel(git2::ObjectType::Tag) + .unwrap() + .id() + .to_string(), + ); + } else { + // add or update + new_state.insert( + to.to_string(), + git_repo + .get_commit_or_tip_of_reference(from) + .unwrap() + .to_string(), + ); + } + } + Ok(new_state) +} + +async fn get_merged_status_events( + term: &console::Term, + repo_ref: &RepoRef, + git_repo: &Repo, + remote_nostr_url: &str, + signer: &NostrSigner, + refspecs_to_git_server: &Vec, +) -> Result> { + let mut events = vec![]; + for refspec in refspecs_to_git_server { + let (from, to) = refspec_to_from_to(refspec)?; + if to.eq("refs/heads/main") || to.eq("refs/heads/master") { + let tip_of_pushed_branch = git_repo.get_commit_or_tip_of_reference(from)?; + let Ok(tip_of_remote_branch) = git_repo.get_commit_or_tip_of_reference( + &refspec_remote_ref_name(&git_repo.git_repo, refspec, remote_nostr_url)?, + ) else { + // branch not on remote + continue; + }; + let (ahead, _) = + git_repo.get_commits_ahead_behind(&tip_of_remote_branch, &tip_of_pushed_branch)?; + for commit_hash in ahead { + let commit = git_repo.git_repo.find_commit(sha1_to_oid(&commit_hash)?)?; + if commit.parent_count() > 1 { + // merge commit + for parent in commit.parents() { + // lookup parent id + let commit_events = get_events_from_cache( + git_repo.get_path()?, + vec![nostr_0_34_1::Filter::default() + .kind(nostr_0_34_1::Kind::GitPatch) + .reference(parent.id().to_string())], + ) + .await?; + if let Some(commit_event) = commit_events.iter().find(|e| { + e.tags.iter().any(|t| { + t.as_vec()[0].eq("commit") + && t.as_vec()[1].eq(&parent.id().to_string()) + }) + }) { + let (proposal_id, revision_id) = + get_proposal_and_revision_root_from_patch(git_repo, commit_event) + .await?; + term.write_line( + format!( + "merge commit {}: create nostr proposal status event", + &commit.id().to_string()[..7], + ) + .as_str(), + )?; + + events.push( + create_merge_status( + signer, + repo_ref, + &get_event_from_cache_by_id(git_repo, &proposal_id).await?, + &if let Some(revision_id) = revision_id { + Some( + get_event_from_cache_by_id(git_repo, &revision_id) + .await?, + ) + } else { + None + }, + &commit_hash, + commit_event.id(), + ) + .await?, + ); + } + } + } + } + } + } + Ok(events) +} + +async fn create_merge_status( + signer: &NostrSigner, + repo_ref: &RepoRef, + proposal: &Event, + revision: &Option, + merge_commit: &Sha1Hash, + merged_patch: EventId, +) -> Result { + let mut public_keys = repo_ref + .maintainers + .iter() + .copied() + .collect::>(); + public_keys.insert(proposal.author()); + if let Some(revision) = revision { + public_keys.insert(revision.author()); + } + sign_event( + EventBuilder::new( + nostr_0_34_1::event::Kind::GitStatusApplied, + String::new(), + [ + vec![ + Tag::custom( + nostr_0_34_1::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), + vec!["git proposal merged / applied".to_string()], + ), + Tag::from_standardized(nostr_0_34_1::TagStandard::Event { + event_id: proposal.id(), + relay_url: repo_ref.relays.first().map(nostr_0_34_1::UncheckedUrl::new), + marker: Some(Marker::Root), + public_key: None, + }), + Tag::from_standardized(nostr_0_34_1::TagStandard::Event { + event_id: merged_patch, + relay_url: repo_ref.relays.first().map(nostr_0_34_1::UncheckedUrl::new), + marker: Some(Marker::Mention), + public_key: None, + }), + ], + if let Some(revision) = revision { + vec![Tag::from_standardized(nostr_0_34_1::TagStandard::Event { + event_id: revision.id(), + relay_url: repo_ref.relays.first().map(nostr_0_34_1::UncheckedUrl::new), + marker: Some(Marker::Root), + public_key: None, + })] + } else { + vec![] + }, + public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(), + repo_ref + .coordinates() + .iter() + .map(|c| Tag::coordinate(c.clone())) + .collect::>(), + vec![ + Tag::from_standardized(nostr_0_34_1::TagStandard::Reference( + repo_ref.root_commit.to_string(), + )), + Tag::from_standardized(nostr_0_34_1::TagStandard::Reference(format!( + "{merge_commit}" + ))), + Tag::custom( + nostr_0_34_1::TagKind::Custom(std::borrow::Cow::Borrowed( + "merge-commit-id", + )), + vec![format!("{merge_commit}")], + ), + ], + ] + .concat(), + ), + signer, + ) + .await +} + +async fn get_proposal_and_revision_root_from_patch( + git_repo: &Repo, + patch: &Event, +) -> Result<(EventId, Option)> { + let proposal_or_revision = if patch.tags.iter().any(|t| t.as_vec()[1].eq("root")) { + patch.clone() + } else { + let proposal_or_revision_id = EventId::parse( + if let Some(t) = patch.tags.iter().find(|t| t.is_root()) { + t.clone() + } else if let Some(t) = patch.tags.iter().find(|t| t.is_reply()) { + t.clone() + } else { + Tag::event(patch.id()) + } + .as_vec()[1] + .clone(), + )?; + + get_events_from_cache( + git_repo.get_path()?, + vec![nostr_0_34_1::Filter::default().id(proposal_or_revision_id)], + ) + .await? + .first() + .unwrap() + .clone() + }; + + if !proposal_or_revision.kind().eq(&Kind::GitPatch) { + bail!("thread root is not a git patch"); + } + + if proposal_or_revision + .tags + .iter() + .any(|t| t.as_vec()[1].eq("revision-root")) + { + Ok(( + EventId::parse( + proposal_or_revision + .tags + .iter() + .find(|t| t.is_reply()) + .unwrap() + .as_vec()[1] + .clone(), + )?, + Some(proposal_or_revision.id()), + )) + } else { + Ok((proposal_or_revision.id(), None)) + } +} + +fn update_remote_refs_pushed( + git_repo: &Repository, + refspec: &str, + nostr_remote_url: &str, +) -> Result<()> { + let (from, _) = refspec_to_from_to(refspec)?; + + let target_ref_name = refspec_remote_ref_name(git_repo, refspec, nostr_remote_url)?; + + if from.is_empty() { + if let Ok(mut remote_ref) = git_repo.find_reference(&target_ref_name) { + remote_ref.delete()?; + } + } else { + let commit = reference_to_commit(git_repo, from) + .context(format!("cannot get commit of reference {from}"))?; + if let Ok(mut remote_ref) = git_repo.find_reference(&target_ref_name) { + remote_ref.set_target(commit, "updated by nostr remote helper")?; + } else { + git_repo.reference( + &target_ref_name, + commit, + false, + "created by nostr remote helper", + )?; + } + } + Ok(()) +} + +fn refspec_to_from_to(refspec: &str) -> Result<(&str, &str)> { + if !refspec.contains(':') { + bail!( + "refspec should contain a colon (:) but consists of: {}", + refspec + ); + } + let parts = refspec.split(':').collect::>(); + Ok(( + if parts.first().unwrap().starts_with('+') { + &parts.first().unwrap()[1..] + } else { + parts.first().unwrap() + }, + parts.get(1).unwrap(), + )) +} + +fn refspec_remote_ref_name( + git_repo: &Repository, + refspec: &str, + nostr_remote_url: &str, +) -> Result { + let (_, to) = refspec_to_from_to(refspec)?; + let nostr_remote = git_repo + .find_remote(&get_remote_name_by_url(git_repo, nostr_remote_url)?) + .context("we should have just located this remote")?; + Ok(format!( + "refs/remotes/{}/{}", + nostr_remote.name().context("remote should have a name")?, + to.replace("refs/heads/", ""), /* TODO only replace if it + * begins with this + * TODO what about tags? */ + )) +} + +fn reference_to_commit(git_repo: &Repository, reference: &str) -> Result { + Ok(git_repo + .find_reference(reference) + .context(format!("cannot find reference: {reference}"))? + .peel_to_commit() + .context(format!("cannot get commit from reference: {reference}"))? + .id()) +} + +// this maybe a commit id or a ref: pointer +fn reference_to_ref_value(git_repo: &Repository, reference: &str) -> Result { + let reference_obj = git_repo + .find_reference(reference) + .context(format!("cannot find reference: {reference}"))?; + if let Some(symref) = reference_obj.symbolic_target() { + Ok(symref.to_string()) + } else { + Ok(reference_obj + .peel_to_commit() + .context(format!("cannot get commit from reference: {reference}"))? + .id() + .to_string()) + } +} + +fn get_refspecs_from_push_batch(stdin: &Stdin, initial_refspec: &str) -> Result> { + let mut line = String::new(); + let mut refspecs = vec![initial_refspec.to_string()]; + loop { + let tokens = read_line(stdin, &mut line)?; + match tokens.as_slice() { + ["push", spec] => { + refspecs.push((*spec).to_string()); + } + [] => break, + _ => { + bail!("after a `push` command we are only expecting another push or an empty line") + } + } + } + Ok(refspecs) +} + +trait BuildRepoState { + async fn build( + identifier: String, + state: HashMap, + signer: &NostrSigner, + ) -> Result; +} +impl BuildRepoState for RepoState { + async fn build( + identifier: String, + state: HashMap, + signer: &NostrSigner, + ) -> Result { + let mut tags = vec![Tag::identifier(identifier.clone())]; + for (name, value) in &state { + tags.push(Tag::custom( + nostr_sdk_0_34_0::TagKind::Custom(name.into()), + vec![value.clone()], + )); + } + let event = sign_event(EventBuilder::new(STATE_KIND, "", tags), signer).await?; + Ok(RepoState { + identifier, + state, + event, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod refspec_to_from_to { + use super::*; + + #[test] + fn trailing_plus_stripped() { + let (from, _) = refspec_to_from_to("+testing:testingb").unwrap(); + assert_eq!(from, "testing"); + } + } +} diff --git a/examples/git_remote_nostr/utils.rs b/examples/git_remote_nostr/utils.rs new file mode 100644 index 0000000000..d872d29474 --- /dev/null +++ b/examples/git_remote_nostr/utils.rs @@ -0,0 +1,445 @@ +use core::str; +use std::{ + collections::HashMap, + fmt, + io::{self, Stdin}, + str::FromStr, +}; + +use anyhow::{bail, Context, Result}; +use git2::Repository; +use gnostr::{ + client::{ + get_all_proposal_patch_events_from_cache, get_events_from_cache, + get_proposals_and_revisions_from_cache, + }, + git::{ + nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, + Repo, RepoActions, + }, + git_events::{ + event_is_revision_root, get_most_recent_patch_with_ancestors, + is_event_proposal_root_for_branch, status_kinds, + }, + repo_ref::RepoRef, +}; +use nostr_sdk_0_34_0::{Event, EventId, Kind, PublicKey, Url}; + +pub fn get_short_git_server_name(git_repo: &Repo, url: &str) -> std::string::String { + if let Ok(name) = get_remote_name_by_url(&git_repo.git_repo, url) { + return name; + } + if let Ok(url) = Url::parse(url) { + if let Some(domain) = url.domain() { + return domain.to_string(); + } + } + url.to_string() +} + +pub fn get_remote_name_by_url(git_repo: &Repository, url: &str) -> Result { + let remotes = git_repo.remotes()?; + Ok(remotes + .iter() + .find(|r| { + if let Some(name) = r { + if let Some(remote_url) = git_repo.find_remote(name).unwrap().url() { + url == remote_url + } else { + false + } + } else { + false + } + }) + .context("could not find remote with matching url")? + .context("remote with matching url must be named")? + .to_string()) +} + +pub fn get_oids_from_fetch_batch( + stdin: &Stdin, + initial_oid: &str, + initial_refstr: &str, +) -> Result> { + let mut line = String::new(); + let mut batch = HashMap::new(); + batch.insert(initial_refstr.to_string(), initial_oid.to_string()); + loop { + let tokens = read_line(stdin, &mut line)?; + match tokens.as_slice() { + ["fetch", oid, refstr] => { + batch.insert((*refstr).to_string(), (*oid).to_string()); + } + [] => break, + _ => bail!( + "after a `fetch` command we are only expecting another fetch or an empty line" + ), + } + } + Ok(batch) +} + +/// Read one line from stdin, and split it into tokens. +pub fn read_line<'a>(stdin: &io::Stdin, line: &'a mut String) -> io::Result> { + line.clear(); + + let read = stdin.read_line(line)?; + if read == 0 { + return Ok(vec![]); + } + let line = line.trim(); + let tokens = line.split(' ').filter(|t| !t.is_empty()).collect(); + + Ok(tokens) +} + +pub async fn get_open_proposals( + git_repo: &Repo, + repo_ref: &RepoRef, +) -> Result)>> { + let git_repo_path = git_repo.get_path()?; + let proposals: Vec = + get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) + .await? + .iter() + .filter(|e| !event_is_revision_root(e)) + .cloned() + .collect(); + + let statuses: Vec = { + let mut statuses = get_events_from_cache( + git_repo_path, + vec![nostr_0_34_1::Filter::default() + .kinds(status_kinds().clone()) + .events(proposals.iter().map(nostr_0_34_1::Event::id))], + ) + .await?; + statuses.sort_by_key(|e| e.created_at); + statuses.reverse(); + statuses + }; + let mut open_proposals = HashMap::new(); + + for proposal in proposals { + let status = if let Some(e) = statuses + .iter() + .filter(|e| { + status_kinds().contains(&e.kind()) + && e.tags() + .iter() + .any(|t| t.as_vec()[1].eq(&proposal.id.to_string())) + }) + .collect::>() + .first() + { + e.kind() + } else { + Kind::GitStatusOpen + }; + if status.eq(&Kind::GitStatusOpen) { + if let Ok(commits_events) = + get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id) + .await + { + if let Ok(most_recent_proposal_patch_chain) = + get_most_recent_patch_with_ancestors(commits_events.clone()) + { + open_proposals + .insert(proposal.id(), (proposal, most_recent_proposal_patch_chain)); + } + } + } + } + Ok(open_proposals) +} + +pub async fn get_all_proposals( + git_repo: &Repo, + repo_ref: &RepoRef, +) -> Result)>> { + let git_repo_path = git_repo.get_path()?; + let proposals: Vec = + get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) + .await? + .iter() + .filter(|e| !event_is_revision_root(e)) + .cloned() + .collect(); + + let mut all_proposals = HashMap::new(); + + for proposal in proposals { + if let Ok(commits_events) = + get_all_proposal_patch_events_from_cache(git_repo_path, repo_ref, &proposal.id).await + { + if let Ok(most_recent_proposal_patch_chain) = + get_most_recent_patch_with_ancestors(commits_events.clone()) + { + all_proposals.insert(proposal.id(), (proposal, most_recent_proposal_patch_chain)); + } + } + } + Ok(all_proposals) +} + +pub fn find_proposal_and_patches_by_branch_name<'a>( + refstr: &'a str, + open_proposals: &'a HashMap)>, + current_user: &Option, +) -> Option<(&'a EventId, &'a (Event, Vec))> { + open_proposals.iter().find(|(_, (proposal, _))| { + is_event_proposal_root_for_branch(proposal, refstr, current_user).unwrap_or(false) + }) +} + +pub fn join_with_and(items: &[T]) -> String { + match items.len() { + 0 => String::new(), + 1 => items[0].to_string(), + _ => { + let last_item = items.last().unwrap().to_string(); + let rest = &items[..items.len() - 1]; + format!( + "{} and {}", + rest.iter() + .map(std::string::ToString::to_string) + .collect::>() + .join(", "), + last_item + ) + } + } +} + +/// get an ordered vector of server protocols to attempt +pub fn get_read_protocols_to_try( + git_repo: &Repo, + server_url: &CloneUrl, + decoded_nostr_url: &NostrUrlDecoded, +) -> Vec { + if server_url.protocol() == ServerProtocol::Filesystem { + vec![(ServerProtocol::Filesystem)] + } else if let Some(protocol) = &decoded_nostr_url.protocol { + vec![protocol.clone()] + } else { + let mut list = if server_url.protocol() == ServerProtocol::Http { + vec![ + ServerProtocol::UnauthHttp, + ServerProtocol::Ssh, + // note: list and fetch stop here if ssh was + // authenticated + ServerProtocol::Http, + ] + } else if server_url.protocol() == ServerProtocol::Ftp { + vec![ServerProtocol::Ftp, ServerProtocol::Ssh] + } else { + vec![ + ServerProtocol::UnauthHttps, + ServerProtocol::Ssh, + // note: list and fetch stop here if ssh was + // authenticated + ServerProtocol::Https, + ] + }; + if let Some(protocol) = get_protocol_preference(git_repo, server_url, &Direction::Fetch) { + if let Some(pos) = list.iter().position(|p| *p == protocol) { + list.remove(pos); + list.insert(0, protocol); + } + } + list + } +} + +/// get an ordered vector of server protocols to attempt +pub fn get_write_protocols_to_try( + git_repo: &Repo, + server_url: &CloneUrl, + decoded_nostr_url: &NostrUrlDecoded, +) -> Vec { + if server_url.protocol() == ServerProtocol::Filesystem { + vec![(ServerProtocol::Filesystem)] + } else if let Some(protocol) = &decoded_nostr_url.protocol { + vec![protocol.clone()] + } else { + let mut list = if server_url.protocol() == ServerProtocol::Http { + vec![ + ServerProtocol::Ssh, + // note: list and fetch stop here if ssh was + // authenticated + ServerProtocol::Http, + ] + } else if server_url.protocol() == ServerProtocol::Ftp { + vec![ServerProtocol::Ssh, ServerProtocol::Ftp] + } else { + vec![ + ServerProtocol::Ssh, + // note: list and fetch stop here if ssh was + // authenticated + ServerProtocol::Https, + ] + }; + if let Some(protocol) = get_protocol_preference(git_repo, server_url, &Direction::Push) { + if let Some(pos) = list.iter().position(|p| *p == protocol) { + list.remove(pos); + list.insert(0, protocol); + } + } + + list + } +} + +#[derive(Debug, PartialEq)] +pub enum Direction { + Push, + Fetch, +} +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Direction::Push => write!(f, "push"), + Direction::Fetch => write!(f, "fetch"), + } + } +} + +pub fn get_protocol_preference( + git_repo: &Repo, + server_url: &CloneUrl, + direction: &Direction, +) -> Option { + let server_short_name = server_url.short_name(); + if let Ok(Some(list)) = + git_repo.get_git_config_item(format!("nostr.protocol-{direction}").as_str(), Some(false)) + { + for item in list.split(';') { + let pair = item.split(',').collect::>(); + if let Some(url) = pair.get(1) { + if *url == server_short_name { + if let Some(protocol) = pair.first() { + if let Ok(protocol) = ServerProtocol::from_str(protocol) { + return Some(protocol); + } + } + } + } + } + } + None +} + +pub fn set_protocol_preference( + git_repo: &Repo, + protocol: &ServerProtocol, + server_url: &CloneUrl, + direction: &Direction, +) -> Result<()> { + let server_short_name = server_url.short_name(); + let mut new = String::new(); + if let Some(list) = + git_repo.get_git_config_item(format!("nostr.protocol-{direction}").as_str(), Some(false))? + { + for item in list.split(';') { + let pair = item.split(',').collect::>(); + if let Some(url) = pair.get(1) { + if *url != server_short_name && !item.is_empty() { + new.push_str(format!("{item};").as_str()); + } + } + } + } + new.push_str(format!("{protocol},{server_short_name};").as_str()); + + git_repo.save_git_config_item( + format!("nostr.protocol-{direction}").as_str(), + new.as_str(), + false, + ) +} + +/// to understand whether to try over another protocol +pub fn fetch_or_list_error_is_not_authentication_failure(error: &anyhow::Error) -> bool { + !error_might_be_authentication_related(error) +} + +/// to understand whether to try over another protocol +pub fn push_error_is_not_authentication_failure(error: &anyhow::Error) -> bool { + !error_might_be_authentication_related(error) +} + +pub fn error_might_be_authentication_related(error: &anyhow::Error) -> bool { + let error_str = error.to_string(); + for s in [ + "no ssh keys found", + "invalid or unknown remote ssh hostkey", + "authentication", + "Permission", + "permission", + "not found", + ] { + if error_str.contains(s) { + return true; + } + } + false +} + +fn count_lines_per_msg(width: u16, msg: &str, prefix_len: usize) -> usize { + if width == 0 { + return 1; + } + // ((msg_len+prefix) / width).ceil() implemented using Integer + // Arithmetic + ((msg.chars().count() + prefix_len) + (width - 1) as usize) / width as usize +} + +pub fn count_lines_per_msg_vec(width: u16, msgs: &[String], prefix_len: usize) -> usize { + msgs.iter() + .map(|msg| count_lines_per_msg(width, msg, prefix_len)) + .sum() +} + +#[cfg(test)] +mod tests { + use super::*; + mod join_with_and { + use super::*; + #[test] + fn test_empty() { + let items: Vec<&str> = vec![]; + assert_eq!(join_with_and(&items), ""); + } + + #[test] + fn test_single_item() { + let items = vec!["apple"]; + assert_eq!(join_with_and(&items), "apple"); + } + + #[test] + fn test_two_items() { + let items = vec!["apple", "banana"]; + assert_eq!(join_with_and(&items), "apple and banana"); + } + + #[test] + fn test_three_items() { + let items = vec!["apple", "banana", "cherry"]; + assert_eq!(join_with_and(&items), "apple, banana and cherry"); + } + + #[test] + fn test_four_items() { + let items = vec!["apple", "banana", "cherry", "date"]; + assert_eq!(join_with_and(&items), "apple, banana, cherry and date"); + } + + #[test] + fn test_multiple_items() { + let items = vec!["one", "two", "three", "four", "five"]; + assert_eq!(join_with_and(&items), "one, two, three, four and five"); + } + } +} diff --git a/src/bin/gnostr-bech32-to-any.rs b/examples/gnostr-bech32-to-any.rs similarity index 100% rename from src/bin/gnostr-bech32-to-any.rs rename to examples/gnostr-bech32-to-any.rs diff --git a/src/bin/gnostr-blame.rs b/examples/gnostr-blame.rs similarity index 100% rename from src/bin/gnostr-blame.rs rename to examples/gnostr-blame.rs diff --git a/src/bin/gnostr-core._rs b/examples/gnostr-core._rs similarity index 100% rename from src/bin/gnostr-core._rs rename to examples/gnostr-core._rs diff --git a/src/bin/gnostr-decrypt-private_key.rs b/examples/gnostr-decrypt-private_key.rs similarity index 100% rename from src/bin/gnostr-decrypt-private_key.rs rename to examples/gnostr-decrypt-private_key.rs diff --git a/src/bin/gnostr-diff.rs b/examples/gnostr-diff.rs similarity index 100% rename from src/bin/gnostr-diff.rs rename to examples/gnostr-diff.rs diff --git a/src/bin/gnostr-dump-relay.rs b/examples/gnostr-dump-relay.rs similarity index 100% rename from src/bin/gnostr-dump-relay.rs rename to examples/gnostr-dump-relay.rs diff --git a/src/bin/gnostr-encrypt-private_key.rs b/examples/gnostr-encrypt-private_key.rs similarity index 100% rename from src/bin/gnostr-encrypt-private_key.rs rename to examples/gnostr-encrypt-private_key.rs diff --git a/src/bin/gnostr-fetch-by-id-with-login.rs b/examples/gnostr-fetch-by-id-with-login.rs similarity index 100% rename from src/bin/gnostr-fetch-by-id-with-login.rs rename to examples/gnostr-fetch-by-id-with-login.rs diff --git a/src/bin/gnostr-fetch-by-id.rs b/examples/gnostr-fetch-by-id.rs similarity index 100% rename from src/bin/gnostr-fetch-by-id.rs rename to examples/gnostr-fetch-by-id.rs diff --git a/src/bin/gnostr-fetch-by-kind-and-author.rs b/examples/gnostr-fetch-by-kind-and-author.rs similarity index 100% rename from src/bin/gnostr-fetch-by-kind-and-author.rs rename to examples/gnostr-fetch-by-kind-and-author.rs diff --git a/src/bin/gnostr-fetch-metadata.rs b/examples/gnostr-fetch-metadata.rs similarity index 100% rename from src/bin/gnostr-fetch-metadata.rs rename to examples/gnostr-fetch-metadata.rs diff --git a/src/bin/gnostr-fetch-nip11.rs b/examples/gnostr-fetch-nip11.rs similarity index 100% rename from src/bin/gnostr-fetch-nip11.rs rename to examples/gnostr-fetch-nip11.rs diff --git a/src/bin/gnostr-fetch-pubkey-relays.rs b/examples/gnostr-fetch-pubkey-relays.rs similarity index 100% rename from src/bin/gnostr-fetch-pubkey-relays.rs rename to examples/gnostr-fetch-pubkey-relays.rs diff --git a/src/bin/gnostr-fetch-relay-list.rs b/examples/gnostr-fetch-relay-list.rs similarity index 100% rename from src/bin/gnostr-fetch-relay-list.rs rename to examples/gnostr-fetch-relay-list.rs diff --git a/src/bin/gnostr-fetch-watch-list-iterator.rs b/examples/gnostr-fetch-watch-list-iterator.rs similarity index 100% rename from src/bin/gnostr-fetch-watch-list-iterator.rs rename to examples/gnostr-fetch-watch-list-iterator.rs diff --git a/src/bin/gnostr-fetch-watch-list.rs b/examples/gnostr-fetch-watch-list.rs similarity index 100% rename from src/bin/gnostr-fetch-watch-list.rs rename to examples/gnostr-fetch-watch-list.rs diff --git a/src/bin/gnostr-form-event-addr.rs b/examples/gnostr-form-event-addr.rs similarity index 100% rename from src/bin/gnostr-form-event-addr.rs rename to examples/gnostr-form-event-addr.rs diff --git a/src/bin/gnostr-generate-keypair.rs b/examples/gnostr-generate-keypair.rs similarity index 100% rename from src/bin/gnostr-generate-keypair.rs rename to examples/gnostr-generate-keypair.rs diff --git a/src/bin/gnostr-get-relays.rs b/examples/gnostr-get-relays.rs similarity index 100% rename from src/bin/gnostr-get-relays.rs rename to examples/gnostr-get-relays.rs diff --git a/src/bin/gnostr-hash.rs b/examples/gnostr-hash.rs similarity index 100% rename from src/bin/gnostr-hash.rs rename to examples/gnostr-hash.rs diff --git a/src/bin/gnostr-init.rs b/examples/gnostr-init.rs similarity index 100% rename from src/bin/gnostr-init.rs rename to examples/gnostr-init.rs diff --git a/src/bin/gnostr-pi.rs b/examples/gnostr-pi.rs similarity index 100% rename from src/bin/gnostr-pi.rs rename to examples/gnostr-pi.rs diff --git a/src/bin/gnostr-privkey-to-bech32.rs b/examples/gnostr-privkey-to-bech32.rs similarity index 100% rename from src/bin/gnostr-privkey-to-bech32.rs rename to examples/gnostr-privkey-to-bech32.rs diff --git a/src/bin/gnostr-pubkey-to-bech32.rs b/examples/gnostr-pubkey-to-bech32.rs similarity index 100% rename from src/bin/gnostr-pubkey-to-bech32.rs rename to examples/gnostr-pubkey-to-bech32.rs diff --git a/src/bin/gnostr-reflog.rs b/examples/gnostr-reflog.rs similarity index 100% rename from src/bin/gnostr-reflog.rs rename to examples/gnostr-reflog.rs diff --git a/examples/gnostr-retry.rs b/examples/gnostr-retry.rs new file mode 100644 index 0000000000..52afa9d5a9 --- /dev/null +++ b/examples/gnostr-retry.rs @@ -0,0 +1,16 @@ +use gnostr::utils::retry::GnostrRetry; + +fn my_sync_fn(_n: &str) -> Result<(), std::io::Error> { + println!("my_sync_fn({})", _n); + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "generic error", + )) +} + +fn main() { + // Retry the operation with a linear strategy (1 second delay, 2 retries) + let retry_strategy = GnostrRetry::new_linear(1, 2); + let result = retry_strategy.run(|| my_sync_fn("Hi")); + assert!(result.is_err()); +} diff --git a/src/bin/gnostr-tag.rs b/examples/gnostr-tag.rs similarity index 100% rename from src/bin/gnostr-tag.rs rename to examples/gnostr-tag.rs diff --git a/src/bin/gnostr-verify-keypair.rs b/examples/gnostr-verify-keypair.rs similarity index 100% rename from src/bin/gnostr-verify-keypair.rs rename to examples/gnostr-verify-keypair.rs diff --git a/src/bin/gnostr-xor.rs b/examples/gnostr-xor.rs similarity index 100% rename from src/bin/gnostr-xor.rs rename to examples/gnostr-xor.rs diff --git a/src/bin/id_to_bech32.rs b/examples/id_to_bech32.rs similarity index 100% rename from src/bin/id_to_bech32.rs rename to examples/id_to_bech32.rs diff --git a/examples/nostr-sqlite.rs b/examples/nostr-sqlite.rs index b1940db155..84ecabab95 100644 --- a/examples/nostr-sqlite.rs +++ b/examples/nostr-sqlite.rs @@ -7,7 +7,9 @@ use std::time::Duration; //use nostr_sdk_0_32_0::{EventBuilder, EventId, FromBech32, Keys, Kind, Metadata, SecretKey, Tag, Url}; use nostr_0_34_1::prelude::Tag; use nostr_0_34_1::prelude::*; -use nostr_database_0_34_0::{nostr::event::Event, nostr::types::filter::Filter, NostrDatabase, Order}; +use nostr_database_0_34_0::{ + nostr::event::Event, nostr::types::filter::Filter, NostrDatabase, Order, +}; use nostr_sqlite_0_34_0::SQLiteDatabase; use tracing_subscriber::fmt::format::FmtSpan; diff --git a/src/bin/post_event.rs b/examples/post_event.rs similarity index 100% rename from src/bin/post_event.rs rename to examples/post_event.rs diff --git a/src/bin/post_from_files.rs b/examples/post_from_files.rs similarity index 100% rename from src/bin/post_from_files.rs rename to examples/post_from_files.rs diff --git a/src/bin/privkey_to_bech32.rs b/examples/privkey_to_bech32.rs similarity index 100% rename from src/bin/privkey_to_bech32.rs rename to examples/privkey_to_bech32.rs diff --git a/src/bin/pubkey_to_bech32.rs b/examples/pubkey_to_bech32.rs similarity index 100% rename from src/bin/pubkey_to_bech32.rs rename to examples/pubkey_to_bech32.rs diff --git a/examples/rev-list.rs b/examples/rev-list.rs deleted file mode 100644 index 665f775402..0000000000 --- a/examples/rev-list.rs +++ /dev/null @@ -1,106 +0,0 @@ -/* - * libgit2 "rev-list" example - shows how to transform a rev-spec into a list - * of commit ids - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all copyright - * and related and neighboring rights to this software to the public domain - * worldwide. This software is distributed without any warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -//#![deny(warnings)] -#![allow(clippy::manual_strip)] - -use clap::Parser; -use git2::{Error, Oid, Repository, Revwalk}; - -#[derive(Parser)] -struct Args { - #[structopt(name = "topo-order", long)] - /// sort commits in topological order - flag_topo_order: bool, - #[structopt(name = "date-order", long)] - /// sort commits in date order - flag_date_order: bool, - #[structopt(name = "reverse", long)] - /// sort commits in reverse - flag_reverse: bool, - #[structopt(name = "not")] - /// don't show - flag_not: Vec, - #[structopt(name = "spec", last = true)] - arg_spec: Vec, -} - -fn run(args: &Args) -> Result<(), git2::Error> { - let repo = Repository::open(".")?; - let mut revwalk = repo.revwalk()?; - - let base = if args.flag_reverse { - git2::Sort::REVERSE - } else { - git2::Sort::NONE - }; - revwalk.set_sorting( - base | if args.flag_topo_order { - git2::Sort::TOPOLOGICAL - } else if args.flag_date_order { - git2::Sort::TIME - } else { - git2::Sort::NONE - }, - )?; - - let specs = args - .flag_not - .iter() - .map(|s| (s, true)) - .chain(args.arg_spec.iter().map(|s| (s, false))) - .map(|(spec, hide)| { - if spec.starts_with('^') { - (&spec[1..], !hide) - } else { - (&spec[..], hide) - } - }); - for (spec, hide) in specs { - let id = if spec.contains("..") { - let revspec = repo.revparse(spec)?; - if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { - return Err(Error::from_str("merge bases not implemented")); - } - push(&mut revwalk, revspec.from().unwrap().id(), !hide)?; - revspec.to().unwrap().id() - } else { - repo.revparse_single(spec)?.id() - }; - push(&mut revwalk, id, hide)?; - } - - for id in revwalk { - let id = id?; - println!("{}", id); - } - Ok(()) -} - -fn push(revwalk: &mut Revwalk, id: Oid, hide: bool) -> Result<(), Error> { - if hide { - revwalk.hide(id) - } else { - revwalk.push(id) - } -} - -fn main() { - let args = Args::parse(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/test_nip46.rs b/examples/test_nip46.rs similarity index 100% rename from src/bin/test_nip46.rs rename to examples/test_nip46.rs diff --git a/examples/user-base-directories.rs b/examples/user-base-directories.rs new file mode 100644 index 0000000000..9ba5202477 --- /dev/null +++ b/examples/user-base-directories.rs @@ -0,0 +1,38 @@ +use directories::BaseDirs; + +fn main() { + println!("--- Base Directories ---"); + if let Some(base_dirs) = BaseDirs::new() { + // User-specific + if let Some(config_dir) = Some(base_dirs.config_dir()) { + println!("User Config: {}", config_dir.display()); + } + if let Some(data_dir) = Some(base_dirs.data_dir()) { + println!("User Data: {}", data_dir.display()); + } + if let Some(data_local_dir) = Some(base_dirs.data_local_dir()) { + println!("User Data (Local): {}", data_local_dir.display()); + } + if let Some(cache_dir) = Some(base_dirs.cache_dir()) { + println!("User Cache: {}", cache_dir.display()); + } + if let Some(state_dir) = base_dirs.state_dir() { + println!("User State: {}", state_dir.display()); + } + if let Some(preference_dir) = Some(base_dirs.preference_dir()) { + println!("User Preferences: {}", preference_dir.display()); + } + if let Some(runtime_dir) = base_dirs.runtime_dir() { + println!("User Runtime: {}", runtime_dir.display()); + } + if let Some(executable_dir) = base_dirs.executable_dir() { + println!("User Executables: {}", executable_dir.display()); + } + //if let Some(font_dir) = Some(base_dirs.font_dir()) { + // println!("User Fonts: {}", font_dir.display()); + //} + } else { + println!("Could not determine base directories."); + } + println!(); +} diff --git a/examples/user-directories.rs b/examples/user-directories.rs new file mode 100644 index 0000000000..cd7619f6bb --- /dev/null +++ b/examples/user-directories.rs @@ -0,0 +1,40 @@ +use directories::UserDirs; + +fn main() { + println!("--- User Directories ---"); + if let Some(user_dirs) = UserDirs::new() { + if let Some(desktop_dir) = user_dirs.desktop_dir() { + println!("Desktop: {}", desktop_dir.display()); + } + if let Some(document_dir) = user_dirs.document_dir() { + println!("Documents: {}", document_dir.display()); + } + if let Some(download_dir) = user_dirs.download_dir() { + println!("Downloads: {}", download_dir.display()); + } + if let Some(picture_dir) = user_dirs.picture_dir() { + println!("Pictures: {}", picture_dir.display()); + } + if let Some(video_dir) = user_dirs.video_dir() { + println!("Videos: {}", video_dir.display()); + } + if let Some(audio_dir) = user_dirs.audio_dir() { + println!("Audio: {}", audio_dir.display()); + } + //if let Some(home_dir) = user_dirs.home_dir() { + // println!("Home: {}", home_dir.display()); + //} + if let Some(font_dir) = user_dirs.font_dir() { + println!("Fonts: {}", font_dir.display()); + } + if let Some(public_dir) = user_dirs.public_dir() { + println!("Public: {}", public_dir.display()); + } + if let Some(template_dir) = user_dirs.template_dir() { + println!("Templates: {}", template_dir.display()); + } + } else { + println!("Could not determine user directories."); + } + println!(); +} diff --git a/examples/user-project-directories.rs b/examples/user-project-directories.rs new file mode 100644 index 0000000000..80e93398bf --- /dev/null +++ b/examples/user-project-directories.rs @@ -0,0 +1,53 @@ +extern crate directories; +use directories::{BaseDirs, ProjectDirs, UserDirs}; + +fn main() { + println!("--- Project Directories ---"); + + // For a typical application: + // qualifier: usually a reverse domain name (e.g., "com.mycompany") + // organization: Your company or organization name + // application: Your application name + if let Some(proj_dirs) = ProjectDirs::from("org", "gnostr", "gnostr") { + println!("ProjectDirs: {:?}", proj_dirs); + + if let Some(config_dir) = proj_dirs.config_dir().to_str() { + println!("Config Dir: {}", config_dir); + } + if let Some(data_dir) = proj_dirs.data_dir().to_str() { + println!("Data Dir: {}", data_dir); + } + if let Some(data_local_dir) = proj_dirs.data_local_dir().to_str() { + println!("Data Local Dir: {}", data_local_dir); + } + if let Some(cache_dir) = proj_dirs.cache_dir().to_str() { + println!("Cache Dir: {}", cache_dir); + } + //if let Some(state_dir) = proj_dirs.state_dir().to_str() { + // println!("State Dir: {}", state_dir); + //} + //if let Some(log_dir) = proj_dirs.log_dir().to_str() { + // println!("Log Dir: {}", log_dir); + //} + //if let Some(executable_dir) = proj_dirs.executable_dir().to_str() { + // println!("Executable Dir: {}", executable_dir); + //} + } else { + println!("Could not determine project directories."); + } + + println!("\n--- Another Project Example (different qualifier) ---"); + // Example for a project without an organization (e.g., an open-source tool) + // You might use just the application name as the qualifier and organization + //if let Some(proj_dirs) = ProjectDirs::from(None, "MyOpenSourceTool", "MyOpenSourceTool") { + // //println!("Application Name: {}", proj_dirs.application_name()); + // if let Some(config_dir) = proj_dirs.config_dir().to_str() { + // println!("Config Dir: {}", config_dir); + // } + // if let Some(data_dir) = proj_dirs.data_dir().to_str() { + // println!("Data Dir: {}", data_dir); + // } + //} else { + // println!("Could not determine project directories for open source tool."); + //} +} diff --git a/src/bin/verify_event.rs b/examples/verify_event.rs similarity index 100% rename from src/bin/verify_event.rs rename to examples/verify_event.rs diff --git a/src/bin/verify_keypair.rs b/examples/verify_keypair.rs similarity index 100% rename from src/bin/verify_keypair.rs rename to examples/verify_keypair.rs diff --git a/genkeys.sh b/genkeys.sh new file mode 100755 index 0000000000..e0e263cdb8 --- /dev/null +++ b/genkeys.sh @@ -0,0 +1,109 @@ +#!/bin/bash +EMAIL=${1:-gnostr@gnostr.org} + +ssh-keygen -t ed25519 -f gnostr-gnit-key -C "$EMAIL" + +# This script sets recommended secure permissions for the ~/.ssh directory +# and its contents, including authorized_keys, private keys, and public keys. +# These permissions are crucial for SSH security and proper functioning. + +# Define the SSH directory path +SSH_DIR="$HOME/.ssh" +AUTHORIZED_KEYS_FILE="$SSH_DIR/authorized_keys" + +echo "Starting SSH permissions setup..." + +# 1. Check if the ~/.ssh directory exists. If not, create it. +if [ ! -d "$SSH_DIR" ]; then + echo "Directory '$SSH_DIR' does not exist. Creating it..." + mkdir -p "$SSH_DIR" + if [ $? -ne 0 ]; then + echo "Error: Failed to create directory '$SSH_DIR'. Exiting." + exit 1 + fi +else + echo "Directory '$SSH_DIR' already exists." +fi + +# 2. Set permissions for the ~/.ssh directory to 700 (drwx------). +# This means only the owner can read, write, and execute (access) the directory. +echo "Setting permissions for '$SSH_DIR' to 700..." +chmod 700 "$SSH_DIR" +if [ $? -ne 0 ]; then + echo "Error: Failed to set permissions for '$SSH_DIR'. Exiting." + exit 1 +else + echo "Permissions for '$SSH_DIR' set to 700." +fi + +# 3. Set permissions for the authorized_keys file. +if [ -f "$AUTHORIZED_KEYS_FILE" ]; then + echo "File '$AUTHORIZED_KEYS_FILE' found." + # Set permissions for the authorized_keys file to 600 (-rw-------). + # This means only the owner can read and write the file. + echo "Setting permissions for '$AUTHORIZED_KEYS_FILE' to 600..." + chmod 600 "$AUTHORIZED_KEYS_FILE" + if [ $? -ne 0 ]; then + echo "Error: Failed to set permissions for '$AUTHORIZED_KEYS_FILE'. Exiting." + exit 1 + else + echo "Permissions for '$AUTHORIZED_KEYS_FILE' set to 600." + fi +else + echo "File '$AUTHORIZED_KEYS_FILE' not found. No permissions to set for it." + echo "Note: If you plan to use SSH keys for authentication, you will need to" + echo "add your public key to '$AUTHORIZED_KEYS_FILE'." +fi + +# 4. Set permissions for private SSH keys. +# Common private key names (without .pub extension) +PRIVATE_KEY_TYPES=("id_rsa" "id_dsa" "id_ecdsa" "id_ed25519" "gnostr-gnit-key") + +echo "Checking for and setting permissions for private SSH keys..." +for key_type in "${PRIVATE_KEY_TYPES[@]}"; do + PRIVATE_KEY_FILE="$SSH_DIR/$key_type" + if [ -f "$PRIVATE_KEY_FILE" ]; then + echo "Private key '$PRIVATE_KEY_FILE' found." + # Set permissions for private keys to 600 (-rw-------). + # This is critical for security; only the owner should have read/write access. + echo "Setting permissions for '$PRIVATE_KEY_FILE' to 600..." + chmod 600 "$PRIVATE_KEY_FILE" + if [ $? -ne 0 ]; then + echo "Error: Failed to set permissions for '$PRIVATE_KEY_FILE'. Exiting." + exit 1 + else + echo "Permissions for '$PRIVATE_KEY_FILE' set to 600." + fi + else + echo "Private key '$PRIVATE_KEY_FILE' not found." + fi +done + +# 5. Set permissions for public SSH keys. +# Public key files typically end with .pub +PUBLIC_KEY_TYPES=("id_rsa.pub" "id_dsa.pub" "id_ecdsa.pub" "id_ed25519.pub" "gnostr-gnit-key.pub") + +echo "Checking for and setting permissions for public SSH keys..." +for key_type in "${PUBLIC_KEY_TYPES[@]}"; do + PUBLIC_KEY_FILE="$SSH_DIR/$key_type" + if [ -f "$PUBLIC_KEY_FILE" ]; then + echo "Public key '$PUBLIC_KEY_FILE' found." + # Set permissions for public keys to 644 (-rw-r--r--). + # Public keys can be read by others, but only written by the owner. + echo "Setting permissions for '$PUBLIC_KEY_FILE' to 644..." + chmod 644 "$PUBLIC_KEY_FILE" + if [ $? -ne 0 ]; then + echo "Error: Failed to set permissions for '$PUBLIC_KEY_FILE'. Exiting." + exit 1 + else + echo "Permissions for '$PUBLIC_KEY_FILE' set to 644." + fi + else + echo "Public key '$PUBLIC_KEY_FILE' not found." + fi +done + +echo "SSH permissions setup complete." +echo "You can verify permissions with:" +echo "ls -ld ~/.ssh" +echo "ls -l ~/.ssh/" diff --git a/query/.github/workflows/release.yml b/query/.github/workflows/release.yml new file mode 100644 index 0000000000..cdbfc18fa1 --- /dev/null +++ b/query/.github/workflows/release.yml @@ -0,0 +1,337 @@ +# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ +# +# Copyright 2022-2024, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a GitHub Release +# +# Note that the GitHub Release will be created with a generated +# title/body based on your changelogs. + +name: Release +permissions: + "contents": "write" + +# This task will run whenever you push a git tag that looks like a version +# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. +# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where +# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION +# must be a Cargo-style SemVer Version (must have at least major.minor.patch). +# +# If PACKAGE_NAME is specified, then the announcement will be for that +# package (erroring out if it doesn't have the given version or isn't dist-able). +# +# If PACKAGE_NAME isn't specified, then the announcement will be for all +# (dist-able) packages in the workspace with that version (this mode is +# intended for workspaces with only one dist-able package, or with all dist-able +# packages versioned/released in lockstep). +# +# If you push multiple tags at once, separate instances of this workflow will +# spin up, creating an independent announcement for each one. However, GitHub +# will hard limit this to 3 tags per commit, as it will assume more tags is a +# mistake. +# +# If there's a prerelease-style suffix to the version, then the release(s) +# will be marked as a prerelease. +on: + pull_request: + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' + +jobs: + # Run 'dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh" + - name: Cache dist + uses: actions/upload-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/dist + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Rust non-interactively if not already installed + if: ${{ matrix.container }} + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + - name: Install dist + run: ${{ matrix.install_dist.run }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + # Determines if we should publish/announce + host: + needs: + - plan + - build-local-artifacts + - build-global-artifacts + # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.host.outputs.manifest }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Fetch artifacts from scratch-storage + - name: Fetch artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: host + shell: bash + run: | + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + echo "artifacts uploaded and released successfully" + cat dist-manifest.json + echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + # Overwrite the previous copy + name: artifacts-dist-manifest + path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* + + publish-homebrew-formula: + needs: + - plan + - host + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLAN: ${{ needs.plan.outputs.val }} + GITHUB_USER: "axo bot" + GITHUB_EMAIL: "admin+bot@axo.dev" + if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} + steps: + - uses: actions/checkout@v4 + with: + repository: "gnostr-org/homebrew-gnostr-org" + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + # So we have access to the formula + - name: Fetch homebrew formulae + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: Formula/ + merge-multiple: true + # This is extra complex because you can make your Formula name not match your app name + # so we need to find releases with a *.rb file, and publish with that filename. + - name: Commit formula files + run: | + git config --global user.name "${GITHUB_USER}" + git config --global user.email "${GITHUB_EMAIL}" + + for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do + filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) + name=$(echo "$filename" | sed "s/\.rb$//") + version=$(echo "$release" | jq .app_version --raw-output) + + export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" + brew update + # We avoid reformatting user-provided data such as the app description and homepage. + brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true + + git add "Formula/${filename}" + git commit -m "${name} ${version}" + done + git push + + announce: + needs: + - plan + - host + - publish-homebrew-formula + # use "always() && ..." to allow us to wait for all publish jobs while + # still allowing individual publish jobs to skip themselves (for prereleases). + # "host" however must run to completion, no skipping allowed! + if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive diff --git a/query/.gitignore b/query/.gitignore new file mode 100644 index 0000000000..0104787a73 --- /dev/null +++ b/query/.gitignore @@ -0,0 +1,17 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/query/.justfile b/query/.justfile new file mode 100644 index 0000000000..4616260b98 --- /dev/null +++ b/query/.justfile @@ -0,0 +1,63 @@ +default: + just --choose + +help: + @make help + +all: + @make all + +bin: + @make bin + +cargo-help: + @make cargo-help + +cargo-release-all: + @make cargo-release-all + +cargo-clean-release: + @make cargo-clean-release + +cargo-publish-all: + @make cargo-publish-all + +cargo-install-bins: + @make cargo-install-bins + +cargo-build: + @make cargo-build + +cargo-install: + @make cargo-install + +cargo-build-release: + @make cargo-build-release + +cargo-check: + @make cargo-check + +cargo-bench: + @make cargo-bench + +cargo-test: + @make cargo-test + +cargo-test-nightly: + @make cargo-test-nightly + +cargo-report: + @make cargo-report + +cargo-run: + @make cargo-run + +cargo-dist: + @make cargo-dist + +cargo-dist-build: + @make cargo-dist-build + +cargo-dist-manifest: + @make cargo-dist-manifest + diff --git a/query/Cargo.toml b/query/Cargo.toml new file mode 100644 index 0000000000..2bc9c0a28c --- /dev/null +++ b/query/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "gnostr-query" +version = "0.0.9" +authors = ["gnostr-org "] +description = "gnostr-query: retrieve nostr events." +repository = "https://github.com/gnostr-org/gnostr-query.git" +homepage = "https://github.com/gnostr-org/gnostr-query" +documentation = "https://github.com/gnostr-org/gnostr-query" +edition = "2021" +license = "MIT" +categories = ["command-line-utilities"] +keywords = ["terminal", "input", "event", "cli"] + +[lib] +name = "gnostr_query" +path = "src/lib.rs" + +[dependencies] +clap = { version = "4", features = ["derive"] } +futures = "0.3" +log = "0.4.27" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } +tokio-tungstenite = { version = "0.13.0", features = ["tls"] } +url = "2.2" + +# The profile that 'dist' will build with +[profile.dist] +inherits = "release" +lto = "thin" diff --git a/query/LICENSE b/query/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/query/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/query/Makefile b/query/Makefile new file mode 100644 index 0000000000..145032d55d --- /dev/null +++ b/query/Makefile @@ -0,0 +1,82 @@ +help: + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?##/ {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @echo + +## +##=============================================================================== +##all +## bin +all: bin### all +##bin +## cargo b --manifest-path Cargo.toml +bin: ### bin + cargo b --manifest-path Cargo.toml + +## +##=============================================================================== +##make cargo-* +cargo-help: ### cargo-help + @awk 'BEGIN {FS = ":.*?###"} /^[a-zA-Z_-]+:.*?###/ {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) +cargo-release-all: ### cargo-release-all +## cargo-release-all recursively cargo build --release + for t in */Cargo.toml; do echo $$t; cargo b -r -vv --manifest-path $$t; done + for t in ffi/*/Cargo.toml; do echo $$t; cargo b -r -vv --manifest-path $$t; done +cargo-clean-release: ### cargo-clean-release - clean release artifacts +## cargo-clean-release recursively cargo clean --release + for t in *Cargo.toml; do echo $$t && cargo clean --release -vv --manifest-path $$t 2>/dev/null; done +cargo-publish-all: ### cargo-publish-all +## cargo-publish-all recursively publish rust projects + for t in *Cargo.toml; do echo $$t; cargo publish -vv --manifest-path $$t; done + +cargo-install-bins:### cargo-install-bins +## cargo-install-all recursively cargo install -vv $(SUBMODULES) +## *** cargo install -vv --force is NOT used. +## *** FORCE=--force cargo install -vv $(FORCE) is used. +## *** FORCE=--force cargo install -vv $(FORCE) --path +## *** to overwrite deploy cargo.io crates. + export RUSTFLAGS=-Awarning; for t in $(SUBMODULES); do echo $$t; cargo install --bins --path $$t -vv $(FORCE) 2>/dev/null || echo ""; done + #for t in $(SUBMODULES); do echo $$t; cargo install -vv gnostr-$$t --force || echo ""; done + +cargo-build: ## cargo build +## cargo-build q=true + @. $(HOME)/.cargo/env + @RUST_BACKTRACE=all cargo b $(QUIET) +cargo-install: ### cargo install --path . $(FORCE) + @. $(HOME)/.cargo/env + @cargo install --path . $(FORCE) +## cargo-br q=true +cargo-build-release: ### cargo-build-release +## cargo-build-release q=true + @. $(HOME)/.cargo/env + @cargo b --release $(QUIET) +cargo-check: ### cargo-check + @. $(HOME)/.cargo/env + @cargo c +cargo-bench: ### cargo-bench + @. $(HOME)/.cargo/env + @cargo bench +cargo-test: ### cargo-test + @. $(HOME)/.cargo/env + #@cargo test + cargo test +cargo-test-nightly: ### cargo-test-nightly + @. $(HOME)/.cargo/env + #@cargo test + cargo +nightly test +cargo-report: ### cargo-report + @. $(HOME)/.cargo/env + cargo report future-incompatibilities --id 1 +cargo-run: ### cargo-run + @. $(HOME)/.cargo/env + cargo run --bin make-just + +##=============================================================================== +cargo-dist: ### cargo-dist -h + cargo dist -h +cargo-dist-build: ### cargo-dist-build + RUSTFLAGS="--cfg tokio_unstable" cargo dist build +cargo-dist-manifest: ### cargo dist manifest --artifacts=all + cargo dist manifest --artifacts=all + +# vim: set noexpandtab: +# vim: set setfiletype make diff --git a/query/README.md b/query/README.md new file mode 100644 index 0000000000..4422eb53a4 --- /dev/null +++ b/query/README.md @@ -0,0 +1,54 @@ +## gnostr-query + +### construct nostr queries + +```sh +Usage: gnostr-query [OPTIONS] + +Options: + -a, --authors Comma-separated list of authors + -p, --mentions Comma-separated list of mentions + -e, --references Comma-separated list of references + -t, --hashtag Comma-separated list of hashtags + -i, --ids Comma-separated list of ids + -k, --kinds Comma-separated list of kinds (integers) + -g, --generic Generic tag query: #: value + -l, --limit Limit the number of results + -h, --help Print help +``` + +## Install gnostr-query 0.0.4 + +### Install prebuilt binaries via shell script + +```sh +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-installer.sh | sh +``` + +### Install prebuilt binaries via powershell script + +```sh +powershell -ExecutionPolicy Bypass -c "irm https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-installer.ps1 | iex" +``` + +### Install prebuilt binaries via Homebrew + +```sh +brew install gnostr-org/gnostr-org/gnostr-query +``` + +## Download gnostr-query 0.0.4 + +| File | Platform | Checksum | +|--------|----------|----------| +| [gnostr-query-aarch64-apple-darwin.tar.xz](https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-aarch64-apple-darwin.tar.xz) | Apple Silicon macOS | [checksum](https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-aarch64-apple-darwin.tar.xz.sha256) | +| [gnostr-query-x86_64-apple-darwin.tar.xz](https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-x86_64-apple-darwin.tar.xz) | Intel macOS | [checksum](https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-x86_64-apple-darwin.tar.xz.sha256) | +| [gnostr-query-x86_64-pc-windows-msvc.zip](https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-x86_64-pc-windows-msvc.zip) | x64 Windows | [checksum](https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-x86_64-pc-windows-msvc.zip.sha256) | +| [gnostr-query-x86_64-unknown-linux-gnu.tar.xz](https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-x86_64-unknown-linux-gnu.tar.xz) | x64 Linux | [checksum](https://github.com/gnostr-org/gnostr-query/releases/download/v0.0.4/gnostr-query-x86_64-unknown-linux-gnu.tar.xz.sha256) | + + +Usage: + +``` +gnostr-query -i 9f832fda858fdddb86c5c79dcc271767804fb3562ed5eea8c8be4f19be8e9cdc +``` diff --git a/query/default_config.conf b/query/default_config.conf new file mode 100644 index 0000000000..93300e8b44 --- /dev/null +++ b/query/default_config.conf @@ -0,0 +1 @@ +some configs diff --git a/query/dist-workspace.toml b/query/dist-workspace.toml new file mode 100644 index 0000000000..5de5d819df --- /dev/null +++ b/query/dist-workspace.toml @@ -0,0 +1,21 @@ +[workspace] +members = ["cargo:."] + +# Config for 'dist' +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.28.0" +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = ["shell", "powershell", "homebrew"] +# A GitHub repo to push Homebrew formulas to +tap = "gnostr-org/homebrew-gnostr-org" +# Target platforms to build apps for (Rust target-triple syntax) +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] +# Path that installers should place binaries in +install-path = "CARGO_HOME" +# Publish jobs to run in CI +publish-jobs = ["homebrew"] +# Whether to install an updater program +install-updater = true diff --git a/query/examples/define_types.rs b/query/examples/define_types.rs new file mode 100644 index 0000000000..58b19fee02 --- /dev/null +++ b/query/examples/define_types.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Result; + +// Define structs that mirror the JSON structure +#[derive(Debug, Deserialize, Serialize)] +struct Outer { + level1_key: Inner, + other_key: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Inner { + level2_key_a: String, + level2_key_b: i64, + another_nested: DeepInner, +} + +#[derive(Debug, Deserialize, Serialize)] +struct DeepInner { + deep_key: bool, +} + +fn main() -> Result<()> { + let json_data = r#" + { + "level1_key": { + "level2_key_a": "value_a", + "level2_key_b": 123, + "another_nested": { + "deep_key": true + } + }, + "other_key": "some_other_value" + } + "#; + + // Deserialize the JSON string directly into your Rust struct + let parsed_data: Outer = serde_json::from_str(json_data)?; + + // Access the second-level key directly through struct fields + let level2_value_a = &parsed_data.level1_key.level2_key_a; + let level2_value_b = parsed_data.level1_key.level2_key_b; + let deep_value = parsed_data.level1_key.another_nested.deep_key; + + + println!("Value of level2_key_a: {}", level2_value_a); + println!("Value of level2_key_b: {}", level2_value_b); + println!("Value of deep_key: {}", deep_value); + + // If a field is missing or has the wrong type, serde_json::from_str will return an Err + // let invalid_json = r#"{ "level1_key": { "level2_key_a": "oops" } }"#; + // let _ = serde_json::from_str::(invalid_json); // This would return an Error + + Ok(()) +} diff --git a/query/examples/extract_elements.rs b/query/examples/extract_elements.rs new file mode 100644 index 0000000000..82b41355c6 --- /dev/null +++ b/query/examples/extract_elements.rs @@ -0,0 +1,161 @@ +use serde_json::{Result, Value}; +//use serde::de::Error; +use serde::ser::Error; + +fn extract_elements(json_str: &str, keys: &[&str]) -> Result { + let json: Value = serde_json::from_str(json_str)?; + + match json { + Value::Object(map) => { + let mut extracted = serde_json::Map::new(); + for key in keys { + if let Some(value) = map.get(*key) { + extracted.insert(key.to_string(), value.clone()); + } + } + Ok(Value::Object(extracted)) + } + _ => Err(serde_json::Error::custom("Input is not a JSON object")), + } +} + +fn main() { + let json_str = r#" + { + "name": "John Doe", + "age": 30, + "city": "New York", + "is_active": true, + "address": { + "street": "123 Main St", + "zip": "10001" + } + } + "#; + + let keys_to_extract = [ +// "name", +// "age", +// "city", +// "is_active", + "address", +// "street", + "zip" + ]; + + match extract_elements(json_str, &keys_to_extract) { + Ok(extracted_json) => { + println!("1:Extracted JSON: {}", extracted_json); + + match extract_elements(&extracted_json.to_string(), &["address", "zip"]) { + Ok(extracted_json) => { + println!("2:Extracted JSON: {}", extracted_json); + + + + } + Err(err) => { + eprintln!("Error: {}", err); + } + } + + + } + Err(err) => { + eprintln!("Error: {}", err); + } + } + + let json_str_array = r#"[ + {"name": "John Doe", "age": 30}, + {"name": "Jane Smith", "age": 25} + ]"#; + + let keys_array = ["name"]; + + let result_array: Result> = + serde_json::from_str(json_str_array).map(|array: Vec| { + array + .into_iter() + .map(|value| match value { + Value::Object(map) => { + let mut extracted = serde_json::Map::new(); + for key in keys_array { + if let Some(val) = map.get(key) { + extracted.insert(key.to_string(), val.clone()); + } + } + Value::Object(extracted) + } + _ => Value::Null, + }) + .collect() + }); + + match result_array { + Ok(extracted_array) => println!( + "Extracted Array: {}", + serde_json::to_string_pretty(&extracted_array).unwrap() + ), + Err(e) => eprintln!("Error extracting from array: {}", e), + } + + let _ = levels(); +} + +fn levels() -> Result<()> { + let json_data = r#" + { + "level1_key": { + "level2_key_a": "value_a", + "level2_key_b": 123, + "another_nested": { + "deep_key": true + } + }, + "other_key": "some_other_value" + } + "#; + + // Parse the JSON string into a serde_json::Value + let parsed_json: Value = serde_json::from_str(json_data)?; + + // Access the second-level key using square brackets + // This returns a `&Value` which can then be converted to the desired type + let level2_value_a = &parsed_json["level1_key"]["level2_key_a"]; + let level2_value_b = &parsed_json["level1_key"]["level2_key_b"]; + + // You can also chain `.get()` calls, which returns an Option<&Value> + // This is safer if keys might be missing, as it avoids panicking + let deep_value = parsed_json + .get("level1_key") + .and_then(|l1| l1.get("another_nested")) + .and_then(|l2| l2.get("deep_key")); + + println!("Value of level2_key_a: {:?}", level2_value_a); + println!("Value of level2_key_b: {:?}", level2_value_b); + + // To get the actual data, you often need to convert it from `&Value` + if let Some(value_str) = level2_value_a.as_str() { + println!("level2_key_a as string: {}", value_str); + } + + if let Some(value_int) = level2_value_b.as_i64() { + println!("level2_key_b as i64: {}", value_int); + } + + if let Some(value_bool) = deep_value.and_then(|v| v.as_bool()) { + println!("deep_key as bool: {}", value_bool); + } + + // Handling missing keys: + let non_existent_key = &parsed_json["level1_key"]["non_existent"]; + println!("Non-existent key: {:?}", non_existent_key); // Prints Null + + // Using .get() for safer access + let non_existent_key_safe = parsed_json.get("level1_key") + .and_then(|l1| l1.get("non_existent_safe")); + println!("Non-existent key (safe): {:?}", non_existent_key_safe); // Prints None + + Ok(()) +} diff --git a/query/install_script.sh b/query/install_script.sh new file mode 100755 index 0000000000..0090641d60 --- /dev/null +++ b/query/install_script.sh @@ -0,0 +1,146 @@ +#!/bin/bash + +# Install cargo-binstall +curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash && cargo binstall just + +# Example: Install a configuration file. +INSTALL_DIR="$HOME/.my_app" +CONFIG_FILE="my_config.conf" + +mkdir -p "$INSTALL_DIR" +cp "$1" "$INSTALL_DIR/$CONFIG_FILE" # $1 is the first argument passed to the script, likely the config file itself. + +echo "Installed configuration to $INSTALL_DIR/$CONFIG_FILE" + +# Example: add a directory to the PATH. +if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then + echo 'export PATH="$PATH:$INSTALL_DIR"' >> "$HOME/.bashrc" + echo "Added $INSTALL_DIR to PATH. Reload your shell." +fi + +#!/usr/bin/env bash + +# Name of the Makefile to be converted +MAKEFILE="Makefile" +rm $MAKEFILE 2>/dev/null || true +touch $MAKEFILE + +tee -a $MAKEFILE </dev/null; done +cargo-publish-all: ### cargo-publish-all +## cargo-publish-all recursively publish rust projects + for t in *Cargo.toml; do echo $\$t; cargo publish -vv --manifest-path $\$t; done + +cargo-install-bins:### cargo-install-bins +## cargo-install-all recursively cargo install -vv \$(SUBMODULES) +## *** cargo install -vv --force is NOT used. +## *** FORCE=--force cargo install -vv \$(FORCE) is used. +## *** FORCE=--force cargo install -vv \$(FORCE) --path +## *** to overwrite deploy cargo.io crates. + export RUSTFLAGS=-Awarning; for t in \$(SUBMODULES); do echo $\$t; cargo install --bins --path $\$t -vv \$(FORCE) 2>/dev/null || echo ""; done + #for t in \$(SUBMODULES); do echo $\$t; cargo install -vv gnostr-$\$t --force || echo ""; done + +cargo-build: ## cargo build +## cargo-build q=true + @. \$(HOME)/.cargo/env + @RUST_BACKTRACE=all cargo b \$(QUIET) +cargo-install: ### cargo install --path . \$(FORCE) + @. \$(HOME)/.cargo/env + @cargo install --path . \$(FORCE) +## cargo-br q=true +cargo-build-release: ### cargo-build-release +## cargo-build-release q=true + @. \$(HOME)/.cargo/env + @cargo b --release \$(QUIET) +cargo-check: ### cargo-check + @. \$(HOME)/.cargo/env + @cargo c +cargo-bench: ### cargo-bench + @. \$(HOME)/.cargo/env + @cargo bench +cargo-test: ### cargo-test + @. \$(HOME)/.cargo/env + #@cargo test + cargo test +cargo-test-nightly: ### cargo-test-nightly + @. \$(HOME)/.cargo/env + #@cargo test + cargo +nightly test +cargo-report: ### cargo-report + @. \$(HOME)/.cargo/env + cargo report future-incompatibilities --id 1 +cargo-run: ### cargo-run + @. \$(HOME)/.cargo/env + cargo run --bin make-just + +##=============================================================================== +cargo-dist: ### cargo-dist -h + cargo dist -h +cargo-dist-build: ### cargo-dist-build + RUSTFLAGS="--cfg tokio_unstable" cargo dist build +cargo-dist-manifest: ### cargo dist manifest --artifacts=all + cargo dist manifest --artifacts=all + +# vim: set noexpandtab: +# vim: set setfiletype make +EOF + + +# Name of the output Justfile +JUSTFILE=".justfile" +rm $JUSTFILE 2>/dev/null || true +touch $JUSTFILE + +# Check if the Makefile exists +if [ ! -f "$MAKEFILE" ]; then + echo "Makefile not found" + exit 1 +fi + +# Clear the Justfile content +> "$JUSTFILE" + +# Add in the default recipe to Justfile +echo "default:" >> "$JUSTFILE" +echo -e " just --choose\n" >> "$JUSTFILE" + +# Read each line in the Makefile +while IFS= read -r line +do + # Extract target names (lines ending with ':') + if [[ "$line" =~ ^[a-zA-Z0-9_-]+: ]]; then + # Extract the target name + target_name=$(echo "$line" | cut -d':' -f1) + + # Write the corresponding recipe to Justfile + echo "$target_name:" >> "$JUSTFILE" + echo -e " @make $target_name\n" >> "$JUSTFILE" + fi +done < "$MAKEFILE" + +echo "Successfully ported the Makefile to Justfile." diff --git a/query/maintainers.yaml b/query/maintainers.yaml new file mode 100644 index 0000000000..859bad6fff --- /dev/null +++ b/query/maintainers.yaml @@ -0,0 +1,5 @@ +identifier: gnostr-query +maintainers: +- npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d +relays: +- wss://relay.damus.io diff --git a/query/src/cli.rs b/query/src/cli.rs new file mode 100644 index 0000000000..3ee1e3c416 --- /dev/null +++ b/query/src/cli.rs @@ -0,0 +1,68 @@ +use clap::{Arg, ArgMatches, Command}; +pub async fn cli() -> Result> { + let matches = Command::new("gnostr-query") + .about("Construct nostr queries and send them over a websocket") + .arg( + Arg::new("authors") + .short('a') + .long("authors") + .help("Comma-separated list of authors"), + ) + .arg( + Arg::new("mentions") + .short('p') + .long("mentions") + .help("Comma-separated list of mentions"), + ) + .arg( + Arg::new("references") + .short('e') + .long("references") + .help("Comma-separated list of references"), + ) + .arg( + Arg::new("hashtag") + .short('t') + .long("hashtag") + .help("Comma-separated list of hashtags"), + ) + .arg( + Arg::new("ids") + .short('i') + .long("ids") + .help("Comma-separated list of ids"), + ) + .arg( + Arg::new("kinds") + .short('k') + .long("kinds") + .help("Comma-separated list of kinds (integers)"), + ) + .arg( + Arg::new("generic") + .short('g') + .long("generic") + .value_names(["tag", "value"]) + .number_of_values(2) + .help("Generic tag query: #: value"), + ) + .arg( + Arg::new("limit") + .short('l') + .long("limit") + .value_parser(clap::value_parser!(i32)) + .default_value("500") + .help("Limit the number of results"), + ) + .arg( + Arg::new("relay") + .short('r') + .long("relay") + .required(false) + //.help("-r wss://relay.damus.io") + .default_value("wss://relay.damus.io"), + ) + .get_matches(); + + Ok(matches) +} diff --git a/query/src/lib.rs b/query/src/lib.rs new file mode 100644 index 0000000000..41c9640c96 --- /dev/null +++ b/query/src/lib.rs @@ -0,0 +1,225 @@ +use futures::{SinkExt, StreamExt}; +use log::debug; +use serde_json::{json, Map}; +use tokio_tungstenite::{connect_async, tungstenite::Message}; +use url::Url; + +pub mod cli; + +#[derive(Debug)] +pub struct Config { + host: String, + port: u16, + use_tls: bool, + retries: u8, + authors: String, + ids: String, + limit: i32, + generic: (String, String), + hashtag: String, + mentions: String, + references: String, + kinds: String, +} + +#[derive(Debug, Default)] +pub struct ConfigBuilder { + host: Option, + port: Option, + use_tls: bool, + retries: u8, + authors: Option, + ids: Option, + limit: Option, + generic: Option<(String, String)>, + hashtag: Option, + mentions: Option, + references: Option, + kinds: Option, +} +impl ConfigBuilder { + pub fn new() -> Self { + ConfigBuilder { + host: None, + port: None, + use_tls: false, + retries: 0, + authors: None, + ids: None, + limit: None, + generic: None, + hashtag: None, + mentions: None, + references: None, + kinds: None, + } + } + pub fn host(mut self, host: &str) -> Self { + self.host = Some(host.to_string()); + self + } + pub fn port(mut self, port: u16) -> Self { + self.port = Some(port); + self + } + pub fn use_tls(mut self, use_tls: bool) -> Self { + self.use_tls = use_tls; + self + } + pub fn retries(mut self, retries: u8) -> Self { + self.retries = retries; + self + } + pub fn authors(mut self, authors: &str) -> Self { + self.authors = Some(authors.to_string()); + self + } + pub fn ids(mut self, ids: &str) -> Self { + self.ids = Some(ids.to_string()); + self + } + pub fn limit(mut self, limit: i32) -> Self { + self.limit = Some(limit); + self + } + //pub fn generic(mut self, generic: &(&str, &str), tag: &str, val: &str) -> Self { + pub fn generic(mut self, tag: &str, val: &str) -> Self { + //self.generic = Some(("".to_string(), "".to_string())); + self.generic = Some((tag.to_string(), val.to_string())); + self + } + pub fn hashtag(mut self, hashtag: &str) -> Self { + self.hashtag = Some(hashtag.to_string()); + self + } + pub fn mentions(mut self, mentions: &str) -> Self { + self.mentions = Some(mentions.to_string()); + self + } + pub fn references(mut self, references: &str) -> Self { + self.references = Some(references.to_string()); + self + } + pub fn kinds(mut self, kinds: &str) -> Self { + self.kinds = Some(kinds.to_string()); + self + } + pub fn build(self) -> Result { + Ok(Config { + host: self.host.ok_or("Missing host")?, + port: self.port.ok_or("Missing port")?, + use_tls: self.use_tls, + retries: self.retries, + authors: self.authors.ok_or("")?, + ids: self.ids.ok_or("")?, + limit: self.limit.ok_or("")?, + generic: self.generic.ok_or("")?, + hashtag: self.hashtag.ok_or("")?, + mentions: self.mentions.ok_or("")?, + references: self.references.ok_or("")?, + kinds: self.kinds.ok_or("")?, + }) + } +} +pub async fn send( + query_string: String, + relay_url: Url, + limit: Option, +) -> Result, Box> { + //println!("\n{}\n", query_string); + //println!("\n{}\n", relay_url); + //println!("\n{}\n", limit.unwrap()); + debug!("\n{query_string}\n"); + debug!("\n{relay_url}\n"); + debug!("\n{}\n", limit.unwrap()); + let (ws_stream, _) = connect_async(relay_url).await?; + let (mut write, mut read) = ws_stream.split(); + write.send(Message::Text(query_string)).await?; + let mut count: i32 = 0; + let mut vec_result: Vec = vec![]; + while let Some(message) = read.next().await { + let data = message?; + if count >= limit.unwrap() { + //std::process::exit(0); + return Ok(vec_result); + } + if let Message::Text(text) = data { + //print!("{text}"); + vec_result.push(text); + count += 1; + } + } + Ok(vec_result) +} + +pub fn build_gnostr_query( + authors: Option<&str>, + ids: Option<&str>, + limit: Option, + generic: Option<(&str, &str)>, + hashtag: Option<&str>, + mentions: Option<&str>, + references: Option<&str>, + kinds: Option<&str>, +) -> Result> { + let mut filt = Map::new(); + + if let Some(authors) = authors { + filt.insert( + "authors".to_string(), + json!(authors.split(',').collect::>()), + ); + } + + if let Some(ids) = ids { + filt.insert( + "ids".to_string(), + json!(ids.split(',').collect::>()), + ); + } + + if let Some(limit) = limit { + filt.insert("limit".to_string(), json!(limit)); + } + + if let Some((tag, val)) = generic { + let tag_with_hash = format!("#{tag}"); + filt.insert(tag_with_hash, json!(val.split(',').collect::>())); + } + + if let Some(hashtag) = hashtag { + filt.insert( + "#t".to_string(), + json!(hashtag.split(',').collect::>()), + ); + } + + if let Some(mentions) = mentions { + filt.insert( + "#p".to_string(), + json!(mentions.split(',').collect::>()), + ); + } + + if let Some(references) = references { + filt.insert( + "#e".to_string(), + json!(references.split(',').collect::>()), + ); + } + + if let Some(kinds) = kinds { + let kind_ints: Result, _> = kinds.split(',').map(|s| s.parse::()).collect(); + match kind_ints { + Ok(kind_ints) => { + filt.insert("kinds".to_string(), json!(kind_ints)); + } + Err(_) => { + return Err("Error parsing kinds. Ensure they are integers.".into()); + } + } + } + + let q = json!(["REQ", "gnostr-query", filt]); + Ok(serde_json::to_string(&q)?) +} diff --git a/query/src/main.rs b/query/src/main.rs new file mode 100644 index 0000000000..6cf8860b8f --- /dev/null +++ b/query/src/main.rs @@ -0,0 +1,113 @@ +use gnostr_query::cli::cli; +use gnostr_query::ConfigBuilder; +use log::{debug, trace}; +use serde_json::{json, to_string}; +use url::Url; + +/// Usage +/// nip-0034 kinds +/// gnostr-query -k 1630,1632,1621,30618,1633,1631,1617,30617 +#[tokio::main] +async fn main() -> Result<(), Box> { + let matches = cli().await?; + let mut filt = serde_json::Map::new(); + + if let Some(authors) = matches.get_one::("authors") { + filt.insert( + "authors".to_string(), + json!(authors.split(',').collect::>()), + ); + } + + if let Some(ids) = matches.get_one::("ids") { + filt.insert( + "ids".to_string(), + json!(ids.split(',').collect::>()), + ); + } + + let mut limit_check: i32 = 0; + if let Some(limit) = matches.get_one::("limit") { + // ["EOSE","gnostr-query"] counts as a message! + 1 + filt.insert("limit".to_string(), json!(limit.clone() /*+ 1*/)); + limit_check = *limit; + } + + if let Some(generic) = matches.get_many::("generic") { + let generic_vec: Vec<&String> = generic.collect(); + if generic_vec.len() == 2 { + let tag = format!("#{}", generic_vec[0]); + let val = generic_vec[1].split(',').collect::>(); + filt.insert(tag, json!(val)); + } + } + + if let Some(hashtag) = matches.get_one::("hashtag") { + filt.insert( + "#t".to_string(), + json!(hashtag.split(',').collect::>()), + ); + } + + if let Some(mentions) = matches.get_one::("mentions") { + filt.insert( + "#p".to_string(), + json!(mentions.split(',').collect::>()), + ); + } + + if let Some(references) = matches.get_one::("references") { + filt.insert( + "#e".to_string(), + json!(references.split(',').collect::>()), + ); + } + + if let Some(kinds) = matches.get_one::("kinds") { + if let Ok(kind_ints) = kinds + .split(',') + .map(|s| s.parse::()) + .collect::, _>>() + { + filt.insert("kinds".to_string(), json!(kind_ints)); + } else { + eprintln!("Error parsing kinds. Ensure they are integers."); + std::process::exit(1); + } + } + + let config = ConfigBuilder::new() + .host("localhost") + .port(8080) + .use_tls(true) + .retries(5) + .authors("") + .ids("") + .limit(limit_check) + .generic("", "") + .hashtag("") + .mentions("") + .references("") + .kinds("") + .build()?; + + debug!("{config:?}"); + let q = json!(["REQ", "gnostr-query", filt]); + let query_string = to_string(&q)?; + let relay_url_str = matches.get_one::("relay").clone().unwrap(); + let relay_url = Url::parse(relay_url_str)?; + let vec_result = gnostr_query::send(query_string.clone(), relay_url, Some(limit_check)).await; + //trace + trace!("{:?}", vec_result); + + let mut json_result: Vec = vec![]; + for element in vec_result.unwrap() { + println!("{}", element); + json_result.push(element); + } + //trace + for element in json_result { + trace!("json_result={}", element); + } + Ok(()) +} diff --git a/repo.toml b/repo.toml index 5a6219058d..e86ecf2c79 100644 --- a/repo.toml +++ b/repo.toml @@ -1,4 +1,4 @@ name = "gnostr-gnit-server" public = true -members = ["gnostr"] +members = ["gnostr", "gnostr-user"] failed_push_message = "Issues and patches can be emailed to admin@gnostr.org" diff --git a/server.toml b/server.toml index 07daeb3a3c..4ca4ed4641 100644 --- a/server.toml +++ b/server.toml @@ -1,16 +1,20 @@ name = "gnostr.org" port = 2222 - hostname = "gnostr.org" -[users.gnostr] -is_admin = true +[users.gnostr-user] +is_admin = false can_create_repos = true -public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDaBogLsfsOkKIpZEZYa3Ee+wFaaxeJuHps05sH2rZLf+KEE6pWX5MT2iWMgP7ihmm6OqbAPkWoBUGEO5m+m/K1S0MgQXUvaTsTI0II3MDqJT/RXA6Z9c+ZIDROEAkNIDrfeU2n8hQXfMHwG6aJjwv3Zky9jR/ey2rSgKLMcTOLrMeAyop6fYhjIHqp0dTagHo1j+XHAbVsrjw6oxC0ohTkp8rzH6cYJyjK4TOKApEgCALJUOA2rbHNxr68wAIe2RS36dRQobD3ops2+HoOGk7pkBQazBAlZp/H4monWRrq7tTEw8FkGMX5udZQX6BNEI0vJZqtdkSpG7jSS3aL7GXcuOYKpsTKxuGm5BWsrRPiphsc25U02oe/y3+qM0ceP/njJp3ZvXQ/a2QGPU4+P8WSD+J0oKS+TiRKrpiTR4ChJk8zWupg4PI5zflN3yyK7MrGXg1n0DsvHxPXcqpvVRz4i8ORt6IlKGkve1tC0Wd9pVy4044LDethMORRZFjWAdS/caN1EMgTrrGMxi0DLVw6ahedGUgZj2WYWfsrEg8Kzbfk3fn32sO/lMnNyz5hmavMBiNORGlIi2Qe2RjQEtcJHn89B7UtyEfnj87V+jZYcFf4nnNQigT2eQ3NlB1YzZS4Zk/OxQeYypclzYFaiYc7RZv2yxKVOy0KvEpldyUKeQ== randy.lee.mcmillan@gmail.com" +public_key = """ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGextjLHvRa/hfsEAVg/hQI26XG6l5Y4FRifeL9/0ADj gnostr@gnostr.org +""" -[users.gnostr-user] +[users.gnostr] +is_admin = true can_create_repos = true -public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDaBogLsfsOkKIpZEZYa3Ee+wFaaxeJuHps05sH2rZLf+KEE6pWX5MT2iWMgP7ihmm6OqbAPkWoBUGEO5m+m/K1S0MgQXUvaTsTI0II3MDqJT/RXA6Z9c+ZIDROEAkNIDrfeU2n8hQXfMHwG6aJjwv3Zky9jR/ey2rSgKLMcTOLrMeAyop6fYhjIHqp0dTagHo1j+XHAbVsrjw6oxC0ohTkp8rzH6cYJyjK4TOKApEgCALJUOA2rbHNxr68wAIe2RS36dRQobD3ops2+HoOGk7pkBQazBAlZp/H4monWRrq7tTEw8FkGMX5udZQX6BNEI0vJZqtdkSpG7jSS3aL7GXcuOYKpsTKxuGm5BWsrRPiphsc25U02oe/y3+qM0ceP/njJp3ZvXQ/a2QGPU4+P8WSD+J0oKS+TiRKrpiTR4ChJk8zWupg4PI5zflN3yyK7MrGXg1n0DsvHxPXcqpvVRz4i8ORt6IlKGkve1tC0Wd9pVy4044LDethMORRZFjWAdS/caN1EMgTrrGMxi0DLVw6ahedGUgZj2WYWfsrEg8Kzbfk3fn32sO/lMnNyz5hmavMBiNORGlIi2Qe2RjQEtcJHn89B7UtyEfnj87V+jZYcFf4nnNQigT2eQ3NlB1YzZS4Zk/OxQeYypclzYFaiYc7RZv2yxKVOy0KvEpldyUKeQ== randy.lee.mcmillan@gmail.com" +public_key = """ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGextjLHvRa/hfsEAVg/hQI26XG6l5Y4FRifeL9/0ADj gnostr@gnostr.org +""" [welcome_message] welcome_message = "welcome to gnostr.org!" diff --git a/src/bin/create_event.rs b/src/bin/create_event.rs deleted file mode 100644 index 74c0e14ed7..0000000000 --- a/src/bin/create_event.rs +++ /dev/null @@ -1,24 +0,0 @@ -use gnostr_types::{PreEvent, Signer, Unixtime}; -use std::env; -use std::io::Read; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let mut args = env::args(); - let _ = args.next(); // program name - - let mut s: String = String::new(); - std::io::stdin().read_to_string(&mut s)?; - let mut pre_event: PreEvent = serde_json::from_str(&s)?; - - // Update creation stamp - pre_event.created_at = Unixtime::now(); - - let signer = gnostr::load_signer()?; - - let event = signer.sign_event(pre_event)?; - - println!("{}", serde_json::to_string(&event)?); - - Ok(()) -} diff --git a/src/bin/create_event_raw.rs b/src/bin/create_event_raw.rs deleted file mode 100644 index 6a5373055c..0000000000 --- a/src/bin/create_event_raw.rs +++ /dev/null @@ -1,57 +0,0 @@ -use gnostr_types::{Event, Id, Signer, Tag}; -use secp256k1::hashes::Hash; -use serde_json::Value; -use std::env; -use std::io::Read; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let mut args = env::args(); - let _ = args.next(); // program name - - let mut s: String = String::new(); - std::io::stdin().read_to_string(&mut s)?; - println!("INPUT: {}", s); - - let value: Value = serde_json::from_str(&s)?; - let obj = value.as_object().unwrap(); - let created_at = format!("{}", obj.get("created_at").unwrap()); - let kind = format!("{}", obj.get("kind").unwrap()); - let tags: Vec = serde_json::from_value(obj.get("tags").unwrap().clone())?; - let content = obj.get("content").unwrap().as_str().unwrap().to_owned(); - let signer = gnostr::load_signer()?; - - // Event pubkey must match our signer - - let serial_for_sig = format!( - "[0,\"{}\",{},{},{},\"{}\"]", - signer.public_key().as_hex_string(), - created_at, - kind, - serde_json::to_string(&tags)?, - &content, - ); - println!("SIGN: {}", serial_for_sig); - let hash = secp256k1::hashes::sha256::Hash::hash(serial_for_sig.as_bytes()); - let id: [u8; 32] = hash.to_byte_array(); - let id = Id(id); - let signature = signer.sign_id(id)?; - - let output = format!( - r##"{{"id":"{}","pubkey":"{}","created_at":{},"kind":{},"tags":{},"content":"{}","sig":"{}"}}"##, - id.as_hex_string(), - signer.public_key().as_hex_string(), - created_at, - kind, - serde_json::to_string(&tags)?, - content, - signature.as_hex_string(), - ); - println!("EVENT: {}", output); - - let event: Event = serde_json::from_str(&output)?; - event.verify(None)?; - - println!("Event verified."); - Ok(()) -} diff --git a/src/bin/generate-server-config.rs b/src/bin/generate-server-config.rs new file mode 100644 index 0000000000..a7ea422bb2 --- /dev/null +++ b/src/bin/generate-server-config.rs @@ -0,0 +1,415 @@ +use gnostr::{blockheight::blockheight_sync, weeble::weeble_sync, wobble::wobble_sync}; +use log::debug; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::env; +use std::fs; +use std::io; +#[cfg(not(windows))] +use std::os::unix::fs::PermissionsExt; // Required for chmod (Unix-specific) + +use std::path::{Path, PathBuf}; +use std::process::{exit, Command}; + +// --- Structs for TOML configuration --- +#[derive(Serialize, Deserialize, Debug)] +struct Config { + name: String, + port: u16, + hostname: String, + users: HashMap, + welcome_message: WelcomeMessage, + extra: Extra, +} + +#[derive(Serialize, Deserialize, Debug)] +struct User { + #[serde(default)] // Use default value (false) if not specified in TOML + is_admin: bool, + can_create_repos: bool, + public_key: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct WelcomeMessage { + welcome_message: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Extra { + extra: String, +} + +fn move_gnostr_gnit_key() -> io::Result<()> { + let home_dir = env::var("HOME").expect("HOME environment variable not set"); + let ssh_dir = PathBuf::from(&home_dir).join(".ssh"); + debug!("{}", ssh_dir.display()); + let gnostr_gnit_key_path = PathBuf::from(&home_dir) + .join(".ssh") + .join("gnostr-gnit-key"); + let gnostr_gnit_key_path_weeble_blockheight_wobble = + PathBuf::from(&home_dir).join(".ssh").join(format!( + "gnostr-gnit-key-{}-{}-{}", + &weeble_sync().unwrap().to_string(), + blockheight_sync(), + &wobble_sync().unwrap().to_string() + )); + + println!( + "Attempting to rename/move '{}' to '{}'", + gnostr_gnit_key_path.display(), + gnostr_gnit_key_path_weeble_blockheight_wobble.display() + ); + + // Rename the file + match fs::rename( + gnostr_gnit_key_path, + gnostr_gnit_key_path_weeble_blockheight_wobble, + ) { + Ok(_) => { + println!("File renamed successfully!"); + } + Err(e) => { + eprintln!("Error renaming file: {}", e); + } + } + + // Clean up (optional) + // fs::remove_file(to_path)?; + // println!("Cleaned up '{}'", to_path.display()); + + Ok(()) +} + +// --- Helper function for setting file permissions --- +fn set_permissions(path: &Path, mode: u32) -> std::io::Result<()> { + // Detect specific operating systems + #[cfg(target_os = "macos")] + { + println!("Running on macOS!"); + // macOS-specific code here + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(mode); + fs::set_permissions(path, perms) + } + + #[cfg(target_os = "linux")] + { + println!("Running on Linux!"); + // Linux-specific code here + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(mode); + fs::set_permissions(path, perms) + } + + #[cfg(target_os = "windows")] + { + println!("Running on Windows!"); + // Windows-specific code here + return Ok(()); + } +} + +fn main() -> io::Result<()> { + // --- SSH Key Generation and Permissions Setup --- + let email = env::args() + .nth(1) + .unwrap_or_else(|| "gnostr@gnostr.org".to_string()); + + let home_dir = env::var("HOME").expect("HOME environment variable not set"); + let ssh_dir = PathBuf::from(&home_dir).join(".ssh"); + let authorized_keys_file = ssh_dir.join("authorized_keys"); + + println!("Starting SSH permissions setup..."); + + // 1. Check if the ~/.ssh directory exists. If not, create it. + if !ssh_dir.exists() { + println!( + "Directory '{}' does not exist. Creating it...", + ssh_dir.display() + ); + if let Err(e) = fs::create_dir_all(&ssh_dir) { + eprintln!( + "Error: Failed to create directory '{}'. {}", + ssh_dir.display(), + e + ); + exit(1); + } + } else { + println!("Directory '{}' already exists.", ssh_dir.display()); + } + + let private_key_types = vec![ + //"id_rsa", + //"id_dsa", + //"id_ecdsa", + //"id_ed25519", + "gnostr-gnit-key", + ]; + + println!("Checking for and setting permissions for private SSH keys..."); + for key_type in private_key_types { + let private_key_file = ssh_dir.join(key_type); + if private_key_file.exists() { + let _ = move_gnostr_gnit_key(); + } + } + + // Call ssh-keygen + println!("Generating SSH key pair (gnostr-gnit-key)..."); + let key_file_name = "gnostr-gnit-key"; + let output = Command::new("ssh-keygen") + .arg("-t") + .arg("ed25519") + .arg("-f") + .arg(ssh_dir.join(key_file_name)) + .arg("-C") + .arg(&email) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + println!("SSH key generation successful."); + } else { + eprintln!("Error: ssh-keygen failed."); + eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout)); + eprintln!("Stderr: {}", String::from_utf8_lossy(&output.stderr)); + exit(1); + } + } + Err(e) => { + eprintln!( + "Error: Could not execute ssh-keygen. Is it installed and in your PATH? {}", + e + ); + exit(1); + } + } + + // 2. Set permissions for the ~/.ssh directory to 700 (drwx------). + println!("Setting permissions for '{}' to 700...", ssh_dir.display()); + if let Err(e) = set_permissions(&ssh_dir, 0o700) { + eprintln!( + "Error: Failed to set permissions for '{}'. {}", + ssh_dir.display(), + e + ); + exit(1); + } else { + println!("Permissions for '{}' set to 700.", ssh_dir.display()); + } + + // 3. Set permissions for the authorized_keys file. + if authorized_keys_file.exists() { + println!("File '{}' found.", authorized_keys_file.display()); + println!( + "Setting permissions for '{}' to 600...", + authorized_keys_file.display() + ); + if let Err(e) = set_permissions(&authorized_keys_file, 0o600) { + eprintln!( + "Error: Failed to set permissions for '{}'. {}", + authorized_keys_file.display(), + e + ); + exit(1); + } else { + println!( + "Permissions for '{}' set to 600.", + authorized_keys_file.display() + ); + } + } else { + println!( + "File '{}' not found. No permissions to set for it.", + authorized_keys_file.display() + ); + println!("Note: If you plan to use SSH keys for authentication, you will need to"); + println!( + "add your public key to '{}'.", + authorized_keys_file.display() + ); + } + + // 4. Set permissions for private SSH keys. + let private_key_types = vec![ + //"id_rsa", + //"id_dsa", + //"id_ecdsa", + //"id_ed25519", + "gnostr-gnit-key", + ]; + + println!("Checking for and setting permissions for private SSH keys..."); + for key_type in private_key_types { + let private_key_file = ssh_dir.join(key_type); + if private_key_file.exists() { + println!("Private key '{}' found.", private_key_file.display()); + println!( + "Setting permissions for '{}' to 600...", + private_key_file.display() + ); + if let Err(e) = set_permissions(&private_key_file, 0o600) { + eprintln!( + "Error: Failed to set permissions for '{}'. {}", + private_key_file.display(), + e + ); + exit(1); + } else { + println!( + "Permissions for '{}' set to 600.", + private_key_file.display() + ); + + println!("Generating SSH key add (gnostr-gnit-key)..."); + let key_file_name = "gnostr-gnit-key"; + let output = Command::new("ssh-add") + .arg(ssh_dir.join(key_file_name)) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + println!("SSH key-add successful."); + } else { + eprintln!("Error: ssh-add failed."); + eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout)); + eprintln!("Stderr: {}", String::from_utf8_lossy(&output.stderr)); + exit(1); + } + } + Err(e) => { + eprintln!( + "Error: Could not execute ssh-add. Is it installed and in your PATH? {}", + e + ); + exit(1); + } + } + } + } else { + println!("Private key '{}' not found.", private_key_file.display()); + } + } + + // 5. Set permissions for public SSH keys. + let public_key_types = vec![ + //"id_rsa.pub", + //"id_dsa.pub", + //"id_ecdsa.pub", + //"id_ed25519.pub", + "gnostr-gnit-key.pub", + ]; + + let mut gnostr_gnit_pubkey: String = "".to_string(); + println!("Checking for and setting permissions for public SSH keys..."); + for key_type in public_key_types { + let public_key_file = ssh_dir.join(key_type); + if public_key_file.exists() { + let pubkey_file_name = "gnostr-gnit-key.pub"; + let output = Command::new("cat") + .arg(ssh_dir.join(pubkey_file_name)) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + let capture_output = &output.clone().stdout; + gnostr_gnit_pubkey = (&String::from_utf8_lossy(&capture_output)) + .to_string() + .replace("\"\"", ""); + println!("gnostr-gnit-key.pub:\n{:?}", output.stdout); + } else { + eprintln!("Error: gnostr-gnit-pubkey not found."); + eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout)); + eprintln!("Stderr: {}", String::from_utf8_lossy(&output.stderr)); + exit(1); + } + } + Err(e) => { + eprintln!( + "Error: Could not execute ssh-keygen. Is it installed and in your PATH? {}", + e + ); + exit(1); + } + } + + println!("Public key '{}' found.", public_key_file.display()); + println!( + "Setting permissions for '{}' to 644...", + public_key_file.display() + ); + if let Err(e) = set_permissions(&public_key_file, 0o644) { + eprintln!( + "Error: Failed to set permissions for '{}'. {}", + public_key_file.display(), + e + ); + exit(1); + } else { + println!( + "Permissions for '{}' set to 644.", + public_key_file.display() + ); + } + } else { + println!("Public key '{}' not found.", public_key_file.display()); + } + } + + println!("SSH permissions setup complete."); + println!("You can verify permissions with:"); + println!("ls -ld ~/.ssh"); + println!("ls -l ~/.ssh/"); + + // --- TOML Configuration Generation --- + println!("\nStarting TOML configuration generation..."); + + let mut users = HashMap::new(); + + println!("{}", gnostr_gnit_pubkey.clone().to_string()); + users.insert( + "gnostr".to_string(), + User { + is_admin: true, + can_create_repos: true, + public_key: gnostr_gnit_pubkey.clone().to_string().replace("\"\"", ""), + }, + ); + + users.insert( + "gnostr-user".to_string(), + User { + is_admin: false, // Explicitly set to false as it's not an admin + can_create_repos: true, + public_key: gnostr_gnit_pubkey.clone().to_string().replace("\"", ""), + }, + ); + + let config = Config { + name: "gnostr.org".to_string(), + port: 2222, + hostname: "gnostr.org".to_string(), + users, + welcome_message: WelcomeMessage { + welcome_message: "welcome to gnostr.org!".to_string(), + }, + extra: Extra { + extra: "extra toml content!".to_string(), + }, + }; + + let toml_string = toml::to_string(&config).expect("Failed to serialize config to TOML"); + + println!("Generated server.toml content:\n{}", toml_string); + + fs::write("server.toml", toml_string)?; + + println!("server.toml generated successfully!"); + + Ok(()) +} diff --git a/src/bin/git-ssh.rs b/src/bin/git-ssh.rs index 2738bffcf1..ecd4a277c3 100644 --- a/src/bin/git-ssh.rs +++ b/src/bin/git-ssh.rs @@ -1,10 +1,10 @@ use env_logger::Env; use gnostr::ssh::start; -use log::error; #[tokio::main] async fn main() { env_logger::init_from_env(Env::default().default_filter_or("info")); if let Err(e) = start().await { + println!("{}", e); println!("EXAMPLE:server.toml\n{}", SERVER_TOML); println!("check the port in your server.toml is available!"); println!("check the port in your server.toml is available!"); diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 77db75263e..87bedbd18a 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -1011,7 +1011,9 @@ async fn create_merge_status( "{merge_commit}" ))), Tag::custom( - nostr_0_34_1::TagKind::Custom(std::borrow::Cow::Borrowed("merge-commit-id")), + nostr_0_34_1::TagKind::Custom(std::borrow::Cow::Borrowed( + "merge-commit-id", + )), vec![format!("{merge_commit}")], ), ], diff --git a/src/bin/gnostr-add.rs b/src/bin/gnostr-add.rs deleted file mode 100755 index ab6e7a64b6..0000000000 --- a/src/bin/gnostr-add.rs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * libgit2 "add" example - shows how to modify the index - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all - * copyright and related and neighboring rights to this software to the - * public domain worldwide. This software is distributed without any - * warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -#![deny(warnings)] -#![allow(trivial_casts)] - -use std::path::Path; - -use git2::Repository; -use structopt::StructOpt; - -#[derive(StructOpt)] -struct Args { - #[structopt(name = "spec")] - arg_spec: Vec, - #[structopt(name = "dry_run", short = "n", long)] - /// dry run - flag_dry_run: bool, - #[structopt(name = "verbose", short, long)] - /// be verbose - flag_verbose: bool, - #[structopt(name = "update", short, long)] - /// update tracked files - flag_update: bool, -} - -fn run(args: &Args) -> Result<(), git2::Error> { - let repo: Repository; - repo = match Repository::discover(".") { - Ok(repo) => repo, - Err(e) => panic!("failed to init: {}", e), - }; - - let mut index = repo.index()?; - - let cb = &mut |path: &Path, _matched_spec: &[u8]| -> i32 { - let status = repo.status_file(path).unwrap(); - - let ret = if status.contains(git2::Status::WT_MODIFIED) - || status.contains(git2::Status::WT_NEW) - { - println!("add '{}'", path.display()); - 0 - } else { - 1 - }; - - if args.flag_dry_run { - 1 - } else { - ret - } - }; - let cb = if args.flag_verbose || args.flag_update { - Some(cb as &mut git2::IndexMatchedPath) - } else { - None - }; - - if args.flag_update { - index.update_all(args.arg_spec.iter(), cb)?; - } else { - index.add_all(args.arg_spec.iter(), git2::IndexAddOption::DEFAULT, cb)?; - } - - index.write()?; - Ok(()) -} - -fn main() { - let args = Args::from_args(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/gnostr-cat-file.rs b/src/bin/gnostr-cat-file.rs deleted file mode 100755 index 0f59600555..0000000000 --- a/src/bin/gnostr-cat-file.rs +++ /dev/null @@ -1,150 +0,0 @@ -/* - * libgit2 "cat-file" example - shows how to print data from the ODB - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all - * copyright and related and neighboring rights to this software to the - * public domain worldwide. This software is distributed without any - * warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -#![deny(warnings)] - -use std::io::{self, Write}; - -use git2::{Blob, Commit, ObjectType, Repository, Signature, Tag, Tree}; -use structopt::StructOpt; - -#[derive(StructOpt)] -struct Args { - #[structopt(name = "object")] - arg_object: String, - #[structopt(short = "t")] - /// show the object type - flag_t: bool, - #[structopt(short = "s")] - /// show the object size - flag_s: bool, - #[structopt(short = "e")] - /// suppress all output - flag_e: bool, - #[structopt(short = "p")] - /// pretty print the contents of the object - flag_p: bool, - #[structopt(name = "quiet", short, long)] - /// suppress output - flag_q: bool, - #[structopt(name = "verbose", short, long)] - flag_v: bool, - #[structopt(name = "dir", long = "git-dir")] - /// use the specified directory as the base directory - flag_git_dir: Option, -} - -fn run(args: &Args) -> Result<(), git2::Error> { - let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); - let repo = Repository::open(path)?; - - let obj = repo.revparse_single(&args.arg_object)?; - if args.flag_v && !args.flag_q { - println!("{} {}\n--", obj.kind().unwrap().str(), obj.id()); - } - - if args.flag_t { - println!("{}", obj.kind().unwrap().str()); - } else if args.flag_s || args.flag_e { - /* ... */ - } else if args.flag_p { - match obj.kind() { - Some(ObjectType::Blob) => { - show_blob(obj.as_blob().unwrap()); - } - Some(ObjectType::Commit) => { - show_commit(obj.as_commit().unwrap()); - } - Some(ObjectType::Tag) => { - show_tag(obj.as_tag().unwrap()); - } - Some(ObjectType::Tree) => { - show_tree(obj.as_tree().unwrap()); - } - Some(ObjectType::Any) | None => println!("unknown {}", obj.id()), - } - } - Ok(()) -} - -fn show_blob(blob: &Blob) { - io::stdout().write_all(blob.content()).unwrap(); -} - -fn show_commit(commit: &Commit) { - println!("tree {}", commit.tree_id()); - for parent in commit.parent_ids() { - println!("parent {}", parent); - } - show_sig("author", Some(commit.author())); - show_sig("committer", Some(commit.committer())); - if let Some(msg) = commit.message() { - println!("\n{}", msg); - } -} - -fn show_tag(tag: &Tag) { - println!("object {}", tag.target_id()); - println!("type {}", tag.target_type().unwrap().str()); - println!("tag {}", tag.name().unwrap()); - show_sig("tagger", tag.tagger()); - - if let Some(msg) = tag.message() { - println!("\n{}", msg); - } -} - -fn show_tree(tree: &Tree) { - for entry in tree.iter() { - println!( - "{:06o} {} {}\t{}", - entry.filemode(), - entry.kind().unwrap().str(), - entry.id(), - entry.name().unwrap() - ); - } -} - -fn show_sig(header: &str, sig: Option) { - let sig = match sig { - Some(s) => s, - None => return, - }; - let offset = sig.when().offset_minutes(); - let (sign, offset) = if offset < 0 { - ('-', -offset) - } else { - ('+', offset) - }; - let (hours, minutes) = (offset / 60, offset % 60); - println!( - "{} {} {} {}{:02}{:02}", - header, - sig, - sig.when().seconds(), - sign, - hours, - minutes - ); -} - -fn main() { - let args = Args::from_args(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/gnostr-cli-example.rs b/src/bin/gnostr-cli-example.rs deleted file mode 100755 index 9dd6dd8638..0000000000 --- a/src/bin/gnostr-cli-example.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::io::Read; - -use reqwest::Url; -fn main() { - let url = Url::parse("https://raw.githubusercontent.com/gnostr-org/gnostr-bins/master/src/bin/gnostr-cli-example.rs").unwrap(); - let mut res = reqwest::blocking::get(url).unwrap(); - - let mut body = String::new(); - res.read_to_string(&mut body).unwrap(); - - println!("{}", body); -} diff --git a/src/bin/gnostr-clone.rs b/src/bin/gnostr-clone.rs deleted file mode 100755 index aebfad4248..0000000000 --- a/src/bin/gnostr-clone.rs +++ /dev/null @@ -1,128 +0,0 @@ -/* - * libgit2 "clone" example - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all - * copyright and related and neighboring rights to this software to the - * public domain worldwide. This software is distributed without any - * warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -#![deny(warnings)] - -use std::cell::RefCell; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; - -use git2::build::{CheckoutBuilder, RepoBuilder}; -use git2::{FetchOptions, Progress, RemoteCallbacks}; -use structopt::StructOpt; - -#[derive(StructOpt)] -struct Args { - #[structopt(name = "url")] - arg_url: String, - #[structopt(name = "path")] - arg_path: String, -} - -struct State { - progress: Option>, - total: usize, - current: usize, - path: Option, - newline: bool, -} - -fn print(state: &mut State) { - let stats = state.progress.as_ref().unwrap(); - let network_pct = (100 * stats.received_objects()) / stats.total_objects(); - let index_pct = (100 * stats.indexed_objects()) / stats.total_objects(); - let co_pct = if state.total > 0 { - (100 * state.current) / state.total - } else { - 0 - }; - let kbytes = stats.received_bytes() / 1024; - if stats.received_objects() == stats.total_objects() { - if !state.newline { - println!(); - state.newline = true; - } - print!( - "Resolving deltas {}/{}\r", - stats.indexed_deltas(), - stats.total_deltas() - ); - } else { - print!( - "net {:3}% ({:4} kb, {:5}/{:5}) / idx {:3}% ({:5}/{:5}) / chk {:3}% ({:4}/{:4}) \ - {}\r", - network_pct, - kbytes, - stats.received_objects(), - stats.total_objects(), - index_pct, - stats.indexed_objects(), - stats.total_objects(), - co_pct, - state.current, - state.total, - state - .path - .as_ref() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_default() - ) - } - io::stdout().flush().unwrap(); -} - -fn run(args: &Args) -> Result<(), git2::Error> { - let state = RefCell::new(State { - progress: None, - total: 0, - current: 0, - path: None, - newline: false, - }); - let mut cb = RemoteCallbacks::new(); - cb.transfer_progress(|stats| { - let mut state = state.borrow_mut(); - state.progress = Some(stats.to_owned()); - print(&mut *state); - true - }); - - let mut co = CheckoutBuilder::new(); - co.progress(|path, cur, total| { - let mut state = state.borrow_mut(); - state.path = path.map(|p| p.to_path_buf()); - state.current = cur; - state.total = total; - print(&mut *state); - }); - - let mut fo = FetchOptions::new(); - fo.remote_callbacks(cb); - RepoBuilder::new() - .fetch_options(fo) - .with_checkout(co) - .clone(&args.arg_url, Path::new(&args.arg_path))?; - println!(); - - Ok(()) -} - -fn main() { - let args = Args::from_args(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/gnostr-dashboard._rs b/src/bin/gnostr-dashboard._rs deleted file mode 100755 index 77f0a78f8c..0000000000 --- a/src/bin/gnostr-dashboard._rs +++ /dev/null @@ -1,579 +0,0 @@ -use std::sync::mpsc; -use std::time::{Duration, Instant}; -use std::{fs, io, process, thread}; - -use chrono::prelude::*; -use crossterm::event::{self, Event as CEvent, KeyCode}; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; -use homedir::get_my_home; -use rand::distributions::Alphanumeric; -use rand::prelude::*; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use tui::backend::CrosstermBackend; -use tui::layout::{Alignment, Constraint, Direction, Layout}; -use tui::style::{Color, Modifier, Style}; -use tui::text::{Span, Spans}; -use tui::widgets::{ - Block, BorderType, Borders, Cell, List, ListItem, ListState, Paragraph, Row, Table, Tabs, -}; -use tui::Terminal; -const APP_NAME: &str = env!("CARGO_PKG_NAME"); -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -const ICON_FONT_SIZE: u16 = 12; -const DB_PATH: &str = "./data/db.json"; - -const INDIGO: Color = Color::Rgb(182, 46, 209); - -#[derive(Error, Debug)] -pub enum Error { - #[error("error reading the DB file: {0}")] - ReadDBError(#[from] io::Error), - #[error("error parsing the DB file: {0}")] - ParseDBError(#[from] serde_json::Error), -} - -enum Event { - Input(I), - Tick, -} - -#[derive(Serialize, Deserialize, Clone)] -struct Pet { - id: usize, - name: String, - category: String, - age: usize, - created_at: DateTime, -} - -#[derive(Copy, Clone, Debug)] -enum MenuItem { - Home, - Pets, -} - -impl From for usize { - fn from(input: MenuItem) -> usize { - match input { - MenuItem::Home => 0, - MenuItem::Pets => 1, - } - } -} - -fn main() -> Result<(), Box> { - enable_raw_mode().expect("can run in raw mode"); - - let db_path: &str = &format!( - "{:}/.gnostr/data/db.json", - get_my_home().unwrap().unwrap().display() - ); - let _ = add_random_pet_to_db(db_path); - let (tx, rx) = mpsc::channel(); - let tick_rate = Duration::from_millis(200); - thread::spawn(move || { - let mut last_tick = Instant::now(); - loop { - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - - if event::poll(timeout).expect("poll works") { - if let CEvent::Key(key) = event::read().expect("can read events") { - tx.send(Event::Input(key)).expect("can send events"); - } - } - - if last_tick.elapsed() >= tick_rate { - if let Ok(_) = tx.send(Event::Tick) { - last_tick = Instant::now(); - } - } - } - }); - - let stdout = io::stdout(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - terminal.clear()?; - - //MENU TITLES - - let menu_titles = vec!["Home", "Relays", "Add", "Delete", "Quit"]; - let mut active_menu_item = MenuItem::Home; - let mut pet_list_state = ListState::default(); - pet_list_state.select(Some(0)); - - //LOOP - loop { - terminal.draw(|rect| { - let size = rect.size(); - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints( - [ - Constraint::Length(3), - Constraint::Min(2), - Constraint::Length(3), - ] - .as_ref(), - ) - .split(size); - - let copyright = Paragraph::new(format!(" {} FOOTER", APP_NAME)) - .style(Style::default().fg(Color::LightCyan)) - .alignment(Alignment::Left) - .block( - Block::default() - .borders(Borders::ALL) - .style(Style::default().fg(Color::White)) - .title(format!(" {} v{}", APP_NAME, VERSION)) - .border_type(BorderType::Plain), - ); - - let menu = menu_titles - .iter() - .map(|t| { - let (first, rest) = t.split_at(1); - Spans::from(vec![ - Span::styled( - first, - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::UNDERLINED), - ), - Span::styled(rest, Style::default().fg(Color::White)), - ]) - }) - .collect(); - - let tabs = Tabs::new(menu) - .select(active_menu_item.into()) - .block(Block::default().title("Menu").borders(Borders::ALL)) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().fg(Color::Yellow)) - .divider(Span::raw("|")); - - rect.render_widget(tabs, chunks[0]); - match active_menu_item { - MenuItem::Home => rect.render_widget(render_home(), chunks[1]), - MenuItem::Pets => { - let pets_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints( - [Constraint::Percentage(20), Constraint::Percentage(80)].as_ref(), - ) - .split(chunks[1]); - let (left, right) = render_pets(db_path, &pet_list_state); - rect.render_stateful_widget(left, pets_chunks[0], &mut pet_list_state); - rect.render_widget(right, pets_chunks[1]); - - //footer not persist after quit here - rect.render_widget(copyright.clone(), chunks[2]); - } - } - })?; - - match rx.recv()? { - Event::Input(event) => match event.code { - KeyCode::Char('q') => { - disable_raw_mode()?; - //terminal.clear()?; - render_home(); - terminal.show_cursor()?; - break; - } - KeyCode::Char('h') => active_menu_item = MenuItem::Home, - KeyCode::Char('r') => active_menu_item = MenuItem::Pets, - KeyCode::Char('a') => { - add_random_pet_to_db(db_path).expect("can add new random pet"); - } - KeyCode::Char('d') => { - remove_pet_at_index(db_path, &mut pet_list_state).expect("can remove pet"); - } - KeyCode::Down => { - if let Some(selected) = pet_list_state.selected() { - let amount_pets = read_db(db_path).expect("can fetch pet list").len(); - if selected >= amount_pets - 1 { - pet_list_state.select(Some(0)); - } else { - pet_list_state.select(Some(selected + 1)); - } - } - } - KeyCode::Up => { - if let Some(selected) = pet_list_state.selected() { - let amount_pets = read_db(db_path).expect("can fetch pet list").len(); - if selected > 0 { - pet_list_state.select(Some(selected - 1)); - } else { - pet_list_state.select(Some(amount_pets - 1)); - } - } - } - _ => {} - }, - Event::Tick => {} - } - } - - clearscreen::clear().expect("failed to clear screen"); - Ok(()) -} - -fn render_home<'a>() -> Paragraph<'a> { - let home = Paragraph::new(vec![ - //REF: Unicode Character “█” (U+2588) - - //center line - //Spans::from(vec![Span::raw(" - //███████████████████████████████████████•███████████████████████████████████████ - //")]), - Spans::from(vec![Span::raw("")]), - Spans::from(vec![Span::raw("")]), - Spans::from(vec![Span::raw("")]), - Spans::from(vec![Span::raw( - " - █•█ ", - )]), - Spans::from(vec![Span::raw( - " - ███•███ ", - )]), - Spans::from(vec![Span::raw( - " - █████•█████ ", - )]), - Spans::from(vec![Span::raw( - " - ███████•███████ ", - )]), - Spans::from(vec![Span::raw( - " - █████████•█████████ ", - )]), - Spans::from(vec![Span::raw( - " - ██████████•███████████ ", - )]), - Spans::from(vec![Span::raw( - " -█ ████████•█████████████", - )]), - Spans::from(vec![Span::raw( - " - ████ ██████•███████████████", - )]), - Spans::from(vec![Span::raw( - " - ████████ ███•█████████████████", - )]), - Spans::from(vec![Span::raw( - " - ████████████ █•███████████████████", - )]), - Spans::from(vec![Span::raw( - " - ████████████████ ██████████████████ ", - )]), - Spans::from(vec![Span::raw( - " -██████████████████ ██████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -███████████████████ ███████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -█████████████████████ █████████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -████████████████████████ ████████████████████████ -", - )]), - Spans::from(vec![Span::raw( - " - ████████████████████████████ ████████████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -███████████████████████████████ █ ████████████████████████ -", - )]), - Spans::from(vec![Span::raw( - " - ██████████████████████████████████ ███ ██████████████████████ ", - )]), - Spans::from(vec![Span::raw( - " -████████████████████████████████████ █████ ████████████████████", - )]), - Spans::from(vec![Span::raw( - " -█████████████████████████████████████ ███████ ██████████████████ -", - )]), - //vim command to find center - //:exe 'normal '.(virtcol('$')/2).'|' - // █ - // ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ▔ ▕ ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ▔ ▕ - // █ - // █ - //FULL BLOCK - //Unicode: U+2588, UTF-8: E2 96 88 - - //center line - Spans::from(vec![Span::raw( - " -█████████████████████████████████████ • ███████ █████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -████████████████████████████████████ ████████ ████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -██████████████████████████████████ ██████████ ███████████████ -", - )]), - Spans::from(vec![Span::raw( - " -████████████████████████████████ ███████████████████████████████", - )]), - Spans::from(vec![Span::raw( - " -███████████████████████████████ ██████████████████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -█████████████████████████████ ████████████████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -██████████████████████████ █████████████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -██████████████████████ █████████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -███████████████████ ██████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -█████████████████ ████████████████ -", - )]), - Spans::from(vec![Span::raw( - " -████████████████ ███████████████ -", - )]), - Spans::from(vec![Span::raw( - " -████████████████ ███████████████", - )]), - Spans::from(vec![Span::raw( - " - ████████████████•████████████████ ", - )]), - Spans::from(vec![Span::raw( - " - ██████████████•██████████████ ", - )]), - Spans::from(vec![Span::raw( - " - ████████████•████████████ ", - )]), - Spans::from(vec![Span::raw( - " - █████████•█████████ ", - )]), - Spans::from(vec![Span::raw( - " - ███████•███████ ", - )]), - Spans::from(vec![Span::raw( - " - █████•█████ ", - )]), - Spans::from(vec![Span::raw( - " - ███•███ ", - )]), - Spans::from(vec![Span::raw( - " - █•█ ", - )]), - //center line - //Spans::from(vec![Span::raw(" - //███████████████████████████████████████•███████████████████████████████████████ - //")]), - Spans::from(vec![Span::styled( - " ", - Style::default().fg(Color::LightBlue), - )]), - Spans::from(vec![Span::raw("")]), - //Spans::from(vec![Span::raw("Press 'p' to access pets, 'a' to add random new pets and 'd' - // to delete the currently selected pet.")]), - ]) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - //.style(Style::default().fg(Color::Magenta)) - //.style(Style::default().fg(Color::Black)) - .style(Style::default().fg(Color::White)) - //.style(Style::default().fg(Color::Rgb(100,1,1))) - //.style(Style::default().fg(Color::Rgb(255,1,1))) - //TODO git repo - .title(" gnostr ") - .border_type(BorderType::Plain), - ); - home -} - -fn render_pets<'a>(db_path: &str, pet_list_state: &ListState) -> (List<'a>, Table<'a>) { - let pets = Block::default() - .borders(Borders::ALL) - .style(Style::default().fg(Color::White)) - .title("Relays") - .border_type(BorderType::Plain); - - let pet_list = read_db(db_path).expect("can fetch pet list"); - let items: Vec<_> = pet_list - .iter() - .map(|pet| { - ListItem::new(Spans::from(vec![Span::styled( - pet.name.clone(), - Style::default(), - )])) - }) - .collect(); - - let selected_pet = pet_list - .get( - pet_list_state - .selected() - .expect("there is always a selected pet"), - ) - .expect("exists") - .clone(); - - let list = List::new(items).block(pets).highlight_style( - Style::default() - .bg(Color::Yellow) - .fg(Color::Black) - .add_modifier(Modifier::BOLD), - ); - - let pet_detail = Table::new(vec![Row::new(vec![ - Cell::from(Span::raw(selected_pet.id.to_string())), - Cell::from(Span::raw(selected_pet.name)), - Cell::from(Span::raw(selected_pet.category)), - Cell::from(Span::raw(selected_pet.age.to_string())), - Cell::from(Span::raw(selected_pet.created_at.to_string())), - ])]) - .header(Row::new(vec![ - Cell::from(Span::styled( - "ID", - Style::default().add_modifier(Modifier::BOLD), - )), - Cell::from(Span::styled( - "Name", - Style::default().add_modifier(Modifier::BOLD), - )), - Cell::from(Span::styled( - "Category", - Style::default().add_modifier(Modifier::BOLD), - )), - Cell::from(Span::styled( - "Age", - Style::default().add_modifier(Modifier::BOLD), - )), - Cell::from(Span::styled( - "Created At", - Style::default().add_modifier(Modifier::BOLD), - )), - ])) - .block( - Block::default() - .borders(Borders::ALL) - .style(Style::default().fg(Color::White)) - .title("Detail") - .border_type(BorderType::Plain), - ) - .widths(&[ - Constraint::Percentage(5), - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(5), - Constraint::Percentage(20), - ]); - - (list, pet_detail) -} - -fn read_db(db_path: &str) -> Result, Error> { - let db_content = fs::read_to_string(db_path)?; - if db_content.len() < 3 { - let _ = add_random_pet_to_db(db_path); - } - let parsed: Vec = serde_json::from_str(&db_content)?; - Ok(parsed) -} - -fn add_random_pet_to_db(db_path: &str) -> Result, Error> { - let mut rng = rand::thread_rng(); - let db_content = fs::read_to_string(db_path)?; - let mut parsed: Vec = serde_json::from_str(&db_content)?; - let catsdogs = match rng.gen_range(0, 1) { - 0 => "cats", - _ => "dogs", - }; - - let random_pet = Pet { - id: rng.gen_range(0, 9999999), - name: rng.sample_iter(Alphanumeric).take(10).collect(), - category: catsdogs.to_owned(), - age: rng.gen_range(1, 15), - created_at: Utc::now(), - }; - - parsed.push(random_pet); - fs::write(db_path, &serde_json::to_vec(&parsed)?)?; - Ok(parsed) -} - -fn remove_pet_at_index(db_path: &str, pet_list_state: &mut ListState) -> Result<(), Error> { - if let Some(selected) = pet_list_state.selected() { - let db_content = fs::read_to_string(db_path)?; - let mut parsed: Vec = serde_json::from_str(&db_content)?; - parsed.remove(selected); - fs::write(db_path, &serde_json::to_vec(&parsed)?)?; - let amount_pets = read_db(db_path).expect("can fetch pet list").len(); - if selected > 0 { - pet_list_state.select(Some(selected - 1)); - } else { - pet_list_state.select(Some(0)); - } - } - Ok(()) -} diff --git a/src/bin/gnostr-fetch.rs b/src/bin/gnostr-fetch.rs deleted file mode 100755 index 6e9e68a10d..0000000000 --- a/src/bin/gnostr-fetch.rs +++ /dev/null @@ -1,134 +0,0 @@ -/* - * libgit2 "fetch" example - shows how to fetch remote data - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all copyright - * and related and neighboring rights to this software to the public domain - * worldwide. This software is distributed without any warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -#![deny(warnings)] - -use clap::Parser; -use git2::{ - /*AutotagOption, */ FetchOptions, RemoteCallbacks, /*RemoteUpdateFlags, */ Repository, -}; -use std::io::{self, Write}; -use std::str; - -#[derive(Parser)] -struct Args { - #[structopt(name = "remote")] - arg_remote: Option, -} - -fn run(args: &Args) -> Result<(), git2::Error> { - let repo = Repository::discover(".")?; - let remote = args.arg_remote.as_ref().map(|s| &s[..]).unwrap_or("origin"); - - // Figure out whether it's a named remote or a URL - println!("Fetching {} for repo", remote); - let mut cb = RemoteCallbacks::new(); - let mut remote = repo - .find_remote(remote) - .or_else(|_| repo.remote_anonymous(remote))?; - cb.sideband_progress(|data| { - print!("remote: {}", str::from_utf8(data).unwrap()); - io::stdout().flush().unwrap(); - true - }); - - // This callback gets called for each remote-tracking branch that gets - // updated. The message we output depends on whether it's a new one or an - // update. - cb.update_tips(|refname, a, b| { - if a.is_zero() { - println!("[new] {:20} {}", b, refname); - } else { - println!("[updated] {:10}..{:10} {}", a, b, refname); - } - true - }); - - // Here we show processed and total objects in the pack and the amount of - // received data. Most frontends will probably want to show a percentage and - // the download rate. - cb.transfer_progress(|stats| { - if stats.received_objects() == stats.total_objects() { - print!( - "Resolving deltas {}/{}\r", - stats.indexed_deltas(), - stats.total_deltas() - ); - } else if stats.total_objects() > 0 { - print!( - "Received {}/{} objects ({}) in {} bytes\r", - stats.received_objects(), - stats.total_objects(), - stats.indexed_objects(), - stats.received_bytes() - ); - } - io::stdout().flush().unwrap(); - true - }); - - // Download the packfile and index it. This function updates the amount of - // received data and the indexer stats which lets you inform the user about - // progress. - let mut fo = FetchOptions::new(); - fo.remote_callbacks(cb); - remote.download(&[] as &[&str], Some(&mut fo))?; - - { - // If there are local objects (we got a thin pack), then tell the user - // how many objects we saved from having to cross the network. - let stats = remote.stats(); - if stats.local_objects() > 0 { - println!( - "\rReceived {}/{} objects in {} bytes (used {} local \ - objects)", - stats.indexed_objects(), - stats.total_objects(), - stats.received_bytes(), - stats.local_objects() - ); - } else { - println!( - "\rReceived {}/{} objects in {} bytes", - stats.indexed_objects(), - stats.total_objects(), - stats.received_bytes() - ); - } - } - - // Disconnect the underlying connection to prevent from idling. - remote.disconnect()?; - - // Update the references in the remote's namespace to point to the right - // commits. This may be needed even if there was no packfile to download, - // which can happen e.g. when the branches have been changed but all the - // needed objects are available locally. - //remote.update_tips( - // None, - // RemoteUpdateFlags::UPDATE_FETCHHEAD, - // AutotagOption::Unspecified, - // None, - //)?; - - Ok(()) -} - -fn main() { - let args = Args::parse(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/gnostr-genssh.rs b/src/bin/gnostr-genssh.rs new file mode 100644 index 0000000000..230aecaa5a --- /dev/null +++ b/src/bin/gnostr-genssh.rs @@ -0,0 +1,290 @@ +use std::env; +use std::fs; +use std::io::Result; +use std::path::{Path, PathBuf}; +use std::process::{exit, Command}; + +fn main() { + let email = env::args() + .nth(1) + .unwrap_or_else(|| "gnostr@gnostr.org".to_string()); + + let home_dir = env::var("HOME") + .or_else(|_| env::var("USERPROFILE")) + .expect("HOME or USERPROFILE environment variable not set"); + let ssh_dir = PathBuf::from(&home_dir).join(".ssh"); + let authorized_keys_file = ssh_dir.join("authorized_keys"); + + println!("Starting SSH permissions setup..."); + + if !ssh_dir.exists() { + println!( + "Directory '{}' does not exist. Creating it...", + ssh_dir.display() + ); + if let Err(e) = fs::create_dir_all(&ssh_dir) { + eprintln!( + "Error: Failed to create directory '{}'. {}", + ssh_dir.display(), + e + ); + exit(1); + } + } else { + println!("Directory '{}' already exists.", ssh_dir.display()); + } + + println!("Generating SSH key pair (gnostr-gnit-key)..."); + let key_file_name = "gnostr-gnit-key"; + let output = Command::new("ssh-keygen") + .arg("-t") + .arg("ed25519") + .arg("-f") + .arg(ssh_dir.join(key_file_name)) + .arg("-C") + .arg(&email) + .arg("-N") + .arg("") + .output(); + + match output { + Ok(output) => { + if output.status.success() { + println!("SSH key generation successful."); + } else { + eprintln!("Error: ssh-keygen failed."); + eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout)); + eprintln!("Stderr: {}", String::from_utf8_lossy(&output.stderr)); + exit(1); + } + } + Err(e) => { + eprintln!( + "Error: Could not execute ssh-keygen. Is it installed and in your PATH? {}", + e + ); + exit(1); + } + } + + // 2. Set permissions for the ~/.ssh directory. + // On Windows, setting specific "mode" bits like 700 directly isn't equivalent. + // We'll aim for "owner only" via readonly and then rely on ACLs for security. + println!("Setting permissions for '{}'...", ssh_dir.display()); + if let Err(e) = set_directory_permissions(&ssh_dir) { + eprintln!( + "Error: Failed to set permissions for '{}'. {}", + ssh_dir.display(), + e + ); + exit(1); + } else { + println!("Permissions for '{}' adjusted for OS.", ssh_dir.display()); + } + + // 3. Set permissions for the authorized_keys file. + if authorized_keys_file.exists() { + println!("File '{}' found.", authorized_keys_file.display()); + println!( + "Setting permissions for '{}'...", + authorized_keys_file.display() + ); + if let Err(e) = set_file_permissions(&authorized_keys_file) { + eprintln!( + "Error: Failed to set permissions for '{}'. {}", + authorized_keys_file.display(), + e + ); + exit(1); + } else { + println!( + "Permissions for '{}' adjusted for OS.", + authorized_keys_file.display() + ); + } + } else { + println!( + "File '{}' not found. No permissions to set for it.", + authorized_keys_file.display() + ); + println!("Note: If you plan to use SSH keys for authentication, you will need to"); + println!( + "add your public key to '{}'.", + authorized_keys_file.display() + ); + } + + // 4. Set permissions for private SSH keys. + let private_key_types = vec!["id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", key_file_name]; + + println!("Checking for and setting permissions for private SSH keys..."); + for key_type in private_key_types { + let private_key_file = ssh_dir.join(key_type); + if private_key_file.exists() { + println!("Private key '{}' found.", private_key_file.display()); + println!( + "Setting permissions for '{}'...", + private_key_file.display() + ); + if let Err(e) = set_file_permissions(&private_key_file) { + eprintln!( + "Error: Failed to set permissions for '{}'. {}", + private_key_file.display(), + e + ); + exit(1); + } else { + println!( + "Permissions for '{}' adjusted for OS.", + private_key_file.display() + ); + } + } else { + println!("Private key '{}' not found.", private_key_file.display()); + } + } + + // 5. Set permissions for public SSH keys. + + let binding = key_file_name.to_owned() + ".pub"; + let public_key_types = vec![ + "id_rsa.pub", + "id_dsa.pub", + "id_ecdsa.pub", + "id_ed25519.pub", + &binding, + ]; + + println!("Checking for and setting permissions for public SSH keys..."); + for key_type in public_key_types { + let public_key_file = ssh_dir.join(key_type); + if public_key_file.exists() { + println!("Public key '{}' found.", public_key_file.display()); + println!("Setting permissions for '{}'...", public_key_file.display()); + if let Err(e) = set_public_key_permissions(&public_key_file) { + eprintln!( + "Error: Failed to set permissions for '{}'. {}", + public_key_file.display(), + e + ); + exit(1); + } else { + println!( + "Permissions for '{}' adjusted for OS.", + public_key_file.display() + ); + } + } else { + println!("Public key '{}' not found.", public_key_file.display()); + } + } +} + +// Function to set permissions for directories +fn set_directory_permissions(path: &Path) -> Result<()> { + #[cfg(target_os = "macos")] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o700); // rwx------ + fs::set_permissions(path, perms) + } + + #[cfg(target_os = "linux")] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o700); // rwx------ + fs::set_permissions(path, perms) + } + + #[cfg(target_os = "windows")] + { + // On Windows, there's no direct equivalent to chmod for directories to make them owner-only + // using just standard library `Permissions`. + // The `SetFileAttributes` function (which `set_permissions` uses) primarily sets + // flags like `FILE_ATTRIBUTE_READONLY`. + // For true granular control (like owner-only), you need to work with ACLs. + // For SSH, the critical part is that the user account running SSH *can* access these files, + // and that other users *cannot*. Relying on default Windows permissions where only the + // current user has full control is often sufficient for ~/.ssh. + // If a stricter ACL is needed, a crate like `windows-permissions` or direct WinAPI calls + // would be necessary. For this script, we'll ensure it's not world-writable via the + // `set_readonly(true)` if it's a file, but for directories, `create_dir_all` often + // inherits sensible permissions. We'll simply ensure it's not marked as readonly. + let mut perms = fs::metadata(path)?.permissions(); + perms.set_readonly(false); // Ensure it's not read-only + fs::set_permissions(path, perms)?; + println!(" (Windows: Relying on default ACLs for directory permissions.)"); + Ok(()) + } +} + +// Function to set permissions for private files (like private keys, authorized_keys) +fn set_file_permissions(path: &Path) -> Result<()> { + #[cfg(target_os = "macos")] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o600); // rw------- + fs::set_permissions(path, perms) + } + + #[cfg(target_os = "linux")] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o600); // rw------- + fs::set_permissions(path, perms) + } + + #[cfg(target_os = "windows")] + { + // On Windows, setting a file to "600" (owner read/write only) means ensuring + // it's not set as FILE_ATTRIBUTE_READONLY and that its ACL only grants + // the current user full control. `set_readonly(true)` makes it *more* restricted. + // For private keys, we want to ensure only the owner can read/write. + // The `std::fs::set_permissions` function on Windows corresponds to `SetFileAttributes`. + // Setting `set_readonly(true)` is the closest standard library equivalent to restrict + // writes, but true owner-only access usually involves ACL manipulation. + // SSH on Windows generally expects the private key file to *not* be accessible + // by other users. The default file creation permissions often achieve this. + let mut perms = fs::metadata(path)?.permissions(); + perms.set_readonly(true); // Attempt to make it read-only for all, closest to 600 + fs::set_permissions(path, perms)?; + println!(" (Windows: Set file to read-only attribute. For stronger security, consider manual ACL review.)"); + Ok(()) + } +} + +// Function to set permissions for public files (like public keys) +fn set_public_key_permissions(path: &Path) -> Result<()> { + #[cfg(target_os = "macos")] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o644); // rw-r--r-- + fs::set_permissions(path, perms) + } + + #[cfg(target_os = "linux")] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o644); // rw-r--r-- + fs::set_permissions(path, perms) + } + + #[cfg(target_os = "windows")] + { + // For public keys (644), we want owner read/write, others read. + // On Windows, this translates to ensuring it's not marked as readonly, + // and relying on default ACLs that typically allow read by "Everyone" + // and write by "Owner" (or administrators). + let mut perms = fs::metadata(path)?.permissions(); + perms.set_readonly(false); // Ensure it's not read-only + fs::set_permissions(path, perms)?; + println!(" (Windows: Ensured public key is not read-only. Default ACLs usually allow broader read access.)"); + Ok(()) + } +} diff --git a/src/bin/gnostr-get-example._rs b/src/bin/gnostr-get-example._rs deleted file mode 100644 index 0557502cd5..0000000000 --- a/src/bin/gnostr-get-example._rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::io::Read; -use std::time::{Instant, SystemTime, UNIX_EPOCH}; - -use gnostr_bins::get_blockheight; -use reqwest::Url; -use tokio::runtime::Runtime; - -//use ureq::get; - -const URL: &str = "https://mempool.space/api/blocks/tip/height"; - -fn main() { - let n = 1; - { - let start = Instant::now(); - let res = blocking(n); - println!("blocking {:?} {} bytes", start.elapsed(), res); - } - { - let start = Instant::now(); - let mut rt = tokio::runtime::Runtime::new().unwrap(); - let res = rt.block_on(non_blocking(n)); - println!("async {:?} {} bytes", start.elapsed(), res); - } -} - -fn blocking(n: usize) -> usize { - (0..n) - .into_iter() - .map(|_| { - std::thread::spawn(|| { - let mut body = ureq::get(URL).call().expect("REASON").into_reader(); - let mut buf = Vec::new(); - body.read_to_end(&mut buf).unwrap(); - // print block count from mempool.space or panic - let text = match std::str::from_utf8(&buf) { - Ok(s) => s, - Err(_) => panic!("Invalid ASCII data"), - }; - println!("{}", text); - buf.len() - }) - }) - .collect::>() - .into_iter() - .map(|it| it.join().unwrap()) - .sum() -} - -async fn non_blocking(n: usize) -> usize { - let tasks = (0..n) - .into_iter() - .map(|_| { - tokio::spawn(async move { - let since_the_epoch = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("get millis error"); - let seconds = since_the_epoch.as_secs(); - let subsec_millis = since_the_epoch.subsec_millis() as u64; - let now_millis = seconds * 1000 + subsec_millis; - //println!("now millis: {}", seconds * 1000 + subsec_millis); - - let _ = get_blockheight(); - let url = Url::parse(URL).unwrap(); - let mut res = reqwest::blocking::get(url).unwrap(); - - let mut tmp_string = String::new(); - res.read_to_string(&mut tmp_string).unwrap(); - //println!("{}", format!("{:?}", res)); - let tmp_u64 = tmp_string.parse::().unwrap_or(0); - println!("{}", format!("{:?}", tmp_u64)); - - //TODO:impl gnostr-weeble_millis - //let weeble = now_millis as f64 / tmp_u64 as f64; - //let weeble = seconds as f64 / tmp_u64 as f64; - //println!("{}", format!("{}", weeble.floor())); - - let body = reqwest::get(URL).await.unwrap().bytes(); - body.await.unwrap().len() - // print block count from mempool.space or panic - //let text = match std::str::from_utf8(&body) { - // Ok(s) => s, - // Err(_) => panic!("Invalid ASCII data"), - //}; - //println!("{}", text); - }) - }) - .collect::>(); - - let mut res = 0; - for task in tasks { - res += task.await.unwrap(); - } - res -} diff --git a/src/bin/gnostr-legit.rs b/src/bin/gnostr-legit.rs new file mode 100644 index 0000000000..0be4d7b774 --- /dev/null +++ b/src/bin/gnostr-legit.rs @@ -0,0 +1,256 @@ +#![allow(unused)] +#![allow(dead_code)] +extern crate chrono; +extern crate time; +use chrono::offset::Utc; +use chrono::DateTime; +use gnostr_asyncgit::sync::commit::{padded_commit_id, serialize_commit}; + +use gnostr::global_rt::global_rt; +use log::debug; +use log::info; +// +use nostr_sdk_0_37_0::prelude::*; +// + +use std::process::Command; +//use std::time::SystemTime; +use std::any::type_name; +use std::convert::TryInto; +use std::env; +use std::error::Error; +use std::io::Result; +use std::thread::sleep; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use time::{get_time, now}; +//use std::mem::size_of; +use argparse::{ArgumentParser, Store}; +use git2::*; +use gnostr::get_pwd; +use gnostr::legit::gitminer; +use gnostr::legit::gitminer::Gitminer; +use gnostr::legit::post_event; +use gnostr::legit::repo; +use gnostr::legit::worker; +use gnostr_types::Event; +use gnostr_types::EventV3; +use pad::{Alignment, PadStr}; +use sha2::{Digest, Sha256}; +use std::{io, thread}; + +use std::path::PathBuf; //for get_current_dir + +//pub mod gitminer; +//pub mod repo; +//pub mod worker; + +//fn type_of(_: T) -> &'static str { +// type_name::() +//} + +fn get_epoch_ms() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() +} + +fn convert_to_u32(v: usize) -> Option { + if v > (std::i8::MAX as i32).try_into().unwrap() { + None + } else { + Some(v as i8) + } +} + +fn get_current_working_dir() -> std::io::Result { + env::current_dir() +} + +#[cfg(debug_assertions)] +fn example() { + debug!("Debugging enabled"); + debug!("cwd={:?}", get_current_working_dir()); +} + +#[cfg(not(debug_assertions))] +fn example() { + debug!("Debugging disabled"); + debug!("cwd={:?}", get_current_working_dir()); +} + +#[tokio::main] +async fn main() -> Result<()> { + #[allow(clippy::if_same_then_else)] + if cfg!(debug_assertions) { + debug!("Debugging enabled"); + } else { + debug!("Debugging disabled"); + } + + #[cfg(debug_assertions)] + debug!("Debugging enabled"); + #[cfg(not(debug_assertions))] + debug!("Debugging disabled"); + example(); + + let start = get_time(); + let epoch = get_epoch_ms(); + println!("epoch:{}", epoch.clone()); + let system_time = SystemTime::now(); + println!("system_time:{:?}", system_time.clone()); + + let datetime: DateTime = system_time.into(); + println!("{}", datetime.format("%d/%m/%Y %T/%s")); + println!("{}", datetime.format("%d/%m/%Y %T/%f")); + println!("{}", datetime.format("%d/%m/%Y %T")); + + //let cwd = get_current_working_dir(); + let cwd = get_pwd(); + #[cfg(debug_assertions)] + println!("Debugging enabled"); + println!("{:#?}", cwd); + let state = repo::state(); + println!("{:#?}", state); + // + let repo_root = std::env::args().nth(1).unwrap_or(".".to_string()); + println!("repo_root={:?}", repo_root.as_str()); + let repo = Repository::discover(repo_root.as_str()).expect("Couldn't open repository"); + println!("{} state={:?}", repo.path().display(), repo.state()); + println!("state={:?}", repo.state()); + + let count = thread::available_parallelism()?.get(); + assert!(count >= 1_usize); + + let now = SystemTime::now(); + + let pwd = env::current_dir()?; + println!("pwd={}", pwd.clone().display()); + let mut hasher = Sha256::new(); + hasher.update(&format!("{}", pwd.clone().display())); + //sha256sum <(echo gnostr-legit) + let pwd_hash: String = format!("{:x}", hasher.finalize()); + println!("pwd_hash={:?}", pwd_hash); + + let mut opts = gitminer::Options { + threads: count.try_into().unwrap(), + target: "00000".to_string(), //default 00000 + //gnostr:##:nonce + //part of the gnostr protocol + //src/worker.rs adds the nonce + pwd_hash: pwd_hash.clone(), + message: cwd.unwrap(), + //message: message, + //message: count.to_string(), + //repo: ".".to_string(), + repo: repo.path().display().to_string(), + timestamp: time::now(), + }; + + parse_args_or_exit(&mut opts); + + let mut miner = match Gitminer::new(opts) { + Ok(m) => m, + Err(e) => { + panic!("Failed to start git miner: {}", e); + } + }; + + let hash = match miner.mine() { + Ok(s) => s, + Err(e) => { + panic!("Failed to generate commit: {}", e); + } + }; + + //initialize git repo + let repo = Repository::discover(".").expect(""); + + //gather some repo info + //find HEAD + let head = repo.head().expect(""); + let obj = head + .resolve() + .expect("") + .peel(ObjectType::Commit) + .expect(""); + + //read top commit + let commit = obj.peel_to_commit().expect(""); + let commit_id = commit.id().to_string(); + + let serialized_commit = serialize_commit(&commit).expect("gnostr-async:error!"); + println!("Serialized commit:\n{}", serialized_commit.clone()); + + //some info wrangling + println!("commit_id:\n{}", commit_id); + let padded_commitid = padded_commit_id(format!("{:0>64}", commit_id.clone())); + println!("padded_commitid:\n{}", padded_commitid.clone()); + global_rt().spawn(async move { + //// commit based keys + //let keys = generate_nostr_keys_from_commit_hash(&commit_id)?; + //info!("keys.secret_key():\n{:?}", keys.secret_key()); + //info!("keys.public_key():\n{}", keys.public_key()); + + //parse keys from sha256 hash + let padded_keys = Keys::parse(padded_commitid).unwrap(); + //create nostr client with commit based keys + //let client = Client::new(keys); + let client = Client::new(padded_keys.clone()); + client.add_relay("wss://relay.damus.io").await.expect(""); + client.add_relay("wss://e.nos.lol").await.expect(""); + client.connect().await; + + //build git gnostr event + let builder = EventBuilder::text_note(serialized_commit.clone()); + + //send git gnostr event + let output = client.send_event_builder(builder).await.expect(""); + + //some reporting + info!("Event ID: {}", output.id()); + info!("Event ID BECH32: {}", output.id().to_bech32().expect("")); + info!("Sent to: {:?}", output.success); + info!("Not sent to: {:?}", output.failed); + }); + + Ok(()) +} + +fn parse_args_or_exit(opts: &mut gitminer::Options) { + let mut ap = ArgumentParser::new(); + ap.set_description("Generate git commit sha with a custom prefix"); + ap.stop_on_first_argument(false); + + //ap.refer(&mut opts.repo) + // //.add_argument("repository-path", Store, "Path to your git repository (required)"); + // .add_argument("repository-path", Store, "Path to your git repository"); + // //.required(); + ap.refer(&mut opts.repo) + .add_argument("repository-path", Store, "Path to your git repository"); + + ap.refer(&mut opts.target).add_option( + &["-p", "--prefix"], + Store, + "Desired commit prefix (required)", + ); + //.required(); + + ap.refer(&mut opts.threads).add_option( + &["-t", "--threads"], + Store, + "Number of worker threads to use (default 8)", + ); + + ap.refer(&mut opts.message).add_option( + &["-m", "--message"], + Store, + "Commit message to use (required)", + ); + //.required(); + + //ap.refer(&mut opts.timestamp) + // .add_option(&["--timestamp"], Store, "Commit timestamp to use (default now)"); + + ap.parse_args_or_exit(); +} diff --git a/src/bin/gnostr-ls-remote.rs b/src/bin/gnostr-ls-remote.rs deleted file mode 100755 index d0abd03f51..0000000000 --- a/src/bin/gnostr-ls-remote.rs +++ /dev/null @@ -1,52 +0,0 @@ -/* - * libgit2 "ls-remote" example - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all - * copyright and related and neighboring rights to this software to the - * public domain worldwide. This software is distributed without any - * warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -#![deny(warnings)] - -use git2::{Direction, Repository}; -use structopt::StructOpt; - -#[derive(StructOpt)] -struct Args { - #[structopt(name = "remote")] - arg_remote: String, -} - -fn run(args: &Args) -> Result<(), git2::Error> { - let repo = Repository::open(".")?; - let remote = &args.arg_remote; - let mut remote = repo - .find_remote(remote) - .or_else(|_| repo.remote_anonymous(remote))?; - - // Connect to the remote and call the printing function for each of the - // remote references. - let connection = remote.connect_auth(Direction::Fetch, None, None)?; - - // Get the list of references on the remote and print out their name next to - // what they point to. - for head in connection.list()?.iter() { - println!("{}\t{}", head.oid(), head.name()); - } - Ok(()) -} - -fn main() { - let args = Args::from_args(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/gnostr-post-event.rs b/src/bin/gnostr-post-event.rs deleted file mode 100755 index dfd821a148..0000000000 --- a/src/bin/gnostr-post-event.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::convert::TryInto; -use std::io::Read; -use std::{env, process}; - -use gnostr_types::Event; - -static DEFAULT_RELAY_URL: &str = "wss://e.nos.lol"; -fn main() { - let mut relay_url = DEFAULT_RELAY_URL; - if relay_url == DEFAULT_RELAY_URL {} - let args_vector: Vec = env::args().collect(); - - #[allow(unreachable_code)] - for i in 0..args_vector.len() { - if i == args_vector.len() { - process::exit(i.try_into().unwrap()); - } else { - if args_vector.len() == 0 { - print!("args_vector.len() = {}", 0); - }; - if args_vector.len() == 1 { - //no args case - //no args case - //no args case - let mut s: String = String::new(); - std::io::stdin().read_to_string(&mut s).unwrap(); - let event: Event = serde_json::from_str(&s).unwrap(); - relay_url = DEFAULT_RELAY_URL; - //always reprint s for further piping - print!("{}\n", s); - gnostr::post_event(&relay_url, event); - }; - if args_vector.len() == 2 { - //catch help - if args_vector[1] == "-h" { - print!( - "gnostr --sec | gnostr-post-event --relay {}", - DEFAULT_RELAY_URL - ); - process::exit(0); - } - if args_vector[1] == "--help" { - print!( - "gnostr --sec | gnostr-post-event --relay {}", - DEFAULT_RELAY_URL - ); - process::exit(0); - } - //catch version - if args_vector[1] == "-v" { - const VERSION: &str = env!("CARGO_PKG_VERSION"); - print!("v{}", VERSION); - process::exit(0); - } - if args_vector[1] == "--version" { - const VERSION: &str = env!("CARGO_PKG_VERSION"); - print!("v{}", VERSION); - process::exit(0); - } - //catch missing url - //because args_vector.len() == 2 - if args_vector[1] == "--relay" { - relay_url = &DEFAULT_RELAY_URL; - //pipe event from command line - //gnostr --sec | gnostr-post-event --relay>// - let mut s: String = String::new(); - std::io::stdin().read_to_string(&mut s).unwrap(); - let event: Event = serde_json::from_str(&s).unwrap(); - //always reprint s for further piping - print!("{}\n", s); - gnostr::post_event(relay_url, event); - process::exit(0); - } - //else assume the second arg is the relay url - relay_url = &args_vector[1]; - //catch the stream - //gnostr --sec | gnostr-post-event - let mut s: String = String::new(); - //this captures the stream when np --relay flag - std::io::stdin().read_to_string(&mut s).unwrap(); - let event: Event = serde_json::from_str(&s).unwrap(); - //if no --relay flag - //assume no reprint - // - //NO print!("{}\n", s); - gnostr::post_event(relay_url, event); - process::exit(0); - }; - //this actually captures the stream when --relay flag - if args_vector.len() == 3 { - //and if - if args_vector[1] == "--relay" || args_vector[1] == "-r" { - relay_url = &args_vector[2]; - let mut s: String = String::new(); - std::io::stdin().read_to_string(&mut s).unwrap(); - let event: Event = serde_json::from_str(&s).unwrap(); - //always reprint s for further piping - print!("{}", s); - gnostr::post_event(relay_url, event); - process::exit(0); - } - relay_url = &args_vector[3 - 1]; - let mut s: String = String::new(); - std::io::stdin().read_to_string(&mut s).unwrap(); - //always reprint s for further piping - //print!("{}\n", s); - let event: Event = serde_json::from_str(&s).unwrap(); - gnostr::post_event(relay_url, event); - }; - } - } -} diff --git a/src/bin/gnostr-query.rs b/src/bin/gnostr-query.rs new file mode 100755 index 0000000000..6cf8860b8f --- /dev/null +++ b/src/bin/gnostr-query.rs @@ -0,0 +1,113 @@ +use gnostr_query::cli::cli; +use gnostr_query::ConfigBuilder; +use log::{debug, trace}; +use serde_json::{json, to_string}; +use url::Url; + +/// Usage +/// nip-0034 kinds +/// gnostr-query -k 1630,1632,1621,30618,1633,1631,1617,30617 +#[tokio::main] +async fn main() -> Result<(), Box> { + let matches = cli().await?; + let mut filt = serde_json::Map::new(); + + if let Some(authors) = matches.get_one::("authors") { + filt.insert( + "authors".to_string(), + json!(authors.split(',').collect::>()), + ); + } + + if let Some(ids) = matches.get_one::("ids") { + filt.insert( + "ids".to_string(), + json!(ids.split(',').collect::>()), + ); + } + + let mut limit_check: i32 = 0; + if let Some(limit) = matches.get_one::("limit") { + // ["EOSE","gnostr-query"] counts as a message! + 1 + filt.insert("limit".to_string(), json!(limit.clone() /*+ 1*/)); + limit_check = *limit; + } + + if let Some(generic) = matches.get_many::("generic") { + let generic_vec: Vec<&String> = generic.collect(); + if generic_vec.len() == 2 { + let tag = format!("#{}", generic_vec[0]); + let val = generic_vec[1].split(',').collect::>(); + filt.insert(tag, json!(val)); + } + } + + if let Some(hashtag) = matches.get_one::("hashtag") { + filt.insert( + "#t".to_string(), + json!(hashtag.split(',').collect::>()), + ); + } + + if let Some(mentions) = matches.get_one::("mentions") { + filt.insert( + "#p".to_string(), + json!(mentions.split(',').collect::>()), + ); + } + + if let Some(references) = matches.get_one::("references") { + filt.insert( + "#e".to_string(), + json!(references.split(',').collect::>()), + ); + } + + if let Some(kinds) = matches.get_one::("kinds") { + if let Ok(kind_ints) = kinds + .split(',') + .map(|s| s.parse::()) + .collect::, _>>() + { + filt.insert("kinds".to_string(), json!(kind_ints)); + } else { + eprintln!("Error parsing kinds. Ensure they are integers."); + std::process::exit(1); + } + } + + let config = ConfigBuilder::new() + .host("localhost") + .port(8080) + .use_tls(true) + .retries(5) + .authors("") + .ids("") + .limit(limit_check) + .generic("", "") + .hashtag("") + .mentions("") + .references("") + .kinds("") + .build()?; + + debug!("{config:?}"); + let q = json!(["REQ", "gnostr-query", filt]); + let query_string = to_string(&q)?; + let relay_url_str = matches.get_one::("relay").clone().unwrap(); + let relay_url = Url::parse(relay_url_str)?; + let vec_result = gnostr_query::send(query_string.clone(), relay_url, Some(limit_check)).await; + //trace + trace!("{:?}", vec_result); + + let mut json_result: Vec = vec![]; + for element in vec_result.unwrap() { + println!("{}", element); + json_result.push(element); + } + //trace + for element in json_result { + trace!("json_result={}", element); + } + Ok(()) +} diff --git a/src/bin/gnostr-remote.rs b/src/bin/gnostr-remote.rs deleted file mode 100644 index 085bfec86b..0000000000 --- a/src/bin/gnostr-remote.rs +++ /dev/null @@ -1,41 +0,0 @@ -use clap::Parser; -use gnostr::remote::host; -//use gnostr::remote::message_stream; -//use gnostr::remote::messages; -//use gnostr::remote::options; -use gnostr::remote::remote_runner; -//use gnostr::remote::tests; - -use anyhow::Result; -use gnostr::remote::options::Opt; -use log::{info, LevelFilter}; -use simple_logger::SimpleLogger; - -fn main() -> Result<()> { - let opt = Opt::parse(); - - let level = match opt.log_level.as_str() { - "error" => LevelFilter::Error, - "warn" => LevelFilter::Warn, - "info" => LevelFilter::Info, - "debug" => LevelFilter::Debug, - "trace" => LevelFilter::Trace, - _ => LevelFilter::Warn, - }; - - SimpleLogger::new() - .with_level(level) - .with_colors(true) - .init()?; - - if opt.remote_runner { - info!("Starting remote-runner"); - remote_runner::update(&opt); - } else { - info!("Starting host"); - host::host_loop(&opt, opt.target.as_ref().map_or("127.0.0.1", |v| v))?; - //host::host_loop(&opt, opt.target.as_ref().unwrap())?; - } - - Ok(()) -} diff --git a/src/bin/gnostr-rev-list.rs b/src/bin/gnostr-rev-list.rs deleted file mode 100755 index ade5c76ec3..0000000000 --- a/src/bin/gnostr-rev-list.rs +++ /dev/null @@ -1,106 +0,0 @@ -/* - * libgit2 "rev-list" example - shows how to transform a rev-spec into a list - * of commit ids - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all - * copyright and related and neighboring rights to this software to the - * public domain worldwide. This software is distributed without any - * warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -#![deny(warnings)] - -use git2::{Error, Oid, Repository, Revwalk}; -use structopt::StructOpt; - -#[derive(StructOpt)] -struct Args { - #[structopt(name = "topo-order", long)] - /// sort commits in topological order - flag_topo_order: bool, - #[structopt(name = "date-order", long)] - /// sort commits in date order - flag_date_order: bool, - #[structopt(name = "reverse", long)] - /// sort commits in reverse - flag_reverse: bool, - #[structopt(name = "not")] - /// don't show \ - flag_not: Vec, - #[structopt(name = "spec", last = true)] - arg_spec: Vec, -} - -fn run(args: &Args) -> Result<(), git2::Error> { - let repo = Repository::open(".")?; - let mut revwalk = repo.revwalk()?; - - let base = if args.flag_reverse { - git2::Sort::REVERSE - } else { - git2::Sort::NONE - }; - revwalk.set_sorting( - base | if args.flag_topo_order { - git2::Sort::TOPOLOGICAL - } else if args.flag_date_order { - git2::Sort::TIME - } else { - git2::Sort::NONE - }, - )?; - - let specs = args - .flag_not - .iter() - .map(|s| (s, true)) - .chain(args.arg_spec.iter().map(|s| (s, false))) - .map(|(spec, hide)| { - if spec.starts_with('^') { - (&spec[1..], !hide) - } else { - (&spec[..], hide) - } - }); - for (spec, hide) in specs { - let id = if spec.contains("..") { - let revspec = repo.revparse(spec)?; - if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { - return Err(Error::from_str("merge bases not implemented")); - } - push(&mut revwalk, revspec.from().unwrap().id(), !hide)?; - revspec.to().unwrap().id() - } else { - repo.revparse_single(spec)?.id() - }; - push(&mut revwalk, id, hide)?; - } - - for id in revwalk { - let id = id?; - println!("{}", id); - } - Ok(()) -} - -fn push(revwalk: &mut Revwalk, id: Oid, hide: bool) -> Result<(), Error> { - if hide { - revwalk.hide(id) - } else { - revwalk.push(id) - } -} - -fn main() { - let args = Args::from_args(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/gnostr-rev-parse.rs b/src/bin/gnostr-rev-parse.rs deleted file mode 100755 index 296fa938e3..0000000000 --- a/src/bin/gnostr-rev-parse.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - * libgit2 "rev-parse" example - shows how to parse revspecs - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all - * copyright and related and neighboring rights to this software to the - * public domain worldwide. This software is distributed without any - * warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -#![deny(warnings)] - -use git2::Repository; -use structopt::StructOpt; - -#[derive(StructOpt)] -struct Args { - #[structopt(name = "spec")] - arg_spec: String, - #[structopt(name = "dir", long = "git-dir")] - /// directory of the git repository to check - flag_git_dir: Option, -} - -fn run(args: &Args) -> Result<(), git2::Error> { - let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); - let repo = Repository::open(path)?; - - let revspec = repo.revparse(&args.arg_spec)?; - - if revspec.mode().contains(git2::RevparseMode::SINGLE) { - println!("{}", revspec.from().unwrap().id()); - } else if revspec.mode().contains(git2::RevparseMode::RANGE) { - let to = revspec.to().unwrap(); - let from = revspec.from().unwrap(); - println!("{}", to.id()); - - if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { - let base = repo.merge_base(from.id(), to.id())?; - println!("{}", base); - } - - println!("^{}", from.id()); - } else { - return Err(git2::Error::from_str("invalid results from revparse")); - } - Ok(()) -} - -fn main() { - let args = Args::from_args(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/gnostr-sha256.rs b/src/bin/gnostr-sha256.rs index b864f98e65..fc88fb50e2 100755 --- a/src/bin/gnostr-sha256.rs +++ b/src/bin/gnostr-sha256.rs @@ -7,8 +7,7 @@ use gnostr::utils::strip_trailing_newline; #[allow(unused_imports)] use gnostr::Config; use std::io::Result; -use std::path::PathBuf; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::SystemTime; use std::{env, process}; fn main() -> Result<()> { diff --git a/src/bin/gnostr-status.rs b/src/bin/gnostr-status.rs deleted file mode 100755 index 8eb9e960c0..0000000000 --- a/src/bin/gnostr-status.rs +++ /dev/null @@ -1,439 +0,0 @@ -/* - * libgit2 "status" example - shows how to use the status APIs - * - * Written by the libgit2 contributors - * - * To the extent possible under law, the author(s) have dedicated all - * copyright and related and neighboring rights to this software to the - * public domain worldwide. This software is distributed without any - * warranty. - * - * You should have received a copy of the CC0 Public Domain Dedication along - * with this software. If not, see - * . - */ - -#![deny(warnings)] - -use std::str; -use std::time::Duration; - -use git2::{Error, ErrorCode, Repository, StatusOptions, SubmoduleIgnore}; -use structopt::StructOpt; - -#[derive(StructOpt)] -struct Args { - arg_spec: Vec, - #[structopt(name = "long", long)] - /// show longer statuses (default) - _flag_long: bool, - /// show short statuses - #[structopt(name = "short", long)] - flag_short: bool, - #[structopt(name = "porcelain", long)] - /// ?? - flag_porcelain: bool, - #[structopt(name = "branch", short, long)] - /// show branch information - flag_branch: bool, - #[structopt(name = "z", short)] - /// ?? - flag_z: bool, - #[structopt(name = "ignored", long)] - /// show ignored files as well - flag_ignored: bool, - #[structopt(name = "opt-modules", long = "untracked-files")] - /// setting for showing untracked files \[no|normal|all\] - flag_untracked_files: Option, - #[structopt(name = "opt-files", long = "ignore-submodules")] - /// setting for ignoring submodules \[all\] - flag_ignore_submodules: Option, - #[structopt(name = "dir", long = "git-dir")] - /// git directory to analyze - flag_git_dir: Option, - #[structopt(name = "repeat", long)] - /// repeatedly show status, sleeping inbetween - flag_repeat: bool, - #[structopt(name = "list-submodules", long)] - /// show submodules - flag_list_submodules: bool, -} - -#[derive(Eq, PartialEq)] -enum Format { - Long, - Short, - Porcelain, -} - -fn run(args: &Args) -> Result<(), Error> { - let path = args.flag_git_dir.clone().unwrap_or_else(|| ".".to_string()); - let repo = Repository::open(&path)?; - if repo.is_bare() { - return Err(Error::from_str("cannot report status on bare repository")); - } - - let mut opts = StatusOptions::new(); - opts.include_ignored(args.flag_ignored); - match args.flag_untracked_files.as_ref().map(|s| &s[..]) { - Some("no") => { - opts.include_untracked(false); - } - Some("normal") => { - opts.include_untracked(true); - } - Some("all") => { - opts.include_untracked(true).recurse_untracked_dirs(true); - } - Some(_) => return Err(Error::from_str("invalid untracked-files value")), - None => {} - } - match args.flag_ignore_submodules.as_ref().map(|s| &s[..]) { - Some("all") => { - opts.exclude_submodules(true); - } - Some(_) => return Err(Error::from_str("invalid ignore-submodules value")), - None => {} - } - opts.include_untracked(!args.flag_ignored); - for spec in &args.arg_spec { - opts.pathspec(spec); - } - - loop { - if args.flag_repeat { - println!("\u{1b}[H\u{1b}[2J"); - } - - let statuses = repo.statuses(Some(&mut opts))?; - - if args.flag_branch { - show_branch(&repo, &args.format())?; - } - if args.flag_list_submodules { - print_submodules(&repo)?; - } - - if args.format() == Format::Long { - print_long(&statuses); - } else { - print_short(&repo, &statuses); - } - - if args.flag_repeat { - std::thread::sleep(Duration::new(10, 0)); - } else { - return Ok(()); - } - } -} - -fn show_branch(repo: &Repository, format: &Format) -> Result<(), Error> { - let head = match repo.head() { - Ok(head) => Some(head), - Err(ref e) if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound => { - None - } - Err(e) => return Err(e), - }; - let head = head.as_ref().and_then(|h| h.shorthand()); - - if format == &Format::Long { - println!( - "# On branch {}", - head.unwrap_or("Not currently on any branch") - ); - } else { - println!("## {}", head.unwrap_or("HEAD (no branch)")); - } - Ok(()) -} - -fn print_submodules(repo: &Repository) -> Result<(), Error> { - let modules = repo.submodules()?; - println!("# Submodules"); - for sm in &modules { - println!( - "# - submodule '{}' at {}", - sm.name().unwrap(), - sm.path().display() - ); - } - Ok(()) -} - -// This function print out an output similar to git's status command in long -// form, including the command-line hints. -fn print_long(statuses: &git2::Statuses) { - let mut header = false; - let mut rm_in_workdir = false; - let mut changes_in_index = false; - let mut changed_in_workdir = false; - - // Print index changes - for entry in statuses - .iter() - .filter(|e| e.status() != git2::Status::CURRENT) - { - if entry.status().contains(git2::Status::WT_DELETED) { - rm_in_workdir = true; - } - let istatus = match entry.status() { - s if s.contains(git2::Status::INDEX_NEW) => "new file: ", - s if s.contains(git2::Status::INDEX_MODIFIED) => "modified: ", - s if s.contains(git2::Status::INDEX_DELETED) => "deleted: ", - s if s.contains(git2::Status::INDEX_RENAMED) => "renamed: ", - s if s.contains(git2::Status::INDEX_TYPECHANGE) => "typechange:", - _ => continue, - }; - if !header { - println!( - "\ -# Changes to be committed: -# (use \"git reset HEAD ...\" to unstage) -#" - ); - header = true; - } - - let old_path = entry.head_to_index().unwrap().old_file().path(); - let new_path = entry.head_to_index().unwrap().new_file().path(); - match (old_path, new_path) { - (Some(old), Some(new)) if old != new => { - println!("#\t{} {} -> {}", istatus, old.display(), new.display()); - } - (old, new) => { - println!("#\t{} {}", istatus, old.or(new).unwrap().display()); - } - } - } - - if header { - changes_in_index = true; - println!("#"); - } - header = false; - - // Print workdir changes to tracked files - for entry in statuses.iter() { - // With `Status::OPT_INCLUDE_UNMODIFIED` (not used in this example) - // `index_to_workdir` may not be `None` even if there are no differences, - // in which case it will be a `Delta::Unmodified`. - if entry.status() == git2::Status::CURRENT || entry.index_to_workdir().is_none() { - continue; - } - - let istatus = match entry.status() { - s if s.contains(git2::Status::WT_MODIFIED) => "modified: ", - s if s.contains(git2::Status::WT_DELETED) => "deleted: ", - s if s.contains(git2::Status::WT_RENAMED) => "renamed: ", - s if s.contains(git2::Status::WT_TYPECHANGE) => "typechange:", - _ => continue, - }; - - if !header { - println!( - "\ -# Changes not staged for commit: -# (use \"git add{} ...\" to update what will be committed) -# (use \"git checkout -- ...\" to discard changes in working directory) -#", - if rm_in_workdir { "/rm" } else { "" } - ); - header = true; - } - - let old_path = entry.index_to_workdir().unwrap().old_file().path(); - let new_path = entry.index_to_workdir().unwrap().new_file().path(); - match (old_path, new_path) { - (Some(old), Some(new)) if old != new => { - println!("#\t{} {} -> {}", istatus, old.display(), new.display()); - } - (old, new) => { - println!("#\t{} {}", istatus, old.or(new).unwrap().display()); - } - } - } - - if header { - changed_in_workdir = true; - println!("#"); - } - header = false; - - // Print untracked files - for entry in statuses - .iter() - .filter(|e| e.status() == git2::Status::WT_NEW) - { - if !header { - println!( - "\ -# Untracked files -# (use \"git add ...\" to include in what will be committed) -#" - ); - header = true; - } - let file = entry.index_to_workdir().unwrap().old_file().path().unwrap(); - println!("#\t{}", file.display()); - } - header = false; - - // Print ignored files - for entry in statuses - .iter() - .filter(|e| e.status() == git2::Status::IGNORED) - { - if !header { - println!( - "\ -# Ignored files -# (use \"git add -f ...\" to include in what will be committed) -#" - ); - header = true; - } - let file = entry.index_to_workdir().unwrap().old_file().path().unwrap(); - println!("#\t{}", file.display()); - } - - if !changes_in_index && changed_in_workdir { - println!("no changes added to commit (use \"git add\" and/or \"git commit -a\")"); - } -} - -// This version of the output prefixes each path with two status columns and -// shows submodule status information. -fn print_short(repo: &Repository, statuses: &git2::Statuses) { - for entry in statuses - .iter() - .filter(|e| e.status() != git2::Status::CURRENT) - { - let mut istatus = match entry.status() { - s if s.contains(git2::Status::INDEX_NEW) => 'A', - s if s.contains(git2::Status::INDEX_MODIFIED) => 'M', - s if s.contains(git2::Status::INDEX_DELETED) => 'D', - s if s.contains(git2::Status::INDEX_RENAMED) => 'R', - s if s.contains(git2::Status::INDEX_TYPECHANGE) => 'T', - _ => ' ', - }; - let mut wstatus = match entry.status() { - s if s.contains(git2::Status::WT_NEW) => { - if istatus == ' ' { - istatus = '?'; - } - '?' - } - s if s.contains(git2::Status::WT_MODIFIED) => 'M', - s if s.contains(git2::Status::WT_DELETED) => 'D', - s if s.contains(git2::Status::WT_RENAMED) => 'R', - s if s.contains(git2::Status::WT_TYPECHANGE) => 'T', - _ => ' ', - }; - - if entry.status().contains(git2::Status::IGNORED) { - istatus = '!'; - wstatus = '!'; - } - if istatus == '?' && wstatus == '?' { - continue; - } - let mut extra = ""; - - // A commit in a tree is how submodules are stored, so let's go take a - // look at its status. - // - // TODO: check for GIT_FILEMODE_COMMIT - let status = entry.index_to_workdir().and_then(|diff| { - let ignore = SubmoduleIgnore::Unspecified; - diff.new_file() - .path_bytes() - .and_then(|s| str::from_utf8(s).ok()) - .and_then(|name| repo.submodule_status(name, ignore).ok()) - }); - if let Some(status) = status { - if status.contains(git2::SubmoduleStatus::WD_MODIFIED) { - extra = " (new commits)"; - } else if status.contains(git2::SubmoduleStatus::WD_INDEX_MODIFIED) - || status.contains(git2::SubmoduleStatus::WD_WD_MODIFIED) - { - extra = " (modified content)"; - } else if status.contains(git2::SubmoduleStatus::WD_UNTRACKED) { - extra = " (untracked content)"; - } - } - - let (mut a, mut b, mut c) = (None, None, None); - if let Some(diff) = entry.head_to_index() { - a = diff.old_file().path(); - b = diff.new_file().path(); - } - if let Some(diff) = entry.index_to_workdir() { - a = a.or_else(|| diff.old_file().path()); - b = b.or_else(|| diff.old_file().path()); - c = diff.new_file().path(); - } - - match (istatus, wstatus) { - ('R', 'R') => println!( - "RR {} {} {}{}", - a.unwrap().display(), - b.unwrap().display(), - c.unwrap().display(), - extra - ), - ('R', w) => println!( - "R{} {} {}{}", - w, - a.unwrap().display(), - b.unwrap().display(), - extra - ), - (i, 'R') => println!( - "{}R {} {}{}", - i, - a.unwrap().display(), - c.unwrap().display(), - extra - ), - (i, w) => println!("{}{} {}{}", i, w, a.unwrap().display(), extra), - } - } - - for entry in statuses - .iter() - .filter(|e| e.status() == git2::Status::WT_NEW) - { - println!( - "?? {}", - entry - .index_to_workdir() - .unwrap() - .old_file() - .path() - .unwrap() - .display() - ); - } -} - -impl Args { - fn format(&self) -> Format { - if self.flag_short { - Format::Short - } else if self.flag_porcelain || self.flag_z { - Format::Porcelain - } else { - Format::Long - } - } -} - -fn main() { - let args = Args::from_args(); - match run(&args) { - Ok(()) => {} - Err(e) => println!("error: {}", e), - } -} diff --git a/src/bin/gnostr-weeble.rs b/src/bin/gnostr-weeble.rs index 027079a16f..6b818190bf 100755 --- a/src/bin/gnostr-weeble.rs +++ b/src/bin/gnostr-weeble.rs @@ -123,9 +123,7 @@ // in a decentrailized version control proposal known as 0x20bf. //! gnostr-weeble -//! -//! async reqwest to -use futures::executor::block_on; +use gnostr::weeble::{/*weeble, */ weeble_millis_sync, weeble_sync}; use std::env; /// /// weeble = (std::time::SystemTime::UNIX_EPOCH (seconds) / bitcoin-blockheight) @@ -133,54 +131,8 @@ use std::env; /// Weebles wobble, but they don't fall down /// /// -/// async fn print_weeble() -/// -/// let weeble = gnostr::get_weeble(); -/// -/// print!("{}",weeble.unwrap()); -pub async fn print_weeble(millis: bool) { - #[cfg(debug_assertions)] - let start = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("get millis error"); - #[cfg(debug_assertions)] - let seconds = start.as_secs(); - #[cfg(debug_assertions)] - let start_subsec_millis = start.subsec_millis() as u64; - #[cfg(debug_assertions)] - let start_millis = seconds * 1000 + start_subsec_millis; - #[cfg(debug_assertions)] - println!("start_millis: {}", start_millis); - - if millis { - let weeble = gnostr::get_weeble_millis(); - print!("{}", weeble.unwrap()); - } else { - let weeble = gnostr::get_weeble(); - print!("{}", weeble.unwrap()); - } - - #[cfg(debug_assertions)] - let stop = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("get millis error"); - #[cfg(debug_assertions)] - let seconds = stop.as_secs(); - #[cfg(debug_assertions)] - let stop_subsec_millis = stop.subsec_millis() as u64; - #[cfg(debug_assertions)] - let stop_millis = seconds * 1000 + stop_subsec_millis; - #[cfg(debug_assertions)] - println!("\nstop_millis: {}", stop_millis); - #[cfg(debug_assertions)] - println!("\ndelta_millis: {}", stop_millis - start_millis); -} /// fn main() -/// -///let future = print_weeble(); -/// -/// futures::executor::block_on(future); fn main() { let mut args = env::args(); let _ = args.next(); // program name @@ -189,16 +141,75 @@ fn main() { None => "false".to_string(), // Default value if no argument is provided }; if millis.eq_ignore_ascii_case("true") || millis.eq_ignore_ascii_case("millis") { - let future = print_weeble(true); - block_on(future); + print!("{}", weeble_millis_sync().unwrap().to_string()); } else { - let future = print_weeble(false); - block_on(future); + print!("{}", weeble_sync().unwrap().to_string()); } } -/// cargo test --bin gnostr-weeble -- --nocapture -#[test] -fn gnostr_weeble() { - //let future = print_weeble(); // Nothing is printed - //block_on(future); + +#[cfg(test)] +mod tests { + use super::*; + use gnostr::get_weeble_async; + use gnostr::get_weeble_sync; + use gnostr::global_rt::global_rt; + use gnostr::weeble::{weeble, weeble_sync}; + /// cargo test --bin gnostr-weeble -- --nocapture + #[test] + fn gnostr_weeble() { + print!("\nweeble:{}\n", weeble().unwrap().to_string()); + print!("\nweeble_sync:{}\n", weeble_sync().unwrap().to_string()); + print!( + "\nweeble_millis_sync:{}\n", + weeble_millis_sync().unwrap().to_string() + ); + } + + #[test] + fn test_weeble_global_rt() { + let rt1 = global_rt(); + let rt2 = global_rt(); + + // Ensure that the same runtime is returned each time. + assert!(std::ptr::eq(rt1, rt2)); + + // Ensure the runtime is functional by spawning a simple task. + rt1.block_on(async { + let _ = tokio::spawn(async { + println!("gnostr-weeble:main test begin..."); + main(); + println!("\ngnostr-weeble:main test end..."); + }) + .await + .unwrap(); + }); + // Ensure the runtime is functional by spawning a simple task. + rt2.block_on(async { + let _ = tokio::spawn(async { + println!("gnostr-weeble:main test begin..."); + main(); + println!("\ngnostr-weeble:main test end..."); + }) + .await + .unwrap(); + }); + rt1.block_on(async { + let _ = tokio::spawn(async { + println!("gnostr-weeble:main test begin..."); + let _ = get_weeble_async().await; + println!("\ngnostr-weeble:main test end..."); + }) + .await + .unwrap(); + }); + rt1.block_on(async { + let _ = tokio::spawn(async { + println!("gnostr-weeble:main test begin..."); + let _ = get_weeble_sync(); + println!("\ngnostr-weeble:main test end..."); + }) + .await + .unwrap(); + }); + } } diff --git a/src/bin/gnostr-wobble.rs b/src/bin/gnostr-wobble.rs index a95df33c02..ceae6aaaf7 100755 --- a/src/bin/gnostr-wobble.rs +++ b/src/bin/gnostr-wobble.rs @@ -123,64 +123,16 @@ // in a decentrailized version control proposal known as 0x20bf. //! gnostr-wobble -//! -//! async reqwest to -use futures::executor::block_on; +use gnostr::wobble::{/*wobble, */ wobble_millis_sync, wobble_sync}; use std::env; /// -/// weeble = (std::time::SystemTime::UNIX_EPOCH (seconds) / bitcoin-blockheight) +/// wobble = (std::time::SystemTime::UNIX_EPOCH (seconds) / bitcoin-blockheight) /// -/// Weebles wobble, but they don't fall down -/// +/// wobbles wobble, but they don't fall down +/// /// -/// async fn print_wobble() -/// -/// let wobble = gnostr::get_wobble(); -/// -/// print!("{}",wobble.unwrap()); -pub async fn print_wobble(millis: bool) { - #[cfg(debug_assertions)] - let start = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("get millis error"); - #[cfg(debug_assertions)] - let seconds = start.as_secs(); - #[cfg(debug_assertions)] - let start_subsec_millis = start.subsec_millis() as u64; - #[cfg(debug_assertions)] - let start_millis = seconds * 1000 + start_subsec_millis; - #[cfg(debug_assertions)] - println!("start_millis: {}", start_millis); - - if millis { - let wobble = gnostr::get_wobble_millis(); - print!("{}", wobble.unwrap()); - } else { - let wobble = gnostr::get_wobble(); - print!("{}", wobble.unwrap()); - } - - #[cfg(debug_assertions)] - let stop = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("get millis error"); - #[cfg(debug_assertions)] - let seconds = stop.as_secs(); - #[cfg(debug_assertions)] - let stop_subsec_millis = stop.subsec_millis() as u64; - #[cfg(debug_assertions)] - let stop_millis = seconds * 1000 + stop_subsec_millis; - #[cfg(debug_assertions)] - println!("\nstop_millis: {}", stop_millis); - #[cfg(debug_assertions)] - println!("\ndelta_millis: {}", stop_millis - start_millis); -} /// fn main() -/// -///let future = print_wobble(); -/// -/// futures::executor::block_on(future); fn main() { let mut args = env::args(); let _ = args.next(); // program name @@ -189,16 +141,75 @@ fn main() { None => "false".to_string(), // Default value if no argument is provided }; if millis.eq_ignore_ascii_case("true") || millis.eq_ignore_ascii_case("millis") { - let future = print_wobble(true); - block_on(future); + print!("{}", wobble_millis_sync().unwrap().to_string()); } else { - let future = print_wobble(false); - block_on(future); + print!("{}", wobble_sync().unwrap().to_string()); } } -/// cargo test --bin gnostr-wobble -- --nocapture -#[test] -fn gnostr_wobble() { - //let future = print_wobble(); // Nothing is printed - //block_on(future); + +#[cfg(test)] +mod tests { + use super::*; + use gnostr::get_wobble_async; + use gnostr::get_wobble_sync; + use gnostr::global_rt::global_rt; + use gnostr::wobble::{wobble, wobble_sync}; + /// cargo test --bin gnostr-wobble -- --nocapture + #[test] + fn gnostr_wobble() { + print!("\nwobble:{}\n", wobble().unwrap().to_string()); + print!("\nwobble_sync:{}\n", wobble_sync().unwrap().to_string()); + print!( + "\nwobble_millis_sync:{}\n", + wobble_millis_sync().unwrap().to_string() + ); + } + + #[test] + fn test_wobble_global_rt() { + let rt1 = global_rt(); + let rt2 = global_rt(); + + // Ensure that the same runtime is returned each time. + assert!(std::ptr::eq(rt1, rt2)); + + // Ensure the runtime is functional by spawning a simple task. + rt1.block_on(async { + let _ = tokio::spawn(async { + println!("gnostr-wobble:main test begin..."); + main(); + println!("\ngnostr-wobble:main test end..."); + }) + .await + .unwrap(); + }); + // Ensure the runtime is functional by spawning a simple task. + rt2.block_on(async { + let _ = tokio::spawn(async { + println!("gnostr-wobble:main test begin..."); + main(); + println!("\ngnostr-wobble:main test end..."); + }) + .await + .unwrap(); + }); + rt1.block_on(async { + let _ = tokio::spawn(async { + println!("gnostr-wobble:main test begin..."); + let _ = get_wobble_async().await; + println!("\ngnostr-wobble:main test end..."); + }) + .await + .unwrap(); + }); + rt1.block_on(async { + let _ = tokio::spawn(async { + println!("gnostr-wobble:main test begin..."); + let _ = get_wobble_sync(); + println!("\ngnostr-wobble:main test end..."); + }) + .await + .unwrap(); + }); + } } diff --git a/src/bin/mempool._rs b/src/bin/mempool._rs deleted file mode 100644 index cc226a6951..0000000000 --- a/src/bin/mempool._rs +++ /dev/null @@ -1,439 +0,0 @@ -use std::process; -extern crate serde_json; -use serde_json::{json, Value}; - -fn print_version() { - const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); - println!("v{}", VERSION.unwrap_or("unknown")); - process::exit(0); -} -fn print_usage(code: i32) { - println!("\n Usage:\n"); - println!("\tsign_message - print \n"); - println!("\tsign_message - print signature of "); - if code == 24 { - println!("\t ^private_key must be hexadecimal characters."); - } - if code == 64 { - println!("\t ^private_key must be 64 characters long."); - } - println!("\n Example:\n"); - println!( - "\tsign_message 0000000000000000000000000000000000000000000000000000000000000001 \"\"" - ); - if code == 999 { - println!("\t private_key must be greater than zero^"); - } - println!(" Expected:\n"); - println!( - "\t3044022077c8d336572f6f466055b5f70f433851f8f535f6c4fc71133a6cfd71079d03b702200ed9f5eb8aa5b266abac35d416c3207e7a538bf5f37649727d7a9823b1069577\n" - ); - - if code == 0 { - process::exit(code); - } - if code == 64 { - process::exit(code); - } - - process::exit(0); -} - -fn is_string_of_length_64(string: &str) -> bool { - return string.len() == 64; -} - -fn is_hex(text: &str) -> bool { - use regex::Regex; - let re = Regex::new(r"^[0-9a-fA-F]+$").unwrap(); - re.is_match(text) -} - -//GLOBAL VARIABLES -//static GLOBAL_VAR_BOOL: &bool = &false; -//static GLOBAL_VAR_STRING: &str = "GLOBAL_VAR_STRING"; -//END GLOBAL VARIABLES - -fn main() -> Result<(), String> { - let mut _verbose = false; - use secp256k1::{Keypair, Scalar, Secp256k1, SecretKey, XOnlyPublicKey}; - use std::env; - use std::str::FromStr; - let secp = Secp256k1::new(); - let tweak = secp256k1::Scalar::random(); - - let args: Vec = env::args().collect(); - let _app_name = &args[0]; - let mut message_str = String::new(); - let mut private_key = - SecretKey::from_str("0000000000000000000000000000000000000000000000000000000000000001") - .unwrap(); - // Create the JSON array - let mut json_array = Vec::new(); - //let _num_args = args.len(); - //#[cfg(debug_assertions)] - //println!("_num_args - 1 = {}", _num_args - 1); - if env::args().len() == 1 { - print_usage(0); - } - - if env::args().len() > 1 { - //begin handle args - //begin handle args - //begin handle args - - // - // - //capture first arg assuming is private_key - // - // - - //mutable to clobber very soon! - let mut _private_key_arg = String::with_capacity(1024); - _private_key_arg = std::env::args() - .nth(1) - .expect("Missing private key argument"); - - // - //0000000000000000000000000000000000000000000000000000000000000000 - if &_private_key_arg == "0000000000000000000000000000000000000000000000000000000000000000" { - //TODO:use as special case - print_usage(999); - } - if &_private_key_arg == "-vv" || &_private_key_arg == "--verbose" { - _verbose = true; - println!("verbose={}", _verbose) - } - if &_private_key_arg == "-h" || &_private_key_arg == "--help" { - print_usage(0); - } - if &_private_key_arg == "-v" - || &_private_key_arg == "--version" - || &_private_key_arg == "-V" - { - print_version(); - } - - if is_string_of_length_64(&_private_key_arg) { - } else { - print_usage(64); - } - if is_hex(&_private_key_arg) { - //private_key_arg isn't a private_key yet - //println!("&_private_key_arg={}", &_private_key_arg); - - //if args.len() > 1 == true AND is_hex == true - //we assume first (unflagged) arg is private_key - private_key = SecretKey::from_str(&_private_key_arg).unwrap(); - //println!("&private_key={}", &private_key.display_secret()); - - //////////////////////////////////////////////// - //////////////////////////////////////////////// - //////////////////////////////////////////////// - //////////////////////////////////////////////// - _private_key_arg = std::env::args().nth(0).expect("Clobbering first argument"); - _private_key_arg = String::new(); - //println!("&private_key_arg={}",&private_key_arg); - //println!("private_key_arg={:?}",&private_key_arg.clone().into_bytes()); - //println!( - // "_private_key_arg={:?}", - // _private_key_arg.clone().into_bytes() - //); - assert_eq!(_private_key_arg, ""); - //////////////////////////////////////////////// - //////////////////////////////////////////////// - //////////////////////////////////////////////// - //////////////////////////////////////////////// - } - if env::args().len() == 2 { - //once private_key captured - //we handle - //println!("env::args().len() == 2 {}", env::args().len() == 2); - let args = env::args(); - let remaining_args = args.skip(0).collect::>(); - //println!("\n{:?}\n", remaining_args); - //println!("\n{:?}\n", remaining_args[0]); - println!("\nhandle private_key conversions {:?}\n", remaining_args[1]); - // - //TODO: - //implement key conversions to bech32/nostr etc... - // - // - // - private_key = SecretKey::from_str( - "0000000000000000000000000000000000000000000000000000000000000001", - ) - .unwrap(); - process::exit(0); - } - if env::args().len() > 2 { - //check - let _sub_command = std::env::args() - .nth(2) - .expect("mempool "); - - //shell Command/duct - //use duct::cmd; - - //recapture arg array - let args = env::args(); - - //pop ../mempool off the top - let remaining_args = args.skip(1).collect::>(); - #[cfg(debug_assertions)] - println!("\n{:?}\n", remaining_args); - //println!("\n{:?}\n", format!("100:args={:?}", remaining_args)); - //println!("\n{ }\n", format!("101:args={:?}", remaining_args)); - - //message_str = format!("{:?}", remaining_args); - //message_str = format!("{}", remaining_args[0]); - //println!("message_str={}", message_str); - - //let args: Vec = env::args().collect(); - let mut count = 0; //skip args[0] - for arg in &remaining_args { - if arg == "--sec" || arg == "-s" || arg == "--secret" { - #[cfg(debug_assertions)] - println!("detected --sec:arg={}", arg); - #[cfg(debug_assertions)] - println!("count + 1 ={}", count + 1); - #[cfg(debug_assertions)] - println!( - "remaining_args[{}]={}", - count + 1, - remaining_args[count + 1] - ); - if is_hex(&remaining_args[count + 1]) { - private_key = SecretKey::from_str(&remaining_args[count + 1]).unwrap(); - #[cfg(debug_assertions)] - println!("private_key={}", private_key.display_secret()); - } else { - //TODO implement bip32/85 - //and or take sha256 of string - } - } - - #[cfg(debug_assertions)] - println!("\nremaining_args{:?}\n", remaining_args); - - if arg == "--message" || arg == "-m" || arg == "--msg" { - #[cfg(debug_assertions)] - println!("detected --message:arg={}", arg); - #[cfg(debug_assertions)] - println!("count + 1 ={}", count + 1); - #[cfg(debug_assertions)] - println!("args[{}]={}", count + 1, remaining_args[count + 1]); - if is_hex(&remaining_args[count + 1]) { - private_key = SecretKey::from_str(&remaining_args[count + 1]).unwrap(); - message_str = format!("{}", &remaining_args[count + 1].to_string()); - #[cfg(debug_assertions)] - println!("private_key={:?}", private_key); - } else { - message_str = format!("{}", &remaining_args[count + 1].to_string()); - #[cfg(debug_assertions)] - println!("message_str={:?}", message_str); - //TODO implement bip32/85 - //and or take sha256 of string - } - } - - #[cfg(debug_assertions)] - println!("\n{:?}\n", remaining_args); - //require private_key for all operations - //private_key MUST be first arg - //TODO - //detect - //--sec as second arg - //mempool --sec [args_array] - // - // - // - //println!("count={}", count); - // - // - // - //println!("117:arg={}", arg); - //println!("118:arg={}", format!("{:?}",arg)); - //println!("119:arg={:}", format!("{ }",arg)); - //println!("120:arg={:}", format!("{:?}",arg)); - //println!("121:arg={:?}", format!("{:?}",arg)); - // - //println!("sub_command={} {:?}",sub_command,env::args.0); - //println!("sub_command={} {:?}",sub_command,env::args.1); - //println!("124:args={:?}", args); - if count > 1 { - //add construct message_str - //println!("args[{}]={:?}", count, args[count]); - #[cfg(debug_assertions)] - println!("239:args[{}]={}", count, remaining_args[count]); - } - - count = count + 1; - //println!("count={}", count); - } - //}//end if_hex - } else { - print_usage(24); - } - //println!("249:private_key={}", &private_key.display_secret()); - - #[cfg(debug_assertions)] - //sign_message 0000000000000000000000000000000000000000000000000000000000000001 - assert_eq!( - "0000000000000000000000000000000000000000000000000000000000000001", - format!("{}", private_key.display_secret()) - ); - #[cfg(debug_assertions)] - println!( - "118:{{\"private_key\": {:}}}", - &private_key.display_secret() - ); - - let key_pair = Keypair::from_secret_key(&secp, &private_key); - let _pubkey_xo = XOnlyPublicKey::from_keypair(&key_pair); - let (pubkey_xo, _parity) = key_pair.x_only_public_key(); - let pubkey_xot = pubkey_xo - .add_tweak(&secp, &tweak) - .expect("Improbable to fail with a randomly generated tweak"); - - let public_xot_0_json = json!({ - "pubkey_xot_0": pubkey_xot.0.to_string() - }); - //println!( - // "143:{{\"public_xot.0\": \"{:}\"}}", - // pubkey_xot.0.to_string() - //); - let public_xot_1_json = json!({ - "pubkey_xot_1": format!("{:?}", pubkey_xot.1) - }); - //println!("144:{{\"public_xot.1\": \"{:?}\"}}", pubkey_xot.1); - - let (/*mut*/ x_public_key, _) = key_pair.x_only_public_key(); - let x_public_key_json = json!({ - "x_public_key": x_public_key.to_string() - }); - //println!("141:{{\"x_public_key\": \"{:}\"}}", x_public_key); - - let x_original = x_public_key; - let (tweaked, parity) = x_public_key - .add_tweak(&secp, &tweak) - .expect("Improbable to fail with a randomly generated tweak"); - assert!(x_original.tweak_add_check(&secp, &tweaked, parity, tweak)); - - //if env::args().len() == 2 { - // #[cfg(debug_assertions)] - // //println!("168:{{\"private_key\": {:}}}", &key_pair.display_secret()); - // println!("169:{{\"public_key\": \"{}\"}}", &key_pair.public_key()); - // process::exit(0); - //} - - use secp256k1::hashes::sha256; - use secp256k1::Message; - - #[cfg(debug_assertions)] - let empty_str: &'static str = ""; - #[cfg(debug_assertions)] - //println!("empty_str={}", empty_str); - #[cfg(debug_assertions)] - let message_hash = Message::from_hashed_data::(empty_str.as_bytes()); - #[cfg(debug_assertions)] - assert_eq!( - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - format!("{}", message_hash) - ); - - //sign_message 0000000000000000000000000000000000000000000000000000000000000005 "" - //let message_str = std::env::args().nth(2).expect("Missing message string"); - let message_str_json = json!({ - "message_str": message_str.to_string() - }); - //println!("164:{{\"message_str\": \"{}\"}}", message_str); - let message_hash = Message::from_hashed_data::(message_str.as_bytes()); - - //let message_hash_json = json!( - - //format!("{{\"message_hash\": \"{}\"}}", message_hash) - - //); - - let message_hash_json = json!({ - "message_hash": message_hash.to_string() - }); - - //println!("179:{{\"message_hash\": \"{}\"}}", message_hash); - - let sig = secp.sign_ecdsa(&message_hash, &private_key); - assert!(secp - .verify_ecdsa(&message_hash, &sig, &key_pair.public_key()) - .is_ok()); - - let sig_json = json!({ - "sig": sig.to_string() - }); - //// Define the data you want to store in the JSON object - //let object0 = json!({ - // "178_name": "John Doe", - // "179_age": 30, - // "180_city": "New York" - //}); - //// Serialize the data into a JSON string - //let json_string = serde_json::to_string(&object0).unwrap(); - - //// Print the JSON string - //println!("186:{}", json_string); - - //// Define the data for each object in the array - //let object1 = json!({ - // "190_name": "John Doe", - // "191_age": 30, - // "192_city": "New York" - //}); - - //let object2 = json!({ - // "196_name": "Jane Doe", - // "197_age": 25, - // "198_city": "Los Angeles" - //}); - - // Create the JSON array - //let mut json_array = Vec::new(); - //json_array.push(object0); - //public_xot_0_json - json_array.push(public_xot_0_json.clone()); - json_array.push(public_xot_1_json.clone()); - json_array.push(x_public_key_json.clone()); - json_array.push(message_str_json.clone()); - json_array.push(message_hash_json.clone()); - json_array.push(sig_json.clone()); - //json_array.push(object0.clone()); - //json_array.push(object1); - //json_array.push(object2); - - // Convert the Vec to a JSON Value - let json_value: Value = json!(json_array); - - // Print the JSON array - println!("{}", json_value); - - //println!("{{\"sig\": \"{}\"}}", sig); - } // end if env::args().len() > 1 - Ok(()) -} -// This code defines a function to add two numbers -pub fn add(a: i32, b: i32) -> i32 { - a + b -} - -// This code is only compiled when running tests -#[cfg(test)] -mod tests { - // Import the add function from the outer scope - use super::*; - - // This function is marked as a test with the `#[test]` attribute - #[test] - fn test_add() { - // This assertion checks if the sum of 1 and 2 is equal to 3 - assert_eq!(add(1, 2), 3); - } -} diff --git a/src/bin/nostr._rs b/src/bin/nostr._rs deleted file mode 100644 index b925f1595b..0000000000 --- a/src/bin/nostr._rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::iter::Peekable; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; -use std::{env, thread}; - -use gnostr_bins::events::extract_events_ws; -use gnostr_bins::nostr_client::Client; -use gnostr_bins::req::ReqFilter; -use gnostr_bins::utils::parse_content_tags; -use gnostr_bins::{Identity, Message}; -use structopt::StructOpt; - -fn handle_message(relay_url: &String, message: &Message) -> Result<(), String> { - println!("Received message from {}: {:?}", relay_url, message); - - let events = extract_events_ws(message); - println!("Events: {:?}", events); - - Ok(()) -} - -fn main() { - let secret_key = env::var("SECRET_KEY").unwrap_or_else(|_| { - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string() - //panic!("SECRET_KEY environment variable not set"); - }); - let content = env::var("CONTENT").unwrap_or_else(|_| "content".to_string()); - - //println!("{}", secret_key); - - let my_identity = Identity::from_str(&secret_key).unwrap(); - - let nostr_client = Arc::new(Mutex::new( - Client::new(vec!["wss://relay.damus.io"]).unwrap(), - )); - - // Run a new thread to handle messages - let nostr_clone = nostr_client.clone(); - let handle_thread = thread::spawn(move || { - // println!("Listening..."); - let events = nostr_clone.lock().unwrap().next_data().unwrap(); - - for (relay_url, message) in events.iter() { - handle_message(relay_url, message).unwrap(); - } - }); - - // Change metadata - nostr_client - .lock() - .unwrap() - .set_metadata( - &my_identity, - Some("gnostr"), - Some("gnostr unsecure account for testing."), - Some("https://avatars.githubusercontent.com/u/135379339?s=400&u=e38855df24087feb9a6679c5e3974816e6aa3753&v=4"), - None, - 0, - ) - .unwrap(); - - // Subscribe to my last text note - let subscription_id = nostr_client - .lock() - .unwrap() - .subscribe(vec![ReqFilter { - ids: None, - authors: Some(vec![ - "a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd".to_string(), - ]), - kinds: None, - e: None, - p: None, - since: None, - until: None, - limit: Some(1), - }]) - .unwrap(); - - // Unsubscribe - nostr_client - .lock() - .unwrap() - .unsubscribe(&subscription_id) - .unwrap(); - - // You can use the parse content tags method to get the content and the tags - // from a string let tags = parse_content_tags("hello #world", vec![], - // Some(nostr_rust::DEFAULT_HASHTAG), true, true); assert_eq!(tags.content, - // "hello #world"); assert_eq!(tags.tags, vec![vec!["t", "world"]]); - - // Publish a text note - nostr_client - .lock() - .unwrap() - .publish_text_note(&my_identity, &content.to_string(), &[], 0) - .unwrap(); - - // Publish a proof of work text note with a difficulty target of 15 - let pow_content = format!("event with pow:{}", content); - nostr_client - .lock() - .unwrap() - .publish_text_note(&my_identity, &pow_content, &[], 15) - .unwrap(); - - // Wait for the thread to finish - handle_thread.join().unwrap(); -} diff --git a/src/bin/server-toml.rs b/src/bin/server-toml.rs new file mode 100644 index 0000000000..87efcdcad7 --- /dev/null +++ b/src/bin/server-toml.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::io; + +#[derive(Serialize, Deserialize, Debug)] +struct Config { + name: String, + port: u16, + hostname: String, + users: HashMap, + welcome_message: WelcomeMessage, + extra: Extra, +} + +#[derive(Serialize, Deserialize, Debug)] +struct User { + #[serde(default)] // Use default value (false) if not specified in TOML + is_admin: bool, + can_create_repos: bool, + public_key: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct WelcomeMessage { + welcome_message: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Extra { + extra: String, +} + +fn main() -> io::Result<()> { + let mut users = HashMap::new(); + + users.insert( + "gnostr".to_string(), + User { + is_admin: true, + can_create_repos: true, + public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWBy03xeN9LL4ZwqAOVcdrDBv26JQXPoIdQ+ZzCaf6LsW4DNNZlSn5GQwBZ340zC9os098ArH2dz5Hbih2x6tAAKdNRraG/CCc8JYe5ogitbPZlMaWcoeJkMLEiaZhJ8ZKBiTVw8tRHxIEGuuEEKXspsicE2WA7vf/Xv5jSKYEO5KUriz+JeOHTDD5C65AFh8odKI5Yb+sYRXT3tAdRTyOEJLfAdLQLRITyZ57eEBH3Ikkcpk3Ixoc/CBFGB45AQsi3X61djiRkAULilvAdTPfvgk2If0ldbEzHdLiHcbkanhW//xwrZ4GU6hjGjviSOq+n3Qki/InNxdJmh2jr7nJ4mdevctvtD3YLyVU+Ku99Y83lyMWWZ2LlRYK3OxK0fc9d7xJQVl9f4kPG3C6ZUcJ1BZbl/mCKOqegrTLnaTLj3wx3+NQSjzw9unhkVmcf7dofL+zYf2GLCiDKQrgVX9f4ZQr7mWi53QDwrZm0BMxDvERj7qJmwAmb1nUkBP6aJU= randymcmillan@DeepSpaceMBPro.local".to_string(), + }, + ); + + users.insert( + "gnostr-user".to_string(), + User { + is_admin: false, // Explicitly set to false as it's not an admin + can_create_repos: true, + public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDaBogLsfsOkKIpZEZYa3Ee+wFaaxeJuHps05sH2rZLf+KEE6pWX5MT2iWMgP7ihmm6OqbAPkWoBUGEO5m+m/K1S0MgQXUvaTsTI0II3MDqJT/RXA6Z9c+ZIDROEAkNIDrfeU2n8hQXfMHbLw6ahedGUgZj2WYWfsrEg8Kzbfk3fn32sO/lMnNyz5hmavMBiNORGlIi2Qe2RjQEtcJHn89B7UtyEfnj87V+jZYcFf4nnNQigT2eQ3NlB1YzZS4Zk/OxQeYypclzYFaiYc7RZv2yxKVOy0KvEpldyUKeQ== randy.lee.mcmillan@gmail.com".to_string(), + }, + ); + + let config = Config { + name: "gnostr.org".to_string(), + port: 2222, + hostname: "gnostr.org".to_string(), + users, + welcome_message: WelcomeMessage { + welcome_message: "welcome to gnostr.org!".to_string(), + }, + extra: Extra { + extra: "extra toml content!".to_string(), + }, + }; + + let toml_string = toml::to_string_pretty(&config).expect("Failed to serialize config to TOML"); + + println!("Generated server.toml content:\n{}", toml_string); + + fs::write("server.toml", toml_string)?; + + println!("server.toml generated successfully!"); + + Ok(()) +} diff --git a/src/lib/app.rs b/src/lib/app.rs index fe70b9f35a..9c2b4a75fe 100644 --- a/src/lib/app.rs +++ b/src/lib/app.rs @@ -1,9 +1,13 @@ use std::{ cell::{Cell, RefCell}, + env, path::{Path, PathBuf}, rc::Rc, }; +use crate::blockheight::blockheight_sync; +use crate::weeble::weeble_sync; +use crate::wobble::wobble_sync; use anyhow::{bail, Result}; use crossbeam_channel::Sender; use crossterm::event::{Event, KeyEvent}; @@ -380,6 +384,10 @@ impl App { pub fn update_async(&mut self, ev: AsyncNotification) -> Result<()> { log::trace!("update_async: {:?}", ev); + env::set_var("WEEBLE", weeble_sync().unwrap().to_string()); + env::set_var("WOBBLE", wobble_sync().unwrap().to_string()); + log::debug!("WEEBLE: {:?}", env::var("WEEBLE")); + if let AsyncNotification::Git(ev) = ev { //chat_tab.update_git self.chat_tab.update_git(ev)?; diff --git a/src/lib/args.rs b/src/lib/args.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/src/lib/args.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/lib/blockhash.rs b/src/lib/blockhash.rs index e9ace7acb6..8b6a192008 100644 --- a/src/lib/blockhash.rs +++ b/src/lib/blockhash.rs @@ -1,24 +1,8 @@ -use std::io::Read; -use std::time::SystemTime; - +use crate::utils::{ureq_async, ureq_sync}; use reqwest::Url; - -pub fn check_curl() { - - //println!("check_curl"); -} +use std::io::Read; pub fn blockhash() -> Result { - let since_the_epoch = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("get millis error"); - let seconds = since_the_epoch.as_secs(); - let subsec_millis = since_the_epoch.subsec_millis() as u64; - let _now_millis = seconds * 1000 + subsec_millis; - //println!("now millis: {}", seconds * 1000 + subsec_millis); - - //let bh = get_blockheight(); - //println!("{}",bh.unwrap()); let url = Url::parse("https://mempool.space/api/blocks/tip/hash").unwrap(); let mut res = reqwest::blocking::get(url).unwrap(); @@ -26,3 +10,15 @@ pub fn blockhash() -> Result { res.read_to_string(&mut blockhash).unwrap(); Ok(blockhash) } + +pub async fn blockhash_async() -> String { + ureq_async("https://mempool.space/api/blocks/tip/hash".to_string()) + .await + .unwrap() + .to_string() +} +pub fn blockhash_sync() -> String { + ureq_sync("https://mempool.space/api/blocks/tip/hash".to_string()) + .unwrap() + .to_string() +} diff --git a/src/lib/blockheight.rs b/src/lib/blockheight.rs index be1072e663..f323fc4a94 100644 --- a/src/lib/blockheight.rs +++ b/src/lib/blockheight.rs @@ -1,9 +1,9 @@ use crate::utils::{ureq_async, ureq_sync}; +use reqwest::Url; +use std::env; use std::io::Read; use std::time::SystemTime; -use reqwest::Url; - pub fn check_curl() { //println!("check_curl"); @@ -32,17 +32,22 @@ pub fn blockheight() -> Result { //let blockheight = seconds as f64 / tmp_u64 as f64; let blockheight = tmp_u64 as f64; //return Ok(blockheight.floor()); + env::set_var("BLOCKHEIGHT", blockheight.to_string()); Ok(blockheight) } pub async fn blockheight_async() -> String { - ureq_async("https://mempool.space/api/blocks/tip/height".to_string()) + let blockheight = ureq_async("https://mempool.space/api/blocks/tip/height".to_string()) .await .unwrap() - .to_string() + .to_string(); + env::set_var("BLOCKHEIGHT", blockheight.clone().to_string()); + blockheight } pub fn blockheight_sync() -> String { - ureq_sync("https://mempool.space/api/blocks/tip/height".to_string()) + let blockheight = ureq_sync("https://mempool.space/api/blocks/tip/height".to_string()) .unwrap() - .to_string() + .to_string(); + env::set_var("BLOCKHEIGHT", blockheight.clone().to_string()); + blockheight } diff --git a/src/lib/chat/mod.rs b/src/lib/chat/mod.rs index 256cdf165e..f2ef661ace 100644 --- a/src/lib/chat/mod.rs +++ b/src/lib/chat/mod.rs @@ -25,9 +25,8 @@ use tui_input::Input; use crate::utils::parse_json; use crate::utils::split_json_string; -pub mod p2p; pub mod ui; -pub use p2p::evt_loop; +use crate::p2p::evt_loop; pub mod msg; pub use msg::*; @@ -287,7 +286,7 @@ pub fn global_rt() -> &'static tokio::runtime::Runtime { } pub fn chat(key: &String, sub_command_args: &ChatSubCommands) -> Result<(), Box> { - let mut args = sub_command_args.clone(); + let args = sub_command_args.clone(); let env_args: Vec = env::args().collect(); let level = if args.debug { LevelFilter::DEBUG diff --git a/src/lib/chat/msg.rs b/src/lib/chat/msg.rs index c9b5118f46..a9069d2abf 100644 --- a/src/lib/chat/msg.rs +++ b/src/lib/chat/msg.rs @@ -1,4 +1,4 @@ -use crate::{blockheight::blockheight_sync, get_weeble, get_wobble, VERSION}; +use crate::{blockheight::blockheight_sync, VERSION}; use std::fmt::Display; use once_cell::sync::Lazy; @@ -92,7 +92,7 @@ impl Msg { self } - pub fn wrap_text(mut self, text: Msg, max_width: usize) -> Self { + pub fn wrap_text(self, text: Msg, max_width: usize) -> Self { // for line in text.content.bytes() { // line diff --git a/src/lib/chat/ui.rs b/src/lib/chat/ui.rs index a8da5b6688..345332fcbb 100644 --- a/src/lib/chat/ui.rs +++ b/src/lib/chat/ui.rs @@ -1,3 +1,5 @@ +use crate::blockheight::blockheight_sync; +use crate::chat::msg; use ratatui::{ backend::{Backend, CrosstermBackend}, crossterm::{ @@ -6,13 +8,12 @@ use ratatui::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }, layout::{Constraint, Direction, Layout}, - style::Color, + style::{Color, Style}, text::Line, widgets::{Block, Borders, List, ListItem, Paragraph}, Frame, Terminal, }; -use ratatui::style::Style; use std::{ error::Error, io, @@ -22,8 +23,6 @@ use std::{ use tui_input::backend::crossterm::EventHandler; use tui_input::Input; -use crate::chat::msg; - #[derive(Default)] pub enum InputMode { #[default] @@ -267,8 +266,14 @@ fn run_app(terminal: &mut Terminal, app: &mut App) -> io::Result< } } else { //TODO refresh and query topic nostr DMs - let m = msg::Msg::default() - .set_content("test message ".to_string(), 0 as usize); + let m = msg::Msg::default().set_content( + format!( + "{}:{}", + &blockheight_sync(), + "test message ".to_string() + ), + 0 as usize, + ); app.add_message(m.clone()); if let Some(ref mut hook) = app._on_input_enter { hook(m); @@ -280,8 +285,14 @@ fn run_app(terminal: &mut Terminal, app: &mut App) -> io::Result< app.msgs_scroll = usize::MAX; app.msgs_scroll = usize::MAX; app.input.reset(); - let m = msg::Msg::default() - .set_content("test message ".to_string(), 0 as usize); + let m = msg::Msg::default().set_content( + format!( + "{}:{}", + &blockheight_sync(), + "".to_string() + ), + 0 as usize, + ); app.add_message(m.clone()); if let Some(ref mut hook) = app._on_input_enter { hook(m); diff --git a/src/lib/cli.rs b/src/lib/cli.rs index 02db63da16..9cdddcd9f8 100644 --- a/src/lib/cli.rs +++ b/src/lib/cli.rs @@ -22,6 +22,48 @@ pub struct CliArgs { pub notify_watcher: bool, } +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct LegitCli { + #[command(subcommand)] + pub command: LegitCommands, + /// remote signer address + #[arg(long, global = true)] + pub bunker_uri: Option, + /// remote signer app secret key + #[arg(long, global = true)] + pub bunker_app_key: Option, + /// nsec or hex private key + #[arg(short, long, global = true)] + pub nsec: Option, + /// password to decrypt nsec + #[arg(short, long, global = true)] + pub password: Option, + /// disable spinner animations + #[arg(long, action, default_value = "false")] + pub disable_cli_spinners: Option, +} + +#[derive(Subcommand, Debug)] +pub enum LegitCommands { + /// update cache with latest updates from nostr + Fetch(sub_commands::fetch::SubCommandArgs), + /// signal you are this repo's maintainer accepting proposals via + /// nostr + Init(sub_commands::init::SubCommandArgs), + /// issue commits as a proposal + Send(sub_commands::send::SubCommandArgs), + /// list proposals; checkout, apply or download selected + List, + /// send proposal revision + Push(sub_commands::push::SubCommandArgs), + /// fetch and apply new proposal commits / revisions linked to + /// branch + Pull, + /// run with --nsec flag to change npub + Login(sub_commands::login::SubCommandArgs), +} #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] #[command(propagate_version = true)] @@ -137,6 +179,8 @@ pub enum GnostrCommands { Tui(crate::gnostr::GnostrSubCommands), /// Chat sub commands Chat(crate::chat::ChatSubCommands), + /// Legit sub commands + Legit(legit::LegitSubCommand), /// Ngit sub commands Ngit(ngit::NgitSubCommand), /// Set metadata. diff --git a/src/lib/client.rs b/src/lib/client.rs index 0969fbe65a..96fe2f2629 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -26,6 +26,7 @@ use futures::{ future::join_all, stream::{self, StreamExt}, }; +use gnostr_crawler::processor::BOOTSTRAP_RELAYS; use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressState, ProgressStyle}; #[cfg(test)] use mockall::*; @@ -107,31 +108,20 @@ impl Connect for Client { "ws://localhost:8052".to_string(), ] } else { - //TODO relay crawler - vec![ - "wss://relay.damus.io".to_string(), /* free, good reliability, have been known - * to delete all messages */ - "wss://nos.lol".to_string(), - "wss://cfrelay.haorendashu.workers.dev".to_string(), - ] + BOOTSTRAP_RELAYS.to_vec() }; - let more_fallback_relays: Vec = if std::env::var("NGITTEST").is_ok() { + let mut more_fallback_relays: Vec = if std::env::var("NGITTEST").is_ok() { vec![ "ws://localhost:8055".to_string(), "ws://localhost:8056".to_string(), ] } else { - vec![ - "wss://purplerelay.com".to_string(), /* free but reliability not tested */ - "wss://purplepages.es".to_string(), /* for profile - * events but - * unreliable */ - "wss://relayable.org".to_string(), /* free but not - * always reliable */ - ] + BOOTSTRAP_RELAYS.to_vec() }; + more_fallback_relays.push("wss://relayable.org".to_string()); + let blaster_relays: Vec = if std::env::var("NGITTEST").is_ok() { vec!["ws://localhost:8057".to_string()] } else { @@ -651,8 +641,14 @@ fn get_dedup_events(relay_results: Vec>>) -> Vec dedup_events } -pub async fn sign_event(event_builder: EventBuilder, signer: &NostrSigner) -> Result { - if signer.r#type().eq(&nostr_signer_0_34_0::NostrSignerType::NIP46) { +pub async fn sign_event( + event_builder: EventBuilder, + signer: &NostrSigner, +) -> Result { + if signer + .r#type() + .eq(&nostr_signer_0_34_0::NostrSignerType::NIP46) + { let term = console::Term::stderr(); term.write_line("signing event with remote signer...")?; let event = signer @@ -769,7 +765,10 @@ pub async fn get_event_from_global_cache( .context("cannot execute query on opened ngit nostr cache database") } -pub async fn save_event_in_cache(git_repo_path: &Path, event: &nostr_0_34_1::Event) -> Result { +pub async fn save_event_in_cache( + git_repo_path: &Path, + event: &nostr_0_34_1::Event, +) -> Result { get_local_cache_database(git_repo_path) .await? .save_event(event) diff --git a/src/lib/components/chat_details/details.rs b/src/lib/components/chat_details/details.rs index 74d3be386a..b389588bce 100644 --- a/src/lib/components/chat_details/details.rs +++ b/src/lib/components/chat_details/details.rs @@ -14,7 +14,6 @@ use sync::CommitTags; use super::style::Detail; use crate::{ app::Environment, - chat, components::{ chat_details::style::style_detail, dialog_paragraph, diff --git a/src/lib/components/chat_details/mod.rs b/src/lib/components/chat_details/mod.rs index a64ba21be5..ccae3f3e67 100644 --- a/src/lib/components/chat_details/mod.rs +++ b/src/lib/components/chat_details/mod.rs @@ -8,28 +8,20 @@ use super::{ use crate::{ accessors, app::Environment, - chat, keys::{key_match, SharedKeyConfig}, strings, }; use anyhow::Result; use chat_details::CompareDetailsComponent; -use crossterm::event::Event as CrossTermEvent; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use details::DetailsComponent; use gnostr_asyncgit::{ sync::{commit_files::OldNew, CommitTags}, AsyncCommitFiles, CommitFilesParams, }; -use nostr_0_34_1::prelude::Tag; -use nostr_database_0_34_0::{nostr::event::Event, nostr::types::filter::Filter, NostrDatabase, Order}; -use nostr_sqlite_0_34_0::SQLiteDatabase; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, Frame, }; -use std::time::Duration; -use tracing_subscriber::fmt::format::FmtSpan; pub struct ChatDetailsComponent { commit: Option, diff --git a/src/lib/components/topiclist.rs b/src/lib/components/topiclist.rs index 818bcc9961..16db7d7d44 100644 --- a/src/lib/components/topiclist.rs +++ b/src/lib/components/topiclist.rs @@ -1,3 +1,5 @@ +use crate::weeble::{weeble_async, weeble_sync}; +use crate::wobble_sync; use anyhow::Result; use chrono::{DateTime, Local}; use gnostr_asyncgit::sync::{ @@ -12,6 +14,7 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, Frame, }; +use std::env; use std::{borrow::Cow, cell::Cell, cmp, collections::BTreeMap, rc::Rc, time::Instant}; use super::utils::logitems::{ItemBatch, LogEntry}; @@ -692,6 +695,16 @@ impl TopicList { None }; + //txt.push("topiclist:695:text".into()); + txt.push( + format!( + "{}/{}/{}", + env::var("WEEBLE").unwrap().to_string(), + env::var("BLOCKHEIGHT").unwrap(), + env::var("WOBBLE").unwrap().to_string() + ) + .into(), //wobble_sync().unwrap()).into() + ); //get_detail_to_add txt.push(self.get_detail_to_add( e, @@ -704,6 +717,7 @@ impl TopicList { now, marked, )); + txt.push("topiclist:708:text".into()); } txt @@ -980,7 +994,7 @@ impl DrawableComponent for TopicList { )); let title = format!( - "topiclist.rs:937: {} {}/{} ", + "topiclist.rs:984: {} {}/{} ", self.title, self.commits.len().saturating_sub(self.selection), self.commits.len(), @@ -1027,7 +1041,7 @@ impl DrawableComponent for TopicList { //.borders(Borders::ALL) .title(Span::styled( format!( - "more_detail--->{:>}<---", + "1032:more_detail--->{:>}<---", //"{}", title.as_str().to_owned(), //more_text.as_str() diff --git a/src/lib/decrypt_privkey.rs b/src/lib/decrypt_privkey.rs new file mode 100644 index 0000000000..2bf2026f8d --- /dev/null +++ b/src/lib/decrypt_privkey.rs @@ -0,0 +1,18 @@ +// TEMPORARILY +#![allow(clippy::uninlined_format_args)] + +use gnostr_types::{EncryptedPrivateKey, PrivateKey}; + +fn decrypt_privkey() { + println!("DANGER this exposes the private key."); + println!("encrypted private key: "); + let mut epk = String::new(); + let stdin = std::io::stdin(); + stdin.read_line(&mut epk).unwrap(); + let epk = EncryptedPrivateKey(epk.trim().to_owned()); + + let password = rpassword::prompt_password("Password: ").unwrap(); + let mut private_key = PrivateKey::import_encrypted(&epk, &password) + .expect("Could not import encrypted private key"); + println!("Private key: {}", private_key.as_hex_string()); +} diff --git a/src/lib/dns_resolver.rs b/src/lib/dns_resolver.rs index afe16a656b..e4ba39ba67 100644 --- a/src/lib/dns_resolver.rs +++ b/src/lib/dns_resolver.rs @@ -1,5 +1,5 @@ use crate::global_rt::global_rt; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::{IpAddr, SocketAddr}; use trust_dns_resolver::config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts}; use trust_dns_resolver::proto::rr::{RData, RecordType}; use trust_dns_resolver::TokioAsyncResolver; diff --git a/src/lib/fetch_by_kind_and_commit.rs b/src/lib/fetch_by_kind_and_commit.rs new file mode 100755 index 0000000000..dd27625997 --- /dev/null +++ b/src/lib/fetch_by_kind_and_commit.rs @@ -0,0 +1,44 @@ +use gnostr_crawler::processor::BOOTSTRAP_RELAYS; +use gnostr_types::{EventKind, Filter, PublicKey, PublicKeyHex}; +use log::debug; +fn fetch_by_kind_and_commit(author_pubkey: &str, kind: &str, relay: &str) { + let author_key = match author_pubkey { + Some(key) => match PublicKey::try_from_bech32_string(&key, true) { + Ok(key) => key, + Err(_) => match PublicKey::try_from_hex_string(&key, true) { + Ok(key) => key, + Err(_) => { + debug!("gnostr_fetch_by_kind_and_commit failed! invalid pubkey"); + } + }, + }, + None => { + debug!("gnostr_fetch_by_kind_and_commit failed! invalid pubkey"); + } + }; + + let kind_number = match kind { + Some(num) => num.parse::().unwrap(), + None => { + debug!("gnostr_fetch_by_kind_and_commit failed! invalid kind"); + } + }; + + let relay_url = match relay { + Some(u) => u, + None => BOOTSTRAP_RELAYS[2].clone(), + }; + + let kind: EventKind = kind_number.into(); + + let key: PublicKeyHex = author_key.into(); + let filter = Filter { + kinds: vec![kind], + authors: vec![key], + ..Default::default() + }; + + for event in gnostr::fetch_by_filter(&relay_url, filter) { + gnostr::print_event(&event); + } +} diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index da852a5787..af10adffa4 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs @@ -1633,7 +1633,9 @@ mod tests { use super::*; use crate::{git_events::generate_patch_event, repo_ref::RepoRef}; - async fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result { + async fn generate_patch_from_head_commit( + test_repo: &GitTestRepo, + ) -> Result { let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id(); let git_repo = Repo::from_path(&test_repo.dir)?; generate_patch_event( diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs index 3668193982..5760fd168e 100644 --- a/src/lib/git/nostr_url.rs +++ b/src/lib/git/nostr_url.rs @@ -137,7 +137,10 @@ impl std::str::FromStr for NostrUrlDecoded { let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; // naddr used if let Ok(coordinate) = Coordinate::parse(part) { - if coordinate.kind.eq(&nostr_sdk_0_34_0::Kind::GitRepoAnnouncement) { + if coordinate + .kind + .eq(&nostr_sdk_0_34_0::Kind::GitRepoAnnouncement) + { coordinates.insert(coordinate); } else { bail!("naddr doesnt point to a git repository announcement"); diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index c95e70ef5c..19e4814b82 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs @@ -97,7 +97,10 @@ pub async fn generate_patch_event( let commit_parent = git_repo .get_commit_parent(commit) .context("failed to get parent commit")?; - let relay_hint = repo_ref.relays.first().map(nostr_0_34_1::UncheckedUrl::from); + let relay_hint = repo_ref + .relays + .first() + .map(nostr_0_34_1::UncheckedUrl::from); sign_event( EventBuilder::new( @@ -137,12 +140,14 @@ pub async fn generate_patch_event( ), ], if let Some(thread_event_id) = thread_event_id { - vec![Tag::from_standardized(nostr_sdk_0_34_0::TagStandard::Event { - event_id: thread_event_id, - relay_url: relay_hint.clone(), - marker: Some(Marker::Root), - public_key: None, - })] + vec![Tag::from_standardized( + nostr_sdk_0_34_0::TagStandard::Event { + event_id: thread_event_id, + relay_url: relay_hint.clone(), + marker: Some(Marker::Root), + public_key: None, + }, + )] } else if let Some(event_ref) = root_proposal_id.clone() { vec![ Tag::hashtag("root"), @@ -162,12 +167,14 @@ pub async fn generate_patch_event( }, mentions.to_vec(), if let Some(id) = parent_patch_event_id { - vec![Tag::from_standardized(nostr_sdk_0_34_0::TagStandard::Event { - event_id: id, - relay_url: relay_hint.clone(), - marker: Some(Marker::Reply), - public_key: None, - })] + vec![Tag::from_standardized( + nostr_sdk_0_34_0::TagStandard::Event { + event_id: id, + relay_url: relay_hint.clone(), + marker: Some(Marker::Reply), + public_key: None, + }, + )] } else { vec![] }, @@ -259,20 +266,24 @@ pub fn event_tag_from_nip19_or_hex( if let Ok(nip19) = Nip19::from_bech32(bech32.clone()) { match nip19 { Nip19::Event(n) => { - break Ok(Tag::from_standardized(nostr_sdk_0_34_0::TagStandard::Event { - event_id: n.event_id, - relay_url: n.relays.first().map(UncheckedUrl::new), - marker: Some(marker), - public_key: None, - })); + break Ok(Tag::from_standardized( + nostr_sdk_0_34_0::TagStandard::Event { + event_id: n.event_id, + relay_url: n.relays.first().map(UncheckedUrl::new), + marker: Some(marker), + public_key: None, + }, + )); } Nip19::EventId(id) => { - break Ok(Tag::from_standardized(nostr_sdk_0_34_0::TagStandard::Event { - event_id: id, - relay_url: None, - marker: Some(marker), - public_key: None, - })); + break Ok(Tag::from_standardized( + nostr_sdk_0_34_0::TagStandard::Event { + event_id: id, + relay_url: None, + marker: Some(marker), + public_key: None, + }, + )); } Nip19::Coordinate(coordinate) => { break Ok(Tag::coordinate(coordinate)); @@ -291,12 +302,14 @@ pub fn event_tag_from_nip19_or_hex( } } if let Ok(id) = nostr_0_34_1::EventId::from_str(&bech32) { - break Ok(Tag::from_standardized(nostr_sdk_0_34_0::TagStandard::Event { - event_id: id, - relay_url: None, - marker: Some(marker), - public_key: None, - })); + break Ok(Tag::from_standardized( + nostr_sdk_0_34_0::TagStandard::Event { + event_id: id, + relay_url: None, + marker: Some(marker), + public_key: None, + }, + )); } if prompt_for_correction { println!("not a valid {reference_name} event reference"); diff --git a/src/lib/internal.rs b/src/lib/internal.rs index 4420871046..1462af5fb6 100644 --- a/src/lib/internal.rs +++ b/src/lib/internal.rs @@ -1,46 +1,23 @@ -use crate::get_weeble; +use crate::blockheight::blockheight_sync; +use crate::utils::pwd::pwd; +use crate::weeble::weeble_sync; +use crate::wobble::wobble_sync; use base64::Engine; use gnostr_types::{ClientMessage, Event, Filter, RelayMessage, RelayMessageV5, SubscriptionId}; use http::Uri; -use std::process::Command; +use log::debug; use tungstenite::protocol::Message; -pub(crate) fn pwd() -> Result { - let get_pwd = if cfg!(target_os = "windows") { - Command::new("cmd") - .args(["/C", "echo %cd%"]) - .output() - .expect("failed to execute process") - } else if cfg!(target_os = "macos") { - Command::new("sh") - .arg("-c") - .arg("echo ${PWD##*/}") - .output() - .expect("failed to execute process") - } else if cfg!(target_os = "linux") { - Command::new("sh") - .arg("-c") - .arg("echo ${PWD##*/}") - .output() - .expect("failed to execute process") - } else { - Command::new("sh") - .arg("-c") - .arg("echo ${PWD##*/}") - .output() - .expect("failed to execute process") - }; - - let mut _pwd = String::from_utf8(get_pwd.stdout) - .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) - .unwrap(); - - let _mutable_string = String::new(); - let mutable_string = _pwd.clone(); - Ok(format!("{}", mutable_string)) -} //end pwd() pub(crate) fn filters_to_wire(filters: Vec) -> String { - let message = ClientMessage::Req(SubscriptionId(get_weeble().expect("").to_owned()), filters); + let message = ClientMessage::Req( + SubscriptionId(format!( + "{:?}/{:?}/{:?}", + weeble_sync(), + blockheight_sync(), + weeble_sync(), + )), + filters, + ); serde_json::to_string(&message).expect("Could not serialize message") } @@ -101,9 +78,12 @@ pub(crate) fn fetch(host: String, uri: Uri, wire: String) -> Vec { RelayMessageV5::Event(_, e) => events.push(*e), RelayMessageV5::Notice(s) => println!("NOTICE: {}", s), RelayMessageV5::Eose(_) => { - let message = ClientMessage::Close(SubscriptionId( - get_weeble().expect("").to_owned(), - )); + let message = ClientMessage::Close(SubscriptionId(format!( + "{:?}/{:?}/{:?}", + weeble_sync(), + blockheight_sync(), + weeble_sync(), + ))); let wire = match serde_json::to_string(&message) { Ok(w) => w, Err(e) => { @@ -174,7 +154,7 @@ pub(crate) fn post(host: String, uri: Uri, wire: String) { let (mut websocket, _response) = tungstenite::connect(request).expect("Could not connect to relay"); - //print!("{}\n", wire); + print!("{}\n", wire); websocket .send(Message::Text(wire)) .expect("Could not send message to relay"); @@ -195,7 +175,7 @@ pub(crate) fn post(host: String, uri: Uri, wire: String) { let relay_message: RelayMessage = serde_json::from_str(&s).expect(&s); match relay_message { RelayMessage::Event(_, e) => { - println!("EVENT: {}", serde_json::to_string(&e).unwrap()) + println!("[\"EVENT\": {}]", serde_json::to_string(&e).unwrap()) } RelayMessage::Notice(s) => println!("NOTICE: {}", s), RelayMessage::Eose(_) => println!("EOSE"), diff --git a/src/lib/legit/gitminer.rs b/src/lib/legit/gitminer.rs new file mode 100644 index 0000000000..37acbb3137 --- /dev/null +++ b/src/lib/legit/gitminer.rs @@ -0,0 +1,263 @@ +use super::worker::Worker; +use crate::blockheight::blockheight_sync; +use crate::weeble::weeble_sync; +use crate::wobble::wobble_sync; +use git2::*; +use gnostr_crawler::processor::BOOTSTRAP_RELAYS; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use std::process; +use std::process::Command; +use std::sync::mpsc::channel; +use std::thread; + +pub struct Options { + pub threads: u32, + pub target: String, + pub message: String, + pub pwd_hash: String, + pub repo: String, + pub timestamp: time::Tm, +} + +pub struct Gitminer { + opts: Options, + repo: git2::Repository, + author: String, + pwd_hash: String, + pub relays: String, +} + +impl Gitminer { + pub fn new(opts: Options) -> Result { + let repo = match git2::Repository::open(&opts.repo) { + Ok(r) => r, + Err(_) => { + return Err("Failed to open repository"); + } + }; + + let author = Gitminer::load_author(&repo)?; + let relays = Gitminer::load_gnostr_relays(&repo)?; + let pwd_hash = Default::default(); + + Ok(Gitminer { + opts, + repo, + author, + pwd_hash, + relays, + }) + } + + pub fn mine(&mut self) -> Result { + let (tree, parent) = match Gitminer::prepare_tree(&mut self.repo) { + Ok((t, p)) => (t, p), + Err(e) => { + return Err(e); + } + }; + + let (tx, rx) = channel(); + for i in 0..self.opts.threads { + let target = self.opts.target.clone(); + let author = self.author.clone(); + let repo = self.author.clone(); + let pwd_hash = self.pwd_hash.clone(); + let msg = self.opts.message.clone(); + let wtx = tx.clone(); + let ts = self.opts.timestamp; + let weeble = weeble_sync().unwrap().to_string(); + let wobble = wobble_sync().unwrap().to_string(); + let bh = blockheight_sync(); + let (wtree, wparent) = (tree.clone(), parent.clone()); + + thread::spawn(move || { + Worker::new( + i, target, wtree, wparent, author, repo, pwd_hash, msg, ts, weeble, wobble, bh, + wtx, + ) + .work(); + }); + } + + let (_, blob, hash) = rx.recv().unwrap(); + + match self.write_commit(&hash, &blob) { + Ok(_) => Ok(hash), + Err(e) => Err(e), + } + } + + fn write_commit(&self, hash: &String, blob: &String) -> Result<(), &'static str> { + Command::new("sh") + .arg("-c") + .arg(format!("mkdir -p {}.gnostr/{} && ", self.opts.repo, hash)) + .output() + .ok() + .expect("Failed to generate commit"); + + /* repo.blob() generates a blob, not a commit. + * we write the commit, then + * we use the tmpfile to create .gnostr/blobs/ + * we 'git show' the mined tmpfile + * and pipe it into the .gnostr/blobs/ + */ + + let tmpfile = format!("/tmp/{}.tmp", hash); + let mut file = File::create(Path::new(&tmpfile)) + .ok() + .unwrap_or_else(|| panic!("Failed to create temporary file {}", &tmpfile)); + + file.write_all(blob.as_bytes()) + .ok() + .unwrap_or_else(|| panic!("Failed to write temporary file {}", &tmpfile)); + + //write the commit + Command::new("sh") + .arg("-c") + .arg(format!( + "cd {} && git hash-object -t commit -w --stdin < {} && git reset --hard {}", + self.opts.repo, tmpfile, hash + )) + .output() + .ok() + .expect("Failed to generate commit"); + + //write the blob + Command::new("sh") + .arg("-c") + .arg(format!("cd {} && mkdir -p .gnostr && touch -f .gnostr/blobs/{} && git show {} > .gnostr/blobs/{}", self.opts.repo, hash, hash, hash)) + .output() + .ok() + .expect("Failed to write .gnostr/blobs/"); + + //REF: + //gnostr-git reflog --format='wss://{RELAY}/{REPO}/%C(auto)%H/%<|(17)%gd:commit:%s' + //gnostr-git-reflog -f + //write the reflog + //the new reflog is associated with a commit + //we will use gnostr-git-reflog -f + //for an integrity check as well + //to test the 'gnostr' protocol + //write the reflog + + //gnostr-git update-index --assume-unchanged .gnostr/reflog + //--[no-]assume-unchanged + //When this flag is specified, the object names recorded for the paths are not updated. Instead, this option sets/unsets the "assume unchanged" bit for the paths. When the "assume unchanged" bit is on, the user promises not to change the file and allows Git to assume that the working tree file matches what is recorded in the index. If you want to change the working tree file, you need to unset the bit to tell Git. This is sometimes helpful when working with a big project on a filesystem that has very slow lstat(2) system call (e.g. cifs). + // + //Git will fail (gracefully) in case it needs to modify this file in the index e.g. when merging in a commit; thus, in case the assumed-untracked file is changed upstream, you will need to handle the situation manually. + + Command::new("sh") + .arg("-c") + .arg(format!("cd {} && mkdir -p .gnostr && touch -f .gnostr/reflog && gnostr-git reflog --format='wss://{}/{}/%C(auto)%H/%<|(17)%gd:commit:%s' > .gnostr/reflog", self.opts.repo, "{RELAY}", "{REPO}")) + .output() + .ok() + .expect("Failed to write .gnostr/reflog"); + Command::new("sh") + .arg("-c") + .arg(format!("cd {} && mkdir -p .gnostr && touch -f .gnostr/reflog && gnostr-git update-index --assume-unchaged .gnostr/reflog", self.opts.repo)) + .output() + .ok() + .expect("Failed to write .gnostr/reflog"); + Ok(()) + } + + fn load_author(repo: &git2::Repository) -> Result { + let cfg = match repo.config() { + Ok(c) => c, + Err(_) => { + return Err("Failed to load git config user.name"); + } + }; + + let name = match cfg.get_string("user.name") { + Ok(s) => s, + Err(_) => { + return Err("Failed to find git config user.name"); + } + }; + + let email = match cfg.get_string("user.email") { + Ok(s) => s, + Err(_) => { + return Err("Failed to find git config user.email"); + } + }; + + Ok(format!("{} <{}>", name, email)) + } + + fn load_gnostr_relays(repo: &git2::Repository) -> Result { + let cfg = match repo.config() { + Ok(c) => c, + Err(_) => { + return Err("Failed to load git config gnostr.relays"); + } + }; + + let relays = match cfg.get_string("gnostr.relays") { + Ok(s) => s, + Err(_) => { + BOOTSTRAP_RELAYS[0].to_string() //return Err("Failed to find git config gnostr.relays"); + } + }; + + Ok(relays) + } + + fn revparse_0(repo: &mut git2::Repository) -> Result<(String), &'static str> { + Gitminer::ensure_no_unstaged_changes(repo)?; + + let head = repo.revparse_single("HEAD").unwrap(); + let head_2 = format!("{}", head.id()); + + Ok(head_2) + } + fn revparse_1(repo: &mut git2::Repository) -> Result<(String), &'static str> { + Gitminer::ensure_no_unstaged_changes(repo)?; + + let head = repo.revparse_single("HEAD~1").unwrap(); + let head_1 = format!("{}", head.id()); + + Ok(head_1) + } + fn prepare_tree(repo: &mut git2::Repository) -> Result<(String, String), &'static str> { + Gitminer::ensure_no_unstaged_changes(repo)?; + + let head = repo.revparse_single("HEAD").unwrap(); + let mut index = repo.index().unwrap(); + let tree = index.write_tree().unwrap(); + + let head_s = format!("{}", head.id()); + let tree_s = format!("{}", tree); + + Ok((tree_s, head_s)) + } + + //repo status CLEAN not enough + fn ensure_no_unstaged_changes(repo: &mut git2::Repository) -> Result<(), &'static str> { + let mut opts = git2::StatusOptions::new(); + let mut m = git2::Status::empty(); + let statuses = repo.statuses(Some(&mut opts)).unwrap(); + + m.insert(git2::Status::WT_NEW); + m.insert(git2::Status::WT_MODIFIED); + m.insert(git2::Status::WT_DELETED); + m.insert(git2::Status::WT_RENAMED); + m.insert(git2::Status::WT_TYPECHANGE); + + for i in 0..statuses.len() { + let status_entry = statuses.get(i).unwrap(); + //println!("status_entry:{}", status_entry.unwrap().clone()); + if status_entry.status().intersects(m) { + println!("Please stash all unstaged changes before running."); + //return Err("Please stash all unstaged changes before running."); + process::exit(1) + } + } + + Ok(()) + } +} diff --git a/src/lib/legit/mod.rs b/src/lib/legit/mod.rs new file mode 100644 index 0000000000..9118ba954e --- /dev/null +++ b/src/lib/legit/mod.rs @@ -0,0 +1,517 @@ +#![allow(unused)] +#![allow(dead_code)] +extern crate chrono; +use crate::event_to_wire; +use crate::internal; +use crate::post; +use chrono::offset::Utc; +use chrono::DateTime; +use gnostr_types::Event; +use http::Uri; +use std::process::Command; +//use std::time::SystemTime; +use std::any::type_name; +use std::convert::TryInto; +use std::env; +use std::io::Result; +use std::thread::sleep; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +//use std::mem::size_of; +use argparse::{ArgumentParser, Store}; +use git2::*; +use gitminer::Gitminer; +use pad::{Alignment, PadStr}; +use sha2::{Digest, Sha256}; +use std::{io, thread}; + +use std::path::PathBuf; //for get_current_dir + +pub mod gitminer; +pub mod repo; +pub mod worker; + +//fn type_of(_: T) -> &'static str { +// type_name::() +//} + +fn get_epoch_ms() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() +} + +fn convert_to_u32(v: usize) -> Option { + if v > (std::i8::MAX as i32).try_into().unwrap() { + None + } else { + Some(v as i8) + } +} + +fn get_current_working_dir() -> std::io::Result { + env::current_dir() +} + +#[cfg(debug_assertions)] +fn example() { + //println!("Debugging enabled"); + //println!("cwd={:?}",get_current_working_dir()); +} + +#[cfg(not(debug_assertions))] +fn example() { + //println!("Debugging disabled"); + //println!("cwd={:?}",get_current_working_dir()); +} + +/// pub fn post_event(url: &str, event: Event) +pub fn post_event(url: &str, event: Event) { + let (host, uri) = url_to_host_and_uri(url); + let wire = event_to_wire(event); + post(host, uri, wire) +} + +pub fn url_to_host_and_uri(url: &str) -> (String, Uri) { + let uri: http::Uri = url.parse::().expect("Could not parse url"); + let authority = uri.authority().expect("Has no hostname").as_str(); + let host = authority + .find('@') + .map(|idx| authority.split_at(idx + 1).1) + .unwrap_or_else(|| authority); + if host.is_empty() { + panic!("URL has empty hostname"); + } + (host.to_owned(), uri) +} + +fn cli() -> io::Result<()> { + #[allow(clippy::if_same_then_else)] + if cfg!(debug_assertions) { + //println!("Debugging enabled"); + } else { + //println!("Debugging disabled"); + } + + #[cfg(debug_assertions)] + //println!("Debugging enabled"); + #[cfg(not(debug_assertions))] + //println!("Debugging disabled"); + example(); + + let start = time::get_time(); + let epoch = get_epoch_ms(); + //println!("{}", epoch); + let system_time = SystemTime::now(); + + let datetime: DateTime = system_time.into(); + //println!("{}", datetime.format("%d/%m/%Y %T/%s")); + //println!("{}", datetime.format("%d/%m/%Y %T")); + + let cwd = get_current_working_dir(); + //#[cfg(debug_assertions)] + //println!("Debugging enabled"); + //println!("{:#?}", cwd); + let state = repo::state(); + //println!("{:#?}", state); + // + let repo_root = std::env::args().nth(1).unwrap_or(".".to_string()); + //println!("repo_root={:?}", repo_root.as_str()); + let repo = Repository::open(repo_root.as_str()).expect("Couldn't open repository"); + //println!("{} state={:?}", repo.path().display(), repo.state()); + //println!("state={:?}", repo.state()); + + //println!("clean {:?}", repo.state()); + #[allow(clippy::if_same_then_else)] + let repo_path = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "cd"]) + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "macos") { + Command::new("sh") + .arg("-c") + .arg("pwd") + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "linux") { + Command::new("sh") + .arg("-c") + .arg("pwd") + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg("pwd") + .output() + .expect("failed to execute process") + }; + + let path = String::from_utf8(repo_path.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + //println!("path={:?}", path); + + //#!/bin/bash + //declare -a RELAYS + //function gnostr-get-relays(){ + + //RELAYS=$(curl 'https://api.nostr.watch/v1/online' 2>/dev/null | + // sed -e 's/[{}]/''/g' | + // sed -e 's/\[/''/g' | + // sed -e 's/\]/''/g' | + // sed -e 's/"//g' | + // awk -v k="text" '{n=split($0,a,","); for (i=1; i<=n; i++) print a[i]}') 2>/dev/null + + //echo $RELAYS + //} + //gnostr-get-relays + + //#!/bin/bash + //gnostr-git config --global --replace-all gnostr.relays "$(gnostr-get-relays)" #&& git config -l | grep gnostr.relays + #[allow(clippy::if_same_then_else)] + let set_relays = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "gnostr-set-relays"]) + .output() + .expect("try:\ngnostr-git config -l | grep gnostr.relays") + } else if cfg!(target_os = "macos") { + Command::new("sh") + .arg("-c") + .arg("gnostr-set-relays") + .output() + .expect("try:\ngnostr-git config -l | grep gnostr.relays") + } else if cfg!(target_os = "linux") { + Command::new("sh") + .arg("-c") + .arg("gnostr-set-relays") + .output() + .expect("try:\ngnostr-git config -l | grep gnostr.relays") + } else { + Command::new("sh") + .arg("-c") + .arg("gnostr-set-relays") + .output() + .expect("try:\ngnostr-git config -l | grep gnostr.relays") + }; + + let count = thread::available_parallelism()?.get(); + assert!(count >= 1_usize); + //println!("{}={}", type_of(count), (count as i32)); + //println!("{}={}", type_of(count), (count as i64)); + //let mut hasher = Sha256::new(); + //hasher.update(pwd); + //// `update` can be called repeatedly and is generic over `AsRef<[u8]>` + //hasher.update("String data"); + //// Note that calling `finalize()` consumes hasher + //let hash = hasher.finalize(); + ////println!("Binary hash: {:?}", hash); + //println!("hash: {:?}", hash); + //println!("sha256 before write: {:x}", hash); + //println!("sha256 before write: {:X}", hash); + + let now = SystemTime::now(); + + //// we sleep for 2 seconds + //sleep(Duration::new(2, 0)); + // match now.elapsed() { + // Ok(elapsed) => { + // // it prints '2' + // println!("{}", elapsed.as_secs()); + // } + // Err(e) => { + // // an error occurred! + // println!("Error: {e:?}"); + // } + //} + + #[allow(clippy::if_same_then_else)] + let get_pwd = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "echo %cd%"]) + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "macos") { + Command::new("sh") + .arg("-c") + .arg("echo ${PWD##*/}") + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "linux") { + Command::new("sh") + .arg("-c") + .arg("echo ${PWD##*/}") + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg("echo ${PWD##*/}") + .output() + .expect("failed to execute process") + }; + + let pwd = String::from_utf8(get_pwd.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + //println!("pwd={}", pwd); + let mut hasher = Sha256::new(); + hasher.update(pwd.clone()); + //sha256sum <(echo gnostr-legit) + let pwd_hash: String = format!("{:x}", hasher.finalize()); + //println!("pwd_hash={:?}", pwd_hash); + + #[allow(clippy::if_same_then_else)] + let gnostr_weeble = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "gnostr-weeble || echo weeble"]) + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "macos") { + Command::new("sh") + .arg("-c") + .arg("gnostr-weeble 2>/tmp/gnostr-legit.log || echo weeble") + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "linux") { + Command::new("sh") + .arg("-c") + .arg("gnostr-weeble 2>/tmp/gnostr-legit.log || echo weeble") + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg("gnostr-weeble 2>/tmp/gnostr-legit.log || echo weeble") + .output() + .expect("failed to execute process") + }; + + let weeble = String::from_utf8(gnostr_weeble.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + + //assert_eq!(weeble.is_empty(), true); // a) + // + //println!("weeble={}", weeble); + + #[allow(clippy::if_same_then_else)] + let gnostr_wobble = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "gnostr-wobble"]) + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "macos") { + Command::new("sh") + .arg("-c") + .arg("gnostr-wobble || echo wobble") + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "linux") { + Command::new("sh") + .arg("-c") + .arg("gnostr-wobble || echo wobble") + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg("gnostr-wobble || echo wobble") + .output() + .expect("failed to execute process") + }; + + let wobble = String::from_utf8(gnostr_wobble.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + //println!("wobble={}", wobble); + #[allow(clippy::if_same_then_else)] + let gnostr_blockheight = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "gnostr-blockheight"]) + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "macos") { + Command::new("sh") + .arg("-c") + .arg("gnostr-blockheight || echo blockheight") + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "linux") { + Command::new("sh") + .arg("-c") + .arg("gnostr-blockheight || echo blockheight") + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg("gnostr-blockheight || echo blockheight") + .output() + .expect("failed to execute process") + }; + + let blockheight = String::from_utf8(gnostr_blockheight.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + //println!("blockheight={}", blockheight); + + let path = env::current_dir()?; + + //println!("The current directory is {}", path.display()); + + let mut opts = gitminer::Options { + threads: count.try_into().unwrap(), + target: "00000".to_string(), //default 00000 + //gnostr:##:nonce + //part of the gnostr protocol + //src/worker.rs adds the nonce + pwd_hash: pwd_hash.clone(), + message: pwd, + //message: message, + //message: count.to_string(), + //repo: ".".to_string(), + repo: path.as_path().display().to_string(), + timestamp: time::now(), + }; + + parse_args_or_exit(&mut opts); + + let mut miner = match Gitminer::new(opts) { + Ok(m) => m, + Err(e) => { + panic!("Failed to start git miner: {}", e); + } + }; + + let hash = match miner.mine() { + Ok(s) => s, + Err(e) => { + panic!("Failed to generate commit: {}", e); + } + }; + + let mut hasher = Sha256::new(); + hasher.update(&hash); + // `update` can be called repeatedly and is generic over `AsRef<[u8]>` + //hasher.update("String data"); + // Note that calling `finalize()` consumes hasher + //let gnostr_sec = hasher.finalize(); + let gnostr_sec: String = format!("{:X}", hasher.finalize()); + //println!("Binary hash: {:?}", hash); + //println!("hash before: {:?}", hash); + //println!("hash after pad: {:?}", hash); + //println!("&hash before: {:?}", &hash); + //println!("&hash after pad: {:?}", &hash); + //println!("gnostr_sec before pad: {:?}", gnostr_sec); + //println!("gnostr_sec after pad: {:?}", gnostr_sec.pad(64, '0', Alignment::Right, true)); + //println!("&gnostr_sec before pad: {:?}", &gnostr_sec); + //println!("&gnostr_sec after pad: {:?}", &gnostr_sec.pad(64, '0', Alignment::Right, true)); + + //let s = "12345".pad(64, '0', Alignment::Right, true); + //println!("s: {:?}", s); + // echo "000000b64a065760e5441bf47f0571cb690b28fc" | openssl dgst -sha256 | sed 's/SHA2-256(stdin)= //g' + // + // + //shell test + let touch = Command::new("sh") + .args(["-c", "touch ", &hash]) + .output() + .expect("failed to execute process"); + let touch_event = String::from_utf8(touch.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + let cat = Command::new("sh") + .args(["-c", "touch ", &hash]) + .output() + .expect("failed to execute process"); + let cat_event = String::from_utf8(cat.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + //shell test + //git rev-parse --verify HEAD + #[allow(clippy::if_same_then_else)] + let event = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "gnostr --sec $(gnostr-sha256 $(gnostr-weeble || echo)) -t gnostr --tag weeble $(gnostr-weeble || echo weeble) --tag wobble $(gnostr-wobble || echo wobble) --tag blockheight $(gnostr-blockheight || echo blockheight) --content \"$(gnostr-git diff HEAD~1 || gnostr-git diff)\" "]) + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "macos") { + Command::new("sh") + .args(["-c", "gnostr --sec $(gnostr-sha256 $(gnostr-weeble || echo)) -t gnostr --tag weeble $(gnostr-weeble || echo weeble) --tag wobble $(gnostr-wobble || echo wobble) --tag blockheight $(gnostr-blockheight || echo blockheight) --content \"$(gnostr-git show HEAD)\" "]) + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "linux") { + Command::new("sh") + .args(["-c", "gnostr --sec $(gnostr-sha256 $(gnostr-weeble || echo)) -t gnostr --tag weeble $(gnostr-weeble || echo weeble) --tag wobble $(gnostr-wobble || echo wobble) --tag blockheight $(gnostr-blockheight || echo blockheight) --content \"$(gnostr-git diff HEAD~1 || gnostr-git diff)\" "]) + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .args(["-c", "gnostr --sec $(gnostr-sha256 $(gnostr-weeble || echo)) -t gnostr --tag weeble $(gnostr-weeble || echo weeble) --tag wobble $(gnostr-wobble || echo wobble) --tag blockheight $(gnostr-blockheight || echo blockheight) --content \"$(gnostr-git diff HEAD~1 || gnostr-git diff)\" "]) + .output() + .expect("failed to execute process") + }; + + let gnostr_event = String::from_utf8(event.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + + //assert... + //echo gnostr|openssl dgst -sha256 | sed 's/SHA2-256(stdin)= //g' + + //gnostr-legit must only return a sha256 generated by the + //recent commit hash + //to enable nested commands + //REF: + //gnostr --hash $(gnostr legit . -p 00000 -m "git rev-parse --verify HEAD") + //gnostr --sec $(gnostr --hash $(gnostr legit . -p 00000 -m "git rev-parse --verify HEAD")) + //Example: + //gnostr --sec $(gnostr --hash $(gnostr legit . -p 00000 -m "#gnostr will exist!")) --envelope --content "$(gnostr-git log -n 1)" | gnostr-cat -u wss://relay.damus.io + // + // + // + let duration = time::get_time() - start; + //println!("Success! Generated commit {} in {} seconds", hash, duration.num_seconds()); + println!("{}", gnostr_event); + Ok(()) +} + +fn parse_args_or_exit(opts: &mut gitminer::Options) { + let mut ap = ArgumentParser::new(); + ap.set_description("Generate git commit sha with a custom prefix"); + ap.stop_on_first_argument(false); + + //ap.refer(&mut opts.repo) + // //.add_argument("repository-path", Store, "Path to your git repository (required)"); + // .add_argument("repository-path", Store, "Path to your git repository"); + // //.required(); + ap.refer(&mut opts.repo) + .add_argument("repository-path", Store, "Path to your git repository"); + + ap.refer(&mut opts.target).add_option( + &["-p", "--prefix"], + Store, + "Desired commit prefix (required)", + ); + //.required(); + + ap.refer(&mut opts.threads).add_option( + &["-t", "--threads"], + Store, + "Number of worker threads to use (default 8)", + ); + + ap.refer(&mut opts.message).add_option( + &["-m", "--message"], + Store, + "Commit message to use (required)", + ); + //.required(); + + //ap.refer(&mut opts.timestamp) + // .add_option(&["--timestamp"], Store, "Commit timestamp to use (default now)"); + + ap.parse_args_or_exit(); +} diff --git a/src/lib/legit/repo.rs b/src/lib/legit/repo.rs new file mode 100644 index 0000000000..dac06dc95c --- /dev/null +++ b/src/lib/legit/repo.rs @@ -0,0 +1,58 @@ +extern crate git2; +use git2::Repository; +use git2::RepositoryState; +use std::process::Command; + +pub trait ToString { + fn to_string(&self) -> String; +} + +pub fn state() -> RepositoryState { + let get_pwd = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "echo %cd%"]) + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg("echo `pwd`") + .output() + .expect("failed to execute process") + }; + + let pwd = String::from_utf8(get_pwd.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + //println!("pwd={:?}", pwd); + + //let repo_root = std::env::args().nth(1).unwrap_or(pwd); + + let repo_root = std::env::args().nth(1).unwrap_or(".".to_string()); + //println!("repo_root={:?}", repo_root.as_str()); + let repo = Repository::open(repo_root.as_str()).expect("Couldn't open repository"); + //println!("{} state={:?}", repo.path().display(), repo.state()); + //println!("state={:?}", repo.state()); + if repo.state() == RepositoryState::Clean { + //println!("clean {:?}", repo.state()); + + let repo_state = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "git status"]) + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg("git status") + .output() + .expect("failed to execute process") + }; + + let state = String::from_utf8(repo_state.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + //println!("state={:?}", state); + } + repo.state() +} diff --git a/src/lib/legit/worker.rs b/src/lib/legit/worker.rs new file mode 100644 index 0000000000..d6912fab62 --- /dev/null +++ b/src/lib/legit/worker.rs @@ -0,0 +1,137 @@ +use crypto::digest::Digest; +use crypto::sha1; +use log::debug; +use std::sync::mpsc; +//use time; + +pub struct Worker { + id: u32, + digest: sha1::Sha1, + tx: mpsc::Sender<(u32, String, String)>, + target: String, + tree: String, + parent: String, + author: String, + repo: String, + pwd_hash: String, + message: String, + timestamp: time::Tm, + weeble: String, + wobble: String, + blockheight: String, +} + +impl Worker { + pub fn new( + id: u32, + //digest: sha1::Sha1, + target: String, + tree: String, + parent: String, + author: String, + repo: String, + pwd_hash: String, + message: String, + timestamp: time::Tm, + weeble: String, + wobble: String, + blockheight: String, + tx: mpsc::Sender<(u32, String, String)>, + ) -> Worker { + Worker { + id, + digest: sha1::Sha1::new(), + target, + tree, + parent, + author, + repo, + pwd_hash, + message, + timestamp, + weeble, + wobble, + blockheight, + tx, + } + } + + pub fn work(&mut self) { + let tstamp = format!("{}", self.timestamp.strftime("%s %z").unwrap()); + + let mut value = 0u32; + loop { + let (raw, blob) = self.generate_blob(value, &tstamp); + let result = self.calculate(&blob); + + if result.starts_with(&self.target) { + self.tx.send((self.id, raw, result)); + break; + } + + value += 1; + } + } + + fn generate_blob(&mut self, value: u32, tstamp: &str) -> (String, String) { + debug!("self.message={}\n", self.message); + + debug!("self.tree={}\n", self.tree); + debug!("self.parent={}\n", self.parent); + debug!("self.author={}\n", self.author); + debug!("self.author={}\n", self.author); + //debug!("self.committer={}\n",self.committer); + debug!("self.tree={}\n", self.tree); + debug!("self.parent={}\n", self.parent); + debug!("self.weeble.trim()={}\n", self.weeble.trim()); + debug!("self.blockheight.trim()={}\n", self.blockheight.trim()); + debug!("self.wobble.trim()={}\n", self.wobble.trim()); + debug!("self.id={}\n", self.id); + debug!("self.value={}\n", value); + debug!("self.message={}\n", self.message); + + let raw = format!( + "tree {}\n\ + parent {}\n\ + author {} {}\n\ + committer {} {}\n\n\ + {}/{}/{}:{}\n\n\"tree\":\"{}\",\"parent\":\"{}\",\"weeble\":\"{:04}\",\"blockheight\":\"{:06}\",\"wobble\":\"{:}\",\"bit\":\"{:02}\",\"nonce\":\"{:08x}\",\"message\":\"{:}\"", + + //below are in essential format + self.tree, + self.parent, + self.author, tstamp, //author + self.author, tstamp, //committer + //above are in essential format + + //first element is commit subject line + self.weeble.trim(), + self.blockheight.trim(), + self.wobble.trim(), + self.message, + + //event body + self.tree, + self.parent, + self.weeble.trim(), + self.blockheight.trim(), + self.wobble.trim(), + self.id, value, + self.message + ); + debug!("raw={}\n", raw); + + //be careful when changing - fails silently when wrong. + let blob = format!("commit {}\0{}", raw.len(), raw); + debug!("blob={}\n", blob); + + (raw, blob) + } + + fn calculate(&mut self, blob: &str) -> String { + self.digest.reset(); + self.digest.input_str(blob); + + self.digest.result_str() + } +} diff --git a/src/lib/login/key_encryption.rs b/src/lib/login/key_encryption.rs index 790595d6dc..9caeeb813c 100644 --- a/src/lib/login/key_encryption.rs +++ b/src/lib/login/key_encryption.rs @@ -27,7 +27,9 @@ pub fn decrypt_key(encrypted_key: &str, password: &str) -> Result 14 { println!("this may take a few seconds..."); } - Ok(nostr_0_34_1::Keys::new(encrypted_key.to_secret_key(password)?)) + Ok(nostr_0_34_1::Keys::new( + encrypted_key.to_secret_key(password)?, + )) } #[cfg(test)] diff --git a/src/lib/login/mod.rs b/src/lib/login/mod.rs index 836d1f3107..faf3f5d883 100644 --- a/src/lib/login/mod.rs +++ b/src/lib/login/mod.rs @@ -587,7 +587,10 @@ fn extract_user_metadata( }) } -fn extract_user_relays(public_key: &nostr_0_34_1::PublicKey, events: &[nostr_0_34_1::Event]) -> UserRelays { +fn extract_user_relays( + public_key: &nostr_0_34_1::PublicKey, + events: &[nostr_0_34_1::Event], +) -> UserRelays { let event = events .iter() .filter(|e| e.kind.eq(&nostr_0_34_1::Kind::RelayList) && e.pubkey.eq(public_key)) @@ -603,10 +606,9 @@ fn extract_user_relays(public_key: &nostr_0_34_1::PublicKey, events: &[nostr_0_3 .tags .iter() .filter(|t| { - t.kind() - .eq(&nostr_0_34_1::TagKind::SingleLetter(SingleLetterTag::lowercase( - Alphabet::R, - ))) + t.kind().eq(&nostr_0_34_1::TagKind::SingleLetter( + SingleLetterTag::lowercase(Alphabet::R), + )) }) .map(|t| UserRelayRef { //url: (t.as_vec().len() == 2 || t.as_vec()[1].clone()).to_string(), diff --git a/src/lib/mod.rs b/src/lib/mod.rs index e348497ce4..95ebbdc61e 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -1,127 +1,152 @@ //! gnostr: a git+nostr workflow utility and library -pub use base64::Engine; -pub use colorful::{Color, Colorful}; -pub use futures_util::stream::FusedStream; -pub use futures_util::{SinkExt, StreamExt}; -pub use http::Uri; -pub use lazy_static::lazy_static; -use log::debug; -// pub //use nostr_types::RelayMessageV5; -pub use gnostr_types::{ - ClientMessage, EncryptedPrivateKey, Event, EventKind, Filter, Id, IdHex, KeySigner, PreEvent, - RelayMessage, RelayMessageV3, RelayMessageV5, Signer, SubscriptionId, Tag, Unixtime, Why, -}; -pub use nostr_sdk_0_19_1::prelude::rand; -pub use tokio::sync::mpsc::{Receiver, Sender}; -pub use tungstenite::Message; -pub use zeroize::Zeroize; -//pub use gnip44::*; -//avoid?//upgrade? -//pub use lightning; - -/// +//! +/// pub mod app; -/// -pub mod args; -/// +/// pub mod bug_report; -/// +/// pub mod chat; -/// +/// pub mod cli; -/// +/// pub mod cli_interactor; -/// +/// pub mod client; -/// +/// pub mod clipboard; -/// +/// pub mod cmdbar; -/// +/// pub mod components; -/// +/// pub mod dns_resolver; -/// +/// pub mod git; -/// +/// pub mod git_events; -/// +/// pub mod global_rt; -/// +/// pub mod gnostr; -/// +/// pub mod input; -/// +/// pub mod keys; -/// +/// +pub mod legit; +/// pub mod login; -/// +/// pub mod notify_mutex; -/// +/// pub mod options; -/// +/// +pub mod p2p; +/// pub mod popup_stack; -/// +/// pub mod popups; -/// +/// pub mod queue; -/// +/// pub mod remote; -/// +/// pub mod repo_ref; -/// +/// pub mod repo_state; -/// +/// pub mod spinner; -/// +/// pub mod ssh; -/// +/// pub mod string_utils; -/// +/// pub mod strings; -/// +/// pub mod sub_commands; -/// +/// pub mod tabs; -/// +/// pub mod tui; -/// +/// pub mod ui; -/// +/// pub mod utils; -/// +/// pub mod verify_keypair; -/// +/// pub mod watcher; /// -/// simple-websockets +/// pub mod ws; +/// +pub use base64::Engine; +/// +pub use colorful::{Color, Colorful}; +/// +pub use futures_util::stream::FusedStream; +/// +pub use futures_util::{SinkExt, StreamExt}; +/// +pub use http::Uri; +/// +pub use lazy_static::lazy_static; +/// +use log::debug; +// pub //use nostr_types::RelayMessageV5; +/// +pub use gnostr_types::{ + ClientMessage, EncryptedPrivateKey, Event, EventKind, Filter, Id, IdHex, KeySigner, PreEvent, + RelayMessage, RelayMessageV3, RelayMessageV5, Signer, SubscriptionId, Tag, Unixtime, Why, +}; +// +/// +pub use nostr_sdk_0_19_1::prelude::rand; +// +/// +pub use tokio::sync::mpsc::{Receiver, Sender}; +/// +pub use tungstenite::Message; +/// +pub use zeroize::Zeroize; +//pub use gnip44::*; +//avoid?//upgrade? +//pub use lightning; +/// use anyhow::{anyhow, Result}; +/// use directories::ProjectDirs; +/// pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// pub fn get_dirs() -> Result { //maintain compat with ngit - ProjectDirs::from("ngit", "gnostr", ".gnostr").ok_or(anyhow!( + ProjectDirs::from("org", "gnostr", "gnostr").ok_or(anyhow!( "should find operating system home directories with rust-directories crate" )) } +/// type Ws = tokio_tungstenite::WebSocketStream>; +/// pub mod reflog; +/// pub use reflog::{ref_hash_list, ref_hash_list_padded, ref_hash_list_w_commit_message}; +/// pub use relays::{ relays, relays_by_nip, relays_offline, relays_online, relays_paid, relays_public, }; +/// pub mod watch_list; +/// pub use watch_list::*; //TODO @@ -168,24 +193,37 @@ pub fn get_relays_offline() -> Result { Ok(format!("{}", relays_offline().unwrap().to_string())) } +/// weeble /// pub fn get_weeble() -> Result pub fn get_weeble() -> Result { - Ok(format!("{}", weeble().unwrap_or(0_f64).to_string())) + get_weeble_sync() } -/// pub async fn get_weeble_async_async() -> Result +/// pub fn get_weeble_sync() -> Result +pub fn get_weeble_sync() -> Result { + Ok(format!("{}", weeble_sync().unwrap_or(0_f64).to_string())) +} +/// pub async fn get_weeble_async() -> Result pub async fn get_weeble_async() -> Result { Ok(format!( "{}", weeble_async().await.unwrap_or(0_f64).to_string() )) } -/// pub fn get_weeble_millis() -> Result -pub fn get_weeble_millis() -> Result { - Ok(format!("{}", weeble_millis().unwrap_or(0_f64).to_string())) +/// pub fn get_weeble_millis_async() -> Result +pub async fn get_weeble_millis_async() -> Result { + Ok(format!( + "{}", + weeble_millis_async().await.unwrap_or(0_f64).to_string() + )) } +/// wobble /// pub fn get_wobble() -> Result pub fn get_wobble() -> Result { - Ok(format!("{}", wobble().unwrap_or(0_f64).to_string())) + get_wobble_sync() +} +/// pub fn get_wobble_sync() -> Result +pub fn get_wobble_sync() -> Result { + Ok(format!("{}", wobble_sync().unwrap_or(0_f64).to_string())) } /// pub async fn get_wobble_async() -> Result pub async fn get_wobble_async() -> Result { @@ -194,10 +232,14 @@ pub async fn get_wobble_async() -> Result { wobble_async().await.unwrap_or(0_f64).to_string() )) } -/// pub fn get_wobble_millis() -> Result -pub fn get_wobble_millis() -> Result { - Ok(format!("{}", wobble_millis().unwrap_or(0_f64).to_string())) +/// pub fn get_wobble_millis_async() -> Result +pub async fn get_wobble_millis_async() -> Result { + Ok(format!( + "{}", + wobble_millis_async().await.unwrap_or(0_f64).to_string() + )) } + /// pub fn get_blockheight() -> Result pub fn get_blockheight() -> Result { Ok(format!("{}", blockheight().unwrap_or(0_f64).to_string())) @@ -298,27 +340,29 @@ pub fn print_event(event: &Event) { mod internal; use internal::*; -/// +/// pub mod weeble; pub use weeble::weeble; pub use weeble::weeble_async; -pub use weeble::weeble_millis; +pub use weeble::weeble_millis_async; +pub use weeble::weeble_sync; -/// +/// pub mod wobble; -pub use crate::wobble::wobble_millis; pub use wobble::wobble; pub use wobble::wobble_async; +pub use wobble::wobble_millis_async; +pub use wobble::wobble_sync; -/// +/// pub mod blockhash; pub use blockhash::blockhash; -/// +/// pub mod blockheight; pub use blockheight::blockheight; -/// +/// pub mod hash; pub use hash::hash; @@ -358,7 +402,7 @@ pub fn fetch_by_id(url: &str, id: IdHex) -> Option { } pub fn get_pwd() -> Result { - let mut no_nl = pwd().unwrap().to_string(); + let mut no_nl = crate::utils::pwd::pwd().unwrap().to_string(); no_nl.retain(|c| c != '\n'); return Ok(format!("{ }", no_nl)); } diff --git a/src/lib/chat/p2p.rs b/src/lib/p2p.rs similarity index 94% rename from src/lib/chat/p2p.rs rename to src/lib/p2p.rs index a24d8a4e14..16751ff377 100644 --- a/src/lib/chat/p2p.rs +++ b/src/lib/p2p.rs @@ -1,14 +1,14 @@ +use crate::blockhash::blockhash_async; use crate::blockheight::blockheight_async; use crate::chat::msg::{Msg, MsgKind}; use chrono::{Local, Timelike}; use futures::stream::StreamExt; -use libp2p::{core::multiaddr::Protocol, core::Multiaddr, identify, identity, ping, relay}; use libp2p::{gossipsub, mdns, noise, swarm::NetworkBehaviour, swarm::SwarmEvent, tcp, yamux}; use std::{env, error::Error, thread}; -use tokio::time::{sleep, Duration}; +use tokio::time::Duration; use tokio::{io, select}; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, warn}; //const TOPIC: &str = "gnostr"; /// MyBehaviour @@ -100,9 +100,8 @@ pub async fn evt_loop( if current_second % 2 != 0 { debug!("Current second ({}) is odd!", current_second); - // Add your code here to be executed only when the time is odd - //debug!("blockheight_async():{}", blockheight_async().await); env::set_var("BLOCKHEIGHT", &blockheight_async().await); + env::set_var("BLOCKHASH", &blockhash_async().await); } else { debug!( "Current second ({}) is even. Skipping this iteration.", @@ -125,10 +124,14 @@ pub async fn evt_loop( .behaviour_mut().gossipsub .publish(topic.clone(), serde_json::to_vec(&m)?) { debug!("Publish error: {e:?}"); - let m = Msg::default() + let mut m = Msg::default() /**/.set_content(format!("{{\"blockheight\":\"{}\"}}", env::var("BLOCKHEIGHT").unwrap()), 0).set_kind(MsgKind::System); //NOTE:recv.send - send to self recv.send(m).await?; + m = Msg::default() + /**/.set_content(format!("{{\"blockhash\":\"{}\"}}", env::var("BLOCKHASH").unwrap()), 0).set_kind(MsgKind::System); + //NOTE:recv.send - send to self + recv.send(m).await?; //let m = Msg::default().set_content("p2p.rs:brief help prompt here!:2".to_string(), 2).set_kind(MsgKind::System); ////NOTE:recv.send - send to self //recv.send(m).await?; diff --git a/src/lib/popups/openchat.rs b/src/lib/popups/openchat.rs index 88a38ec566..7cb70182a1 100644 --- a/src/lib/popups/openchat.rs +++ b/src/lib/popups/openchat.rs @@ -6,7 +6,7 @@ use crossterm::{ terminal::{EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; -use gnostr_asyncgit::sync::{get_config_string, utils::repo_work_dir, RepoPath}; +use gnostr_asyncgit::sync::{utils::repo_work_dir, RepoPath}; use ratatui::{ layout::Rect, text::{Line, Span}, diff --git a/src/lib/remote/remote_runner.rs b/src/lib/remote/remote_runner.rs index de4b085369..1b89769b9c 100644 --- a/src/lib/remote/remote_runner.rs +++ b/src/lib/remote/remote_runner.rs @@ -6,8 +6,7 @@ use anyhow::*; //https://crates.io/crates/bincode/1.3.1 //bincode use core::result::Result::Ok; -use log::{debug, error, info, trace, LevelFilter}; -use simple_logger::SimpleLogger; +use log::{debug, error, info, trace}; use std::{ fs::File, io::{Read, Write}, diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs index e5219cc807..cf52dd0fe6 100644 --- a/src/lib/repo_ref.rs +++ b/src/lib/repo_ref.rs @@ -158,7 +158,9 @@ impl RepoRef { self.relays.clone(), ), Tag::custom( - nostr_0_34_1::TagKind::Custom(std::borrow::Cow::Borrowed("maintainers")), + nostr_0_34_1::TagKind::Custom(std::borrow::Cow::Borrowed( + "maintainers", + )), self.maintainers .iter() .map(std::string::ToString::to_string) diff --git a/src/lib/ssh/git/mod.rs b/src/lib/ssh/git/mod.rs index 23a10ce44d..68d2bfbbb5 100644 --- a/src/lib/ssh/git/mod.rs +++ b/src/lib/ssh/git/mod.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::{anyhow, Context}; -use log::{debug, info}; +use log::debug; pub struct Repo { dir: PathBuf, } diff --git a/src/lib/ssh/ssh/mod.rs b/src/lib/ssh/ssh/mod.rs index 4b9d7948a1..228f79a644 100644 --- a/src/lib/ssh/ssh/mod.rs +++ b/src/lib/ssh/ssh/mod.rs @@ -10,7 +10,7 @@ use tokio::process::ChildStdin; use crate::ssh::config::server::ServerUser; use crate::ssh::State; -use log::{debug, error, info}; +use log::{debug, error}; use tokio::sync::Mutex; use toml::Table; diff --git a/src/lib/sub_commands/fetch.rs b/src/lib/sub_commands/fetch.rs index 9325c22954..ffb6b9b2d3 100644 --- a/src/lib/sub_commands/fetch.rs +++ b/src/lib/sub_commands/fetch.rs @@ -29,7 +29,7 @@ pub async fn launch( #[cfg(test)] let mut client: &crate::client::MockConnect = &mut Default::default(); #[cfg(not(test))] - let mut client = Client::default(); + let client = Client::default(); let repo_coordinates = if args.repo.is_empty() { get_repo_coordinates(&git_repo, &client).await? diff --git a/src/lib/sub_commands/legit.rs b/src/lib/sub_commands/legit.rs new file mode 100644 index 0000000000..7190de9b05 --- /dev/null +++ b/src/lib/sub_commands/legit.rs @@ -0,0 +1,50 @@ +#![cfg_attr(not(test), warn(clippy::pedantic))] +#![cfg_attr(not(test), warn(clippy::expect_used))] +use crate::cli::LegitCommands; +use crate::sub_commands::fetch; +use crate::sub_commands::init; +use crate::sub_commands::list; +use crate::sub_commands::login; +use crate::sub_commands::pull; +use crate::sub_commands::push; +use crate::sub_commands::send; +use clap::Args; +use nostr_sdk_0_34_0::prelude::*; + +use serde::ser::StdError; + +#[derive(Args, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct LegitSubCommand { + #[command(subcommand)] + command: LegitCommands, + ///// nsec or hex private key + #[arg(short, long, global = true)] + nsec: Option, + ///// password to decrypt nsec + #[arg(short, long, global = true)] + password: Option, + ///// nsec or hex private key + #[arg(short, long, global = true)] + repo: Option, + ///// password to decrypt nsec + #[arg(long, global = true)] + pow: Option, + ///// disable spinner animations + #[arg(long, action)] + disable_cli_spinners: bool, +} + +pub async fn legit(sub_command_args: &LegitSubCommand) -> Result<(), Box> { + match &sub_command_args.command { + LegitCommands::Login(args) => login::launch(&args).await?, + LegitCommands::Init(args) => init::launch(&args).await?, + LegitCommands::Send(args) => send::launch(&args, true).await?, + LegitCommands::List => list::launch().await?, + LegitCommands::Pull => pull::launch().await?, + LegitCommands::Push(args) => push::launch(&args).await?, + LegitCommands::Fetch(args) => fetch::launch(&args).await?, + } + Ok(()) +} diff --git a/src/lib/sub_commands/list.rs b/src/lib/sub_commands/list.rs index 80288e0aac..8b571b4efe 100644 --- a/src/lib/sub_commands/list.rs +++ b/src/lib/sub_commands/list.rs @@ -32,9 +32,9 @@ pub async fn launch() -> Result<()> { // TODO: check for other claims #[cfg(test)] - let mut client: &crate::client::MockConnect = &mut Default::default(); + let client: &crate::client::MockConnect = &mut Default::default(); #[cfg(not(test))] - let mut client = Client::default(); + let client = Client::default(); let repo_coordinates = get_repo_coordinates(&git_repo, &client).await?; diff --git a/src/lib/sub_commands/mod.rs b/src/lib/sub_commands/mod.rs index 30ee3bdc4b..55c869fc5d 100644 --- a/src/lib/sub_commands/mod.rs +++ b/src/lib/sub_commands/mod.rs @@ -8,6 +8,7 @@ pub mod delete_event; pub mod delete_profile; pub mod generate_keypair; pub mod hide_public_channel_message; +pub mod legit; pub mod list_events; pub mod mute_publickey; pub mod ngit; diff --git a/src/lib/sub_commands/pull.rs b/src/lib/sub_commands/pull.rs index 350ea788de..a762a9a3ca 100644 --- a/src/lib/sub_commands/pull.rs +++ b/src/lib/sub_commands/pull.rs @@ -30,7 +30,7 @@ pub async fn launch() -> Result<()> { } #[cfg(test)] - let mut client: &crate::client::MockConnect = &mut Default::default(); + let client: &crate::client::MockConnect = &mut Default::default(); #[cfg(not(test))] let mut client = Client::default(); diff --git a/src/lib/sub_commands/react.rs b/src/lib/sub_commands/react.rs index 712e0fc907..0fcd246d57 100644 --- a/src/lib/sub_commands/react.rs +++ b/src/lib/sub_commands/react.rs @@ -7,7 +7,7 @@ use nostr_sdk_0_32_0::prelude::*; use crate::utils::{create_client, parse_private_key}; use gnostr_crawler::processor::BOOTSTRAP_RELAYS; -use tracing::{debug, error, info, trace, warn}; +use tracing::debug; #[derive(Args, Debug)] pub struct ReactionSubCommand { diff --git a/src/lib/sub_commands/send.rs b/src/lib/sub_commands/send.rs index 4cfe00b807..92749cae55 100644 --- a/src/lib/sub_commands/send.rs +++ b/src/lib/sub_commands/send.rs @@ -62,7 +62,7 @@ pub async fn launch( .get_main_or_master_branch() .context("the default branches (main or master) do not exist")?; #[cfg(test)] - let mut client: &mut crate::client::MockConnect = &mut Default::default(); + let client: &mut crate::client::MockConnect = &mut Default::default(); //let mut client: &mut Client::MockConnect = &mut Default::default(); #[cfg(not(test))] let mut client = Client::default(); @@ -383,9 +383,11 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to( marker: _, public_key: _, }) => { - let events = - get_events_from_cache(git_repo_path, vec![nostr_0_34_1::Filter::new().id(*event_id)]) - .await?; + let events = get_events_from_cache( + git_repo_path, + vec![nostr_0_34_1::Filter::new().id(*event_id)], + ) + .await?; if let Some(first) = events.iter().find(|e| e.id.eq(event_id)) { if event_is_patch_set_root(first) { diff --git a/src/lib/tabs/home.rs b/src/lib/tabs/home.rs index 595afbdd0d..9a54803aaa 100644 --- a/src/lib/tabs/home.rs +++ b/src/lib/tabs/home.rs @@ -36,7 +36,7 @@ use crate::{ TopicList, }, keys::{key_match, SharedKeyConfig}, - popups::{DisplayChatOpen, FileTreeOpen, InspectChatOpen, InspectCommitOpen}, + popups::{FileTreeOpen, InspectChatOpen, InspectCommitOpen}, queue::{InternalEvent, Queue, StackablePopupOpen}, strings::{self, order}, try_or_popup, diff --git a/src/lib/tui.rs b/src/lib/tui.rs index 6225557bba..626035c6ab 100644 --- a/src/lib/tui.rs +++ b/src/lib/tui.rs @@ -26,10 +26,8 @@ //TODO: // #![deny(clippy::expect_used)] -use std::{ - cell::RefCell, - time::{Duration, Instant}, -}; +use chrono::{Local, Timelike}; +use std::{cell::RefCell, time::Instant}; use crate::app::App; use crate::app::QuitState; @@ -39,12 +37,8 @@ use crate::spinner::Spinner; use crate::sub_commands::tui::*; use crate::ui::style::Theme; use crate::watcher::RepoWatcher; -use anyhow::{bail, Result}; -use crossbeam_channel::{never, tick, unbounded, Receiver, Select}; -use crossterm::{ - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, -}; +use anyhow::Result; +use crossbeam_channel::{never, tick, unbounded}; use gnostr_asyncgit::{ sync::{utils::repo_work_dir, RepoPath}, AsyncGitNotification, @@ -52,6 +46,12 @@ use gnostr_asyncgit::{ use scopetime; use scopetime::scope_time; +use crate::blockhash::blockhash_async; +use crate::blockheight::blockheight_async; +use crate::weeble::weeble_async; +use crate::wobble::wobble_async; +use std::env; +use tracing::debug; /// # Errors /// /// Will return `Err` if `filename` does not exist or the user does not have @@ -108,6 +108,34 @@ pub async fn run_app( log::trace!("app start: {} ms", app_start.elapsed().as_millis()); loop { + let my_string = "hello".to_string(); + let my_string2 = "hello".to_string(); + + //// Check if the current second is odd + //let handle = tokio::spawn(async { + // let now = Local::now(); + + // // Get the current second + // let current_second = now.second(); + + // if current_second % 2 != 0 { + // debug!("Current second ({}) is odd!", current_second); + // //env::set_var("BLOCKHEIGHT", &blockheight_async().await); + // env::set_var("WEEBLE", &weeble_async().await.unwrap().to_string()); + // //env::set_var("BLOCKHASH", &blockhash_async().await); + // } else { + // debug!( + // "Current second ({}) is even. Skipping this iteration.", + // current_second + // ); + // } + //}); + + //debug!("Still running other things while the task is awaited..."); + + //handle.await.unwrap_or(()); // Wait for the async task to complete + //debug!("All done!"); + let event = if first_update { first_update = false; QueueEvent::Notify @@ -125,6 +153,35 @@ pub async fn run_app( { if matches!(event, QueueEvent::SpinnerUpdate) { spinner.update(); + + //let my_string = "hello".to_string(); + //let my_string2 = "hello".to_string(); + + //// Check if the current second is odd + //let handle = tokio::spawn(async { + // let now = Local::now(); + + // // Get the current second + // let current_second = now.second(); + + // if current_second % 2 != 0 { + // debug!("Current second ({}) is odd!", current_second); + // //env::set_var("BLOCKHEIGHT", &blockheight_async().await); + // env::set_var("WEEBLE", &weeble_async().await.unwrap().to_string()); + // //env::set_var("BLOCKHASH", &blockhash_async().await); + // } else { + // debug!( + // "Current second ({}) is even. Skipping this iteration.", + // current_second + // ); + // } + //}); + + //debug!("Still running other things while the task is awaited..."); + + //handle.await.unwrap_or(()); // Wait for the async task to complete + //debug!("All done!"); + spinner.draw(terminal)?; continue; } diff --git a/src/lib/utils.rs b/src/lib/utils/mod.rs similarity index 99% rename from src/lib/utils.rs rename to src/lib/utils/mod.rs index fc7a8f4279..1afedf9cb0 100644 --- a/src/lib/utils.rs +++ b/src/lib/utils/mod.rs @@ -1,3 +1,6 @@ +pub mod pwd; +pub mod retry; + use log::{debug, error}; use nostr_sdk_0_32_0::prelude::*; use serde_json; diff --git a/src/lib/utils/pwd.rs b/src/lib/utils/pwd.rs new file mode 100644 index 0000000000..94bbc7bd92 --- /dev/null +++ b/src/lib/utils/pwd.rs @@ -0,0 +1,35 @@ +use std::process::Command; +pub fn pwd() -> Result { + let get_pwd = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "echo %cd%"]) + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "macos") { + Command::new("sh") + .arg("-c") + .arg("echo ${PWD##*/}") + .output() + .expect("failed to execute process") + } else if cfg!(target_os = "linux") { + Command::new("sh") + .arg("-c") + .arg("echo ${PWD##*/}") + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg("echo ${PWD##*/}") + .output() + .expect("failed to execute process") + }; + + let mut _pwd = String::from_utf8(get_pwd.stdout) + .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned()) + .unwrap(); + + let _mutable_string = String::new(); + let mutable_string = _pwd.clone(); + Ok(format!("{}", mutable_string)) +} //end pwd() diff --git a/src/lib/utils/retry.rs b/src/lib/utils/retry.rs new file mode 100644 index 0000000000..e4b3f28e93 --- /dev/null +++ b/src/lib/utils/retry.rs @@ -0,0 +1,561 @@ +//! # GnostrRetry +//! +//! `gnostr::utils::retry` is a Rust library that provides utilities for retrying operations with different strategies. +//! +//! This library provides several retry strategies, including linear, exponential, and their asynchronous versions. You can choose the strategy that best fits your needs. +//! +//! The library is designed to be simple and easy to use. It provides a single enum, `GnostrRetry`, that represents different retry strategies. You can create a new retry strategy by calling one of the `new_*` methods on the `GnostrRetry` enum. +//! +//! The library provides a `run` method that takes a closure and runs the operation with the specified retry strategy. The `run` method returns the result of the operation, or an error if the operation fails after all retries. +//! +//! The run method expects the closure to return a `Result` type. The `Ok` variant should contain the result of the operation, and the `Err` variant should contain the error that occurred during the operation. +//! +//! # Features +//! +//! * **Linear Retry**: In this strategy, the delay between retries is constant. +//! * **Exponential Retry**: In this strategy, the delay between retries doubles after each retry. +//! * **Linear Async Retry**: This is an asynchronous version of the linear retry strategy. This feature is only available when the `async` feature is enabled. +//! * **Exponential Async Retry**: This is an asynchronous version of the exponential retry strategy. This feature is only available when the `async` feature is enabled. +//! +//! # Examples +//! +//! ``` +//! use gnostr::utils::retry::GnostrRetry; +//! +//! fn my_sync_fn(_n: &str) -> Result<(), std::io::Error> { +//! Err(std::io::Error::new(std::io::ErrorKind::Other, "generic error")) +//! } +//! +//! // Retry the operation with a linear strategy (1 second delay, 2 retries) +//! let retry_strategy = GnostrRetry::new_linear(1, 2); +//! let result = retry_strategy.run(|| my_sync_fn("Hi")); +//! assert!(result.is_err()); +//! +//! ``` +//! +//! # Asynchronous Example +//! +//! ```rust +//! use gnostr::utils::retry::GnostrRetry; +//! +//! async fn my_async_fn(_n: u64) -> Result<(), std::io::Error> { +//! Err(std::io::Error::new(std::io::ErrorKind::Other, "generic error")) +//! } +//! +//! #[tokio::main] +//! async fn main() { +//! // Retry the operation with an exponential strategy (1 second delay, 2 retries) +//! let retry_strategy = GnostrRetry::new_exponential_async(1, 2); +//! let result = retry_strategy.run_async(|| my_async_fn(42)).await; +//! assert!(result.is_err()); +//! +//! } +//! ``` +//! # Usage +//! +//! Add this to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! gnostr = "*" +//! ``` +//! +//! Then, add this to your crate root (`main.rs` or `lib.rs`): +//! +//! ```rust +//! use gnostr::utils::retry; +//! ``` +//! +//! Now, you can use the `GnostrRetry` enum to create a retry strategy: +//! +//! ```rust +//! use gnostr::utils::retry::GnostrRetry; +//! +//! let retry_strategy = GnostrRetry::new_linear(100, 5); +//! ``` +//! +//! # License +//! +//! This project is licensed under the MIT License. + +#![deny(missing_docs)] +use std::fmt::Debug; +//#[cfg(feature = "async")] +use std::future::Future; +#[derive(Debug, Copy, Clone)] + +/// `GnostrRetry` is an enum representing different kinds of retry strategies. +pub enum GnostrRetry { + /// Represents a linear retry strategy. + Linear { + #[doc(hidden)] + /// The delay between retries in seconds. + delay: u64, + #[doc(hidden)] + /// The number of retries. + retries: u64, + }, + /// Represents an exponential retry strategy. + Exponential { + /// The delay between retries in seconds. + #[doc(hidden)] + delay: u64, + /// The number of retries. + #[doc(hidden)] + retries: u64, + }, + /// Represents an asynchronous version of the linear retry strategy. + /// + /// This variant is only available when the `async` feature is enabled. + //#[cfg(feature = "async")] + LinearAsync { + /// The delay between retries in seconds. + #[doc(hidden)] + delay: u64, + /// The number of retries. + #[doc(hidden)] + retries: u64, + }, + /// Represents an asynchronous version of the exponential retry strategy. + /// + /// This variant is only available when the `async` feature is enabled. + //#[cfg(feature = "async")] + ExponentialAsync { + /// The delay between retries in seconds. + #[doc(hidden)] + delay: u64, + /// The number of retries. + #[doc(hidden)] + retries: u64, + }, +} + +impl GnostrRetry { + /// Creates a new `GnostrRetry::Linear` variant with the specified delay and number of retries. + /// + /// # Arguments + /// + /// * `delay` - The delay between retries in seconds. + /// * `retries` - The number of retries. + /// + /// # Examples + /// + /// ``` + /// use gnostr::utils::retry::GnostrRetry; + /// + /// let retry_strategy = GnostrRetry::new_linear(100, 5); + /// ``` + pub fn new_linear(delay: u64, retries: u64) -> Self { + GnostrRetry::Linear { delay, retries } + } + + /// Creates a new `GnostrRetry::Exponential` variant with the specified initial delay and number of retries. + /// + /// # Arguments + /// + /// * `delay` - The delay between retries in . The delay doubles after each retry. + /// * `retries` - The number of retries. + /// + /// # Examples + /// + /// ``` + /// use gnostr::utils::retry::GnostrRetry; + /// + /// let retry_strategy = GnostrRetry::new_exponential(100, 5); + /// ``` + pub fn new_exponential(delay: u64, retries: u64) -> Self { + GnostrRetry::Exponential { delay, retries } + } + + /// Creates a new `GnostrRetry::LinearAsync` variant with the specified delay and number of retries. + /// + /// # Arguments + /// + /// * `delay` - The delay between retries in seconds. + /// * `retries` - The number of retries. + /// + /// # Examples + /// + /// ``` + /// use gnostr::utils::retry::GnostrRetry; + /// + /// let retry_strategy = GnostrRetry::new_linear_async(100, 5); + /// ``` + //#[cfg(feature = "async")] + pub fn new_linear_async(delay: u64, retries: u64) -> Self { + GnostrRetry::LinearAsync { delay, retries } + } + + /// Creates a new `GnostrRetry::ExponentialAsync` variant with the specified initial delay and number of retries. + /// + /// # Arguments + /// + /// * `delay` - The delay between retries in seconds. The delay doubles after each retry. + /// * `retries` - The number of retries. + /// + /// # Examples + /// + /// ``` + /// use gnostr::utils::retry::GnostrRetry; + /// + /// let retry_strategy = GnostrRetry::new_exponential_async(100, 5); + /// ``` + //#[cfg(feature = "async")] + pub fn new_exponential_async(delay: u64, retries: u64) -> Self { + GnostrRetry::ExponentialAsync { delay, retries } + } + + /// Runs the provided function `f` with a retry strategy. + /// + /// This function takes a function `f` that implements the `SyncReturn` trait and runs it with a retry strategy. The `SyncReturn` trait is implemented for `FnMut` closures, which can mutate their captured variables and can be called multiple times. + /// + /// The function `f` should return a `Result` with the operation's result or error. The types of the result and error are determined by the `SyncReturn` trait's associated types `Item` and `Error`. + /// + /// # Errors + /// + /// Will return an error if the operation fails after all retries. + pub fn run(&self, f: T) -> Result<::Item, ::Error> + where + T: SyncReturn, + { + Retry::run(f, *self) + } + + /// Runs the provided function `f` with a retry strategy. + /// + /// This function takes a function `f` that implements the `AsyncReturn` trait and runs it with a retry strategy. The `AsyncReturn` trait is implemented for `FnMut` closures, which can mutate their captured variables and can be called multiple times. This function is only available when the `async` feature is enabled. + /// + /// The function `f` should return a `Result` with the operation's result or error. The types of the result and error are determined by the `SyncReturn` trait's associated types `Item` and `Error`. + /// # Errors + /// + /// Will return an error if the operation fails after all retries. + //#[cfg(feature = "async")] + pub async fn run_async<'a, T>( + &'a self, + f: T, + ) -> Result<::Item, ::Error> + where + T: AsyncReturn + 'a + 'static, + { + Retry::run_async(f, *self).await + } + + fn get_retries(&self) -> u64 { + match self { + GnostrRetry::Linear { retries, .. } => *retries, + GnostrRetry::Exponential { retries, .. } => *retries, + //#[cfg(feature = "async")] + GnostrRetry::LinearAsync { retries, .. } => *retries, + //#[cfg(feature = "async")] + GnostrRetry::ExponentialAsync { retries, .. } => *retries, + } + } + + fn get_delay(&self) -> u64 { + match self { + GnostrRetry::Linear { delay, .. } => *delay, + GnostrRetry::Exponential { delay, .. } => *delay, + //#[cfg(feature = "async")] + GnostrRetry::LinearAsync { delay, .. } => *delay, + //#[cfg(feature = "async")] + GnostrRetry::ExponentialAsync { delay, .. } => *delay, + } + } + + fn linear(x: u64) -> u64 { + x + } + + fn exponential(x: u64) -> u64 { + 2u64.pow(x.try_into().unwrap_or_default()) + } + + fn retry_fn(&self) -> fn(u64) -> u64 { + match self { + GnostrRetry::Linear { .. } => Self::linear, + GnostrRetry::Exponential { .. } => Self::exponential, + //#[cfg(feature = "async")] + GnostrRetry::LinearAsync { .. } => Self::linear, + //#[cfg(feature = "async")] + GnostrRetry::ExponentialAsync { .. } => Self::exponential, + } + } +} + +fn do_retry(mut f: F, t: GnostrRetry) -> Result +where + F: FnMut() -> Result, +{ + let mut retries: u64 = 0; + loop { + match f() { + Ok(v) => return Ok(v), + Err(e) => { + if retries >= t.get_retries() { + return Err(e); + } + retries += 1; + std::thread::sleep(std::time::Duration::from_secs((t.retry_fn())( + t.get_delay(), + ))); + } + } + } +} + +//#[cfg(feature = "async")] +async fn do_retry_async(mut f: F, t: GnostrRetry) -> Result +where + F: FnMut() -> std::pin::Pin>>>, +{ + let mut retries = 0; + loop { + match f().await { + Ok(v) => return Ok(v), + Err(e) => { + if retries >= t.get_retries() { + return Err(e); + } + retries += 1; + tokio::time::sleep(std::time::Duration::from_secs((t.retry_fn())( + t.get_delay(), + ))) + .await; + } + } + } +} +struct Retry; +impl Retry { + //#[cfg(feature = "async")] + async fn run_async( + mut f: T, + t: GnostrRetry, + ) -> Result<::Item, ::Error> + where + T: AsyncReturn + 'static, + { + do_retry_async(move || Box::pin(f.run()), t).await + } + + fn run(mut f: T, t: GnostrRetry) -> Result<::Item, ::Error> + where + T: SyncReturn, + { + do_retry(move || f.run(), t) + } +} +/// The `AsyncReturn` trait is used for operations that need to return a value asynchronously. +/// +/// This trait provides a single method, `run`, which takes no arguments and returns a `Future` that resolves to a `Result` with the operation's result or error. Both the result and error types must implement the `Debug` trait. +/// +/// This trait is only available when the `async` feature is enabled. +/// +/// # Associated Types +/// +/// * `Item`: The type of the value returned by the `run` method. This type must implement the `Debug` trait. +/// * `Error`: The type of the error returned by the `run` method. This type must also implement the `Debug` trait. +/// * `Future`: The type of the `Future` returned by the `run` method. This `Future` should resolve to a `Result`. +/// +/// # Methods +/// +/// * `run`: Performs the operation and returns a `Future` that resolves to the result. +/// +/// # Examples +/// +/// ``` +/// use gnostr::utils::retry::AsyncReturn; +/// use std::fmt::Debug; +/// use futures::future::ready; +/// +/// struct MyOperation; +/// +/// impl AsyncReturn for MyOperation { +/// type Item = i32; +/// type Error = &'static str; +/// type Future = futures::future::Ready>; +/// +/// fn run(&mut self) -> Self::Future { +/// // Perform the operation and return the result... +/// ready(Ok(42)) +/// } +/// } +/// +/// let mut operation = MyOperation; +/// let future = operation.run(); +/// ``` +//#[cfg(feature = "async")] +pub trait AsyncReturn { + /// The type of the value returned by the `run` method. + type Item: Debug; + /// The type of the error returned by the `run` method. + type Error: Debug; + /// The type of the `Future` returned by the `run` method. + type Future: Future>; + + /// Performs the operation and returns a `Future` that resolves to the result. + fn run(&mut self) -> Self::Future; +} + +//#[cfg(feature = "async")] +impl>, F: FnMut() -> T> AsyncReturn for F { + type Item = I; + type Error = E; + type Future = T; + fn run(&mut self) -> Self::Future { + self() + } +} + +/// The `SyncReturn` trait is used for operations that need to return a value synchronously. +/// +/// This trait provides a single method, `run`, which takes no arguments and returns a `Result` with the operation's result or error. Both the result and error types must implement the `Debug` trait. +/// +/// # Type Parameters +/// +/// * `Item`: The type of the value returned by the `run` method. This type must implement the `Debug` trait. +/// * `Error`: The type of the error returned by the `run` method. This type must also implement the `Debug` trait. +/// +/// # Methods +/// +/// * `run`: Performs the operation and returns the result. +/// +/// # Errors +/// +/// If the operation fails, this method returns `Err` containing the error. The type of the error is defined by the `Error` associated type. +/// +/// # Examples +/// +/// ``` +/// use gnostr::utils::retry::SyncReturn; +/// use std::fmt::Debug; +/// +/// struct MyOperation; +/// +/// impl SyncReturn for MyOperation { +/// type Item = i32; +/// type Error = &'static str; +/// +/// fn run(&mut self) -> Result { +/// // Perform the operation and return the result... +/// Ok(42) +/// } +/// } +/// +/// let mut operation = MyOperation; +/// assert_eq!(operation.run(), Ok(42)); +/// ``` +pub trait SyncReturn { + /// The type of the value returned by the `run` method. + type Item: Debug; + /// The type of the error returned by the `run` method. + type Error: Debug; + + /// Performs the operation and returns the result. + fn run(&mut self) -> Result; +} + +impl Result> SyncReturn for F { + type Item = I; + type Error = E; + fn run(&mut self) -> Result { + self() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[derive(Debug, Clone)] + struct NotCopy { + pub _n: usize, + } + + fn to_retry_not_copy(n: &NotCopy) -> Result<(), std::io::Error> { + let _r = n.clone(); + + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "generic error", + )) + } + + fn to_retry(_n: usize) -> Result<(), std::io::Error> { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "generic error", + )) + } + + //#[cfg(feature = "async")] + async fn to_retry_async(n: usize) -> Result<(), std::io::Error> { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "generic error", + )) + } + + #[test] + fn test_linear() { + let retries = 2; + let delay = 1; + let instant = std::time::Instant::now(); + let s = GnostrRetry::Linear { retries, delay }.run(|| to_retry(1)); + assert!(s.is_err()); + let elapsed = instant.elapsed(); + assert!(elapsed.as_secs() >= retries * delay); + } + + #[test] + fn test_expontential() { + let retries = 2; + let delay = 1; + let instant = std::time::Instant::now(); + let s = GnostrRetry::Exponential { retries, delay }.run(|| to_retry(1)); + assert!(s.is_err()); + let elapsed = instant.elapsed(); + assert!(elapsed.as_secs() >= retries * delay); + } + + #[test] + fn test_not_copy() { + let retries = 2; + let delay = 1; + let not_copy = NotCopy { _n: 1 }; + let instant = std::time::Instant::now(); + + let s = GnostrRetry::Linear { retries, delay }.run(|| to_retry_not_copy(¬_copy)); + assert!(s.is_err()); + let elapsed = instant.elapsed(); + assert!(elapsed.as_secs() >= retries * delay); + } + + //#[cfg(feature = "async")] + #[tokio::test] + async fn test_linear_async() { + let retries = 2; + let delay = 1; + let instant = std::time::Instant::now(); + let s = GnostrRetry::LinearAsync { retries, delay } + .run_async(|| to_retry_async(1)) + .await; + assert!(s.is_err()); + let elapsed = instant.elapsed(); + assert!(elapsed.as_secs() >= retries * delay); + } + + //#[cfg(feature = "async")] + #[tokio::test] + async fn test_expontential_async() { + let retries = 2; + let delay = 1; + let instant = std::time::Instant::now(); + let s = GnostrRetry::ExponentialAsync { retries, delay } + .run_async(|| to_retry_async(1)) + .await; + assert!(s.is_err()); + let elapsed = instant.elapsed(); + assert!(elapsed.as_secs() >= retries * delay); + } +} diff --git a/src/lib/weeble.rs b/src/lib/weeble.rs index 16ccb5dc48..3975b28c66 100644 --- a/src/lib/weeble.rs +++ b/src/lib/weeble.rs @@ -1,11 +1,15 @@ +use crate::blockheight::{blockheight_async, blockheight_sync}; use log::debug; -use reqwest; -use reqwest::Url; -use std::io::Read; +use std::env; use std::time::SystemTime; /// pub fn weeble() -> Result /// pub fn weeble() -> Result { + weeble_sync() +} +/// pub fn weeble_sync() -> Result +/// +pub fn weeble_sync() -> Result { //! weeble = utc_secs / blockheight let since_the_epoch = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -14,18 +18,28 @@ pub fn weeble() -> Result { let subsec_millis = since_the_epoch.subsec_millis() as u64; let _now_millis = seconds * 1000 + subsec_millis; debug!("now millis: {}", seconds * 1000 + subsec_millis); - - let url = Url::parse("https://mempool.space/api/blocks/tip/height").unwrap(); - let mut res = reqwest::blocking::get(url).unwrap(); - - let mut tmp_string = String::new(); - res.read_to_string(&mut tmp_string).unwrap(); - let tmp_u64 = tmp_string.parse::().unwrap_or(0); - - //TODO:impl gnostr-weeble_millis - //gnostr-chat uses millis - //let weeble = now_millis as f64 / tmp_u64 as f64; + let blockheight = blockheight_sync(); + let tmp_u64 = blockheight.parse::().unwrap_or(0); let weeble = seconds as f64 / tmp_u64 as f64; + env::set_var("WEEBLE", weeble.clone().to_string()); + return Ok(weeble.floor()); +} +/// pub fn weeble_millis_sync() -> Result +/// +pub fn weeble_millis_sync() -> Result { + //! weeble = utc_secs / blockheight + let since_the_epoch = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("get millis error"); + let seconds = since_the_epoch.as_secs(); + let subsec_millis = since_the_epoch.subsec_millis() as u64; + let now_millis = seconds * 1000 + subsec_millis; + debug!("now millis: {}", seconds * 1000 + subsec_millis); + let blockheight = blockheight_sync(); + let tmp_u64 = blockheight.parse::().unwrap_or(0); + //gnostr-chat uses millis + let weeble = now_millis as f64 / tmp_u64 as f64; + env::set_var("WEEBLE_MILLIS", weeble.clone().to_string()); return Ok(weeble.floor()); } /// pub async fn weeble_async() -> Result @@ -39,25 +53,15 @@ pub async fn weeble_async() -> Result { let subsec_millis = since_the_epoch.subsec_millis() as u64; let _now_millis = seconds * 1000 + subsec_millis; debug!("now millis: {}", seconds * 1000 + subsec_millis); - - let url = Url::parse("https://mempool.space/api/blocks/tip/height").unwrap(); - let res = reqwest::get(url) - .await - .map_err(|_| ascii::AsciiChar::Null)?; // Error handling - - let tmp_string = res.text().await.map_err(|_| ascii::AsciiChar::Null)?; // Error handling - - let tmp_u64 = tmp_string.parse::().unwrap_or(0); - - //TODO:impl gnostr-weeble_millis - //gnostr-chat uses millis - //let weeble = now_millis as f64 / tmp_u64 as f64; + let blockheight = blockheight_async(); + let tmp_u64 = blockheight.await.parse::().unwrap_or(0); let weeble = seconds as f64 / tmp_u64 as f64; + env::set_var("WEEBLE", weeble.clone().to_string()); return Ok(weeble.floor()); } -/// pub fn weeble_millis() -> Result +/// pub fn weeble_millis_async() -> Result /// -pub fn weeble_millis() -> Result { +pub async fn weeble_millis_async() -> Result { //! weeble = utc_secs / blockheight let since_the_epoch = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -66,14 +70,8 @@ pub fn weeble_millis() -> Result { let subsec_millis = since_the_epoch.subsec_millis() as u64; let now_millis = seconds * 1000 + subsec_millis; debug!("now millis: {}", seconds * 1000 + subsec_millis); - - let url = Url::parse("https://mempool.space/api/blocks/tip/height").unwrap(); - let mut res = reqwest::blocking::get(url).unwrap(); - - let mut tmp_string = String::new(); - res.read_to_string(&mut tmp_string).unwrap(); - let tmp_u64 = tmp_string.parse::().unwrap_or(0); - + let blockheight = blockheight_async().await; + let tmp_u64 = blockheight.parse::().unwrap_or(0); //gnostr-chat uses millis let weeble = now_millis as f64 / tmp_u64 as f64; return Ok(weeble.floor()); diff --git a/src/lib/wobble.rs b/src/lib/wobble.rs index 7f60a8bc67..34978420b8 100644 --- a/src/lib/wobble.rs +++ b/src/lib/wobble.rs @@ -1,11 +1,15 @@ +use crate::blockheight::{blockheight_async, blockheight_sync}; use log::debug; -use reqwest; -use reqwest::Url; -use std::io::Read; +use std::env; use std::time::SystemTime; /// pub fn wobble() -> Result /// pub fn wobble() -> Result { + wobble_sync() +} +/// pub fn wobble_sync() -> Result +/// +pub fn wobble_sync() -> Result { //! wobble = utc_secs % blockheight let since_the_epoch = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -14,24 +18,34 @@ pub fn wobble() -> Result { let subsec_millis = since_the_epoch.subsec_millis() as u64; let _now_millis = seconds * 1000 + subsec_millis; debug!("now millis: {}", seconds * 1000 + subsec_millis); - - let url = Url::parse("https://mempool.space/api/blocks/tip/height").unwrap(); - let mut res = reqwest::blocking::get(url).unwrap(); - - let mut tmp_string = String::new(); - res.read_to_string(&mut tmp_string).unwrap(); - let tmp_u64 = tmp_string.parse::().unwrap_or(0); - - //TODO:impl gnostr-wobble_millis - //gnostr-chat uses millis - //let wobble = now_millis as f64 % tmp_u64 as f64; + let blockheight = blockheight_sync(); + let tmp_u64 = blockheight.parse::().unwrap_or(0); let wobble = seconds as f64 % tmp_u64 as f64; + env::set_var("WOBBLE", wobble.to_string()); + return Ok(wobble.floor()); +} +/// pub fn wobble_millis_sync() -> Result +/// +pub fn wobble_millis_sync() -> Result { + //! wobble = utc_secs % blockheight + let since_the_epoch = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("get millis error"); + let seconds = since_the_epoch.as_secs(); + let subsec_millis = since_the_epoch.subsec_millis() as u64; + let now_millis = seconds * 1000 + subsec_millis; + debug!("now millis: {}", seconds * 1000 + subsec_millis); + let blockheight = blockheight_sync(); + let tmp_u64 = blockheight.parse::().unwrap_or(0); + //gnostr-chat uses millis + let wobble = now_millis as f64 % tmp_u64 as f64; + env::set_var("WOBBLE_MILLIS", wobble.to_string()); return Ok(wobble.floor()); } /// pub async fn wobble_async() -> Result /// pub async fn wobble_async() -> Result { - //! wobble = utc_secs % blockheight + //! wobble = utc_secs / blockheight let since_the_epoch = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("get millis error"); @@ -39,28 +53,16 @@ pub async fn wobble_async() -> Result { let subsec_millis = since_the_epoch.subsec_millis() as u64; let _now_millis = seconds * 1000 + subsec_millis; debug!("now millis: {}", seconds * 1000 + subsec_millis); - - let url = Url::parse("https://mempool.space/api/blocks/tip/height").unwrap(); - - let res = reqwest::get(url) - .await - .map_err(|_| ascii::AsciiChar::Null)?; // Error handling - - // Use .text().await to read the body asynchronously - let tmp_string = res.text().await.map_err(|_| ascii::AsciiChar::Null)?; // Error handling - - let tmp_u64 = tmp_string.parse::().unwrap_or(0); - - //TODO:impl gnostr-wobble_millis - //gnostr-chat uses millis - //let wobble = now_millis as f64 % tmp_u64 as f64; + let blockheight = blockheight_async(); + let tmp_u64 = blockheight.await.parse::().unwrap_or(0); let wobble = seconds as f64 % tmp_u64 as f64; + env::set_var("WOBBLE", wobble.to_string()); return Ok(wobble.floor()); } -/// pub fn wobble_millis() -> Result +/// pub fn wobble_millis_async() -> Result /// -pub fn wobble_millis() -> Result { - //! wobble = utc_secs % blockheight +pub async fn wobble_millis_async() -> Result { + //! wobble = utc_secs / blockheight let since_the_epoch = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("get millis error"); @@ -68,14 +70,8 @@ pub fn wobble_millis() -> Result { let subsec_millis = since_the_epoch.subsec_millis() as u64; let now_millis = seconds * 1000 + subsec_millis; debug!("now millis: {}", seconds * 1000 + subsec_millis); - - let url = Url::parse("https://mempool.space/api/blocks/tip/height").unwrap(); - let mut res = reqwest::blocking::get(url).unwrap(); - - let mut tmp_string = String::new(); - res.read_to_string(&mut tmp_string).unwrap(); - let tmp_u64 = tmp_string.parse::().unwrap_or(0); - + let blockheight = blockheight_async().await; + let tmp_u64 = blockheight.parse::().unwrap_or(0); //gnostr-chat uses millis let wobble = now_millis as f64 % tmp_u64 as f64; return Ok(wobble.floor()); diff --git a/src/main.rs b/src/main.rs index 5a7e80cb4f..7a7893e811 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use gnostr::cli::{get_app_cache_path, setup_logging, GnostrCli, GnostrCommands}; use gnostr::{blockheight, sub_commands}; use sha2::{Digest, Sha256}; use std::env; -use tracing::{debug, error, info, trace, warn}; +use tracing::{debug, trace}; use tracing_core::metadata::LevelFilter; use tracing_subscriber::FmtSubscriber; @@ -12,7 +12,9 @@ use serde::ser::StdError; #[tokio::main] async fn main() -> Result<(), Box> { - env::set_var("BLOCKHEIGHT", blockheight::blockheight_sync()); + env::set_var("WEEBLE", "0"); + env::set_var("BLOCKHEIGHT", "0"); + env::set_var("WOBBLE", "0"); let mut args: GnostrCli = GnostrCli::parse(); let app_cache = get_app_cache_path(); let _logging = if args.logging { @@ -69,6 +71,10 @@ async fn main() -> Result<(), Box> { debug!("sub_command_args:{:?}", sub_command_args); sub_commands::chat::chat(&args.nsec.unwrap().to_string(), sub_command_args).await } + Some(GnostrCommands::Legit(sub_command_args)) => { + debug!("sub_command_args:{:?}", sub_command_args); + sub_commands::legit::legit(sub_command_args).await + } Some(GnostrCommands::Ngit(sub_command_args)) => { debug!("sub_command_args:{:?}", sub_command_args); sub_commands::ngit::ngit(sub_command_args).await