├── .github └── workflows │ ├── build.yml │ ├── dart.yml │ ├── docker-publish.yml │ └── post-release.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.remote ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin ├── helloworld.dart └── nostr_console.dart ├── lib ├── console_ui.dart ├── event_ds.dart ├── nip_019.dart ├── relays.dart ├── settings.dart ├── tree_ds.dart ├── user.dart └── utils.dart ├── pubspec.yaml ├── scripts ├── all_nostr_events.txt ├── announce.sh ├── gotoChannel.sh ├── output_test_servers.txt ├── readme.md ├── relay_list_all.txt ├── relay_list_best.txt ├── relay_list_nostr_info.txt ├── send_request.sh └── test_servers.sh ├── test └── nostr_console_test.dart └── test_event_file.csv /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build binaries 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build-linux: 11 | name: Build for Linux 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | arch: [amd64, arm64] 16 | fail-fast: false 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Cache Dart dependencies 22 | id: cache-deps 23 | uses: actions/cache@v4 24 | with: 25 | path: /tmp/dart 26 | key: ${{ runner.os }}-dart-deps-${{ matrix.arch }} 27 | 28 | - name: Create temporary dir for Dart deps 29 | if: steps.cache-deps.outputs.cache-hit != 'true' 30 | run: mkdir -p /tmp/dart 31 | 32 | - name: Set up QEMU 33 | uses: docker/setup-qemu-action@v2 34 | 35 | - name: Install dependencies 36 | run: 37 | docker run --platform linux/${{ matrix.arch }} -i --rm -v /tmp/dart:/tmp/dart -e PUB_CACHE=/tmp/dart -v $(pwd):/work --workdir /work dart:stable dart pub get 38 | 39 | - name: Build 40 | run: 41 | docker run --platform linux/${{ matrix.arch }} -i --rm -v /tmp/dart:/tmp/dart -e PUB_CACHE=/tmp/dart -v $(pwd):/work --workdir /work dart:stable dart compile exe bin/nostr_console.dart --output bin/nostr_console_linux_${{ matrix.arch }} 42 | 43 | - name: Make file executable 44 | run: | 45 | sudo chown -R $(whoami) bin 46 | chmod 755 bin/nostr_console_linux_${{ matrix.arch }} 47 | 48 | 49 | - name: Archive production artifacts 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: nostr_console_linux_${{ matrix.arch }} 53 | path: | 54 | bin/nostr_console_linux_${{ matrix.arch }} 55 | 56 | build-others: 57 | name: Build 58 | runs-on: ${{ matrix.os }}-latest 59 | strategy: 60 | matrix: 61 | os: [macos, windows] 62 | fail-fast: false 63 | env: 64 | PUB_CACHE=: tmp/dart 65 | 66 | steps: 67 | - uses: actions/checkout@v4 68 | 69 | - name: Cache Dart dependencies 70 | id: cache-deps 71 | uses: actions/cache@v4 72 | with: 73 | path: /tmp/dart 74 | key: ${{ runner.os }}-dart-deps-amd64 75 | 76 | - name: Create temporary dir for Dart deps 77 | if: steps.cache-deps.outputs.cache-hit != 'true' 78 | run: mkdir -p /tmp/dart 79 | 80 | - name: Install Dart 81 | uses: dart-lang/setup-dart@v1 82 | 83 | - name: Install dependencies 84 | run: dart pub get 85 | 86 | - name: Build (macOS) 87 | if: ${{ matrix.os == 'macos' }} 88 | run: dart compile exe bin/nostr_console.dart --output bin/nostr_console_${{ matrix.os }}_amd64 89 | 90 | # This has the .exe extension 91 | # There is probably a better way to do this 92 | - name: Build (Windows) 93 | if: ${{ matrix.os == 'windows' }} 94 | run: dart compile exe bin/nostr_console.dart --output bin/nostr_console_${{ matrix.os }}_amd64.exe 95 | 96 | - name: Make file executable (macOS only) 97 | if: ${{ matrix.os == 'macos' }} 98 | run: chmod 755 bin/nostr_console_${{ matrix.os }}_amd64 99 | 100 | - name: Archive production artifacts (macOS) 101 | if: ${{ matrix.os == 'macos' }} 102 | uses: actions/upload-artifact@v4 103 | with: 104 | name: nostr_console_${{ matrix.os }}_amd64 105 | path: | 106 | bin/nostr_console_${{ matrix.os }}_amd64 107 | 108 | - name: Archive production artifacts (Windows) 109 | if: ${{ matrix.os == 'windows' }} 110 | uses: actions/upload-artifact@v4 111 | with: 112 | name: nostr_console_${{ matrix.os }}_amd64.exe 113 | path: | 114 | bin/nostr_console_${{ matrix.os }}_amd64.exe 115 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Compile & test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | fail-fast: false 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | # Note: This workflow uses the latest stable version of the Dart SDK. 21 | # You can specify other versions if desired, see documentation here: 22 | # https://github.com/dart-lang/setup-dart/blob/main/README.md 23 | - uses: dart-lang/setup-dart@v1 24 | 25 | - name: Install dependencies 26 | run: dart pub get 27 | 28 | # Uncomment this step to verify the use of 'dart format' on each commit. 29 | # - name: Verify formatting 30 | # run: dart format --output=none --set-exit-if-changed . 31 | 32 | # Consider passing '--fatal-infos' for slightly stricter analysis. 33 | - name: Analyze project source 34 | run: dart analyze --no-fatal-warnings 35 | 36 | # Your project will need to have tests in test/ and a dependency on 37 | # package:test for this step to succeed. Note that Flutter projects will 38 | # want to change this to 'flutter test'. 39 | - name: Run tests 40 | run: dart test 41 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build & publish Docker container images 2 | 3 | on: 4 | schedule: 5 | - cron: '22 17 * * *' 6 | push: 7 | branches: [ "main" ] 8 | # Publish semver tags as releases. 9 | tags: [ 'v*.*.*' ] 10 | pull_request: 11 | branches: [ "main" ] 12 | 13 | env: 14 | # Use docker.io for Docker Hub if empty 15 | REGISTRY: ghcr.io 16 | # github.repository as / 17 | IMAGE_NAME: ${{ github.repository }} 18 | 19 | 20 | jobs: 21 | build: 22 | 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: read 26 | packages: write 27 | # This is used to complete the identity challenge 28 | # with sigstore/fulcio when running outside of PRs. 29 | id-token: write 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v3 34 | 35 | # Install the cosign tool except on PR 36 | # https://github.com/sigstore/cosign-installer 37 | - name: Install cosign 38 | if: github.event_name != 'pull_request' 39 | uses: sigstore/cosign-installer@main 40 | with: 41 | cosign-release: 'v1.13.1' 42 | 43 | 44 | - name: Set up QEMU 45 | uses: docker/setup-qemu-action@v3 46 | - name: Set up Docker Buildx 47 | uses: docker/setup-buildx-action@v3 48 | 49 | # Login against a Docker registry except on PR 50 | # https://github.com/docker/login-action 51 | - name: Log into registry ${{ env.REGISTRY }} 52 | if: github.event_name != 'pull_request' 53 | uses: docker/login-action@v3 54 | with: 55 | registry: ${{ env.REGISTRY }} 56 | username: ${{ github.actor }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | # Extract metadata (tags, labels) for Docker 60 | # https://github.com/docker/metadata-action 61 | - name: Extract Docker metadata 62 | id: meta 63 | uses: docker/metadata-action@v3 64 | with: 65 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 66 | 67 | # Build and push Docker image with Buildx (don't push on PR) 68 | # https://github.com/docker/build-push-action 69 | - name: Build and push Docker image 70 | id: build-and-push 71 | uses: docker/build-push-action@v3 72 | with: 73 | context: . 74 | push: ${{ github.event_name != 'pull_request' }} 75 | tags: ${{ steps.meta.outputs.tags }} 76 | labels: ${{ steps.meta.outputs.labels }} 77 | cache-from: type=gha 78 | cache-to: type=gha,mode=max 79 | platforms: linux/amd64,linux/arm64 80 | 81 | 82 | - name: Sign the published Docker image 83 | if: ${{ github.event_name != 'pull_request' }} 84 | env: 85 | COSIGN_EXPERIMENTAL: "true" 86 | # This step uses the identity token to provision an ephemeral certificate 87 | # against the sigstore community Fulcio instance. 88 | run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} 89 | -------------------------------------------------------------------------------- /.github/workflows/post-release.yml: -------------------------------------------------------------------------------- 1 | name: create release digests 2 | 3 | on: 4 | release: 5 | types: [ published] 6 | branches: [ master ] 7 | 8 | jobs: 9 | once: 10 | name: Creating digests 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Digest 14 | uses: MCJack123/ghaction-generate-release-hashes@v1 15 | with: 16 | hash-type: sha1 17 | file-name: nostr_console_digests.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # Avoid committing generated Javascript files: 15 | *.dart.js 16 | *.info.json # Produced by the --dump-info flag. 17 | *.js # When generated by dart2js. Don't specify *.js if your 18 | # project includes source files written in JavaScript. 19 | *.js_ 20 | *.js.deps 21 | *.js.map 22 | del*.txt 23 | bin/nostr_console_win64.exe 24 | all_nostr_events.txt 25 | *.exe 26 | *.zip 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | - 10 July 2022 - receives hard coded users latest events, and latest events from feed if NIP 02 type event is seen of the given user 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # file from: https://hub.docker.com/_/dart/ 2 | 3 | # run using 4 | #docker build -t nostr_console . 5 | #docker run -it nostr_console start 6 | 7 | 8 | # Specify the Dart SDK base image version using dart: (ex: dart:2.12) 9 | FROM dart:stable AS build 10 | 11 | RUN apt -y update && apt -y upgrade 12 | # Resolve app dependencies. 13 | WORKDIR /app 14 | COPY pubspec.* ./ 15 | RUN dart pub get 16 | 17 | COPY . . 18 | RUN dart pub get --offline 19 | RUN dart compile exe bin/nostr_console.dart -o bin/nostr_console 20 | 21 | FROM scratch 22 | COPY --from=build /runtime/ / 23 | COPY --from=build /app/bin/nostr_console /app/bin/ 24 | 25 | ENTRYPOINT [ "/app/bin/nostr_console" ] 26 | 27 | #CMD [ "yarn" ] 28 | -------------------------------------------------------------------------------- /Dockerfile.remote: -------------------------------------------------------------------------------- 1 | # file from: https://hub.docker.com/_/dart/ 2 | 3 | # Specify the Dart SDK base image version using dart: (ex: dart:2.12) 4 | FROM dart:stable AS build 5 | 6 | RUN apt -y update && apt -y upgrade 7 | # Resolve app dependencies. 8 | WORKDIR /app 9 | COPY pubspec.* ./ 10 | RUN dart pub get 11 | 12 | COPY . . 13 | RUN dart pub get --offline 14 | RUN dart compile exe bin/nostr_console.dart -o bin/nostr_console 15 | 16 | FROM scratch 17 | COPY --from=build /runtime/ / 18 | COPY --from=build /app/bin/nostr_console /app/bin/ 19 | 20 | 21 | # build nostr-terminal and invoke it 22 | FROM node:16 23 | 24 | WORKDIR /nostr-terminal 25 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 26 | RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 27 | 28 | RUN apt update -y && apt install -y yarn 29 | RUN npm install node-pty dotenv 30 | 31 | # https://stackoverflow.com/questions/38905135/why-wont-my-docker-entrypoint-sh-execute 32 | RUN git config --global core.autocrlf input 33 | 34 | 35 | RUN git clone https://github.com/vishalxl/nostr-terminal.git 36 | 37 | COPY --from=build /app/bin/nostr_console /nostr-terminal/ 38 | RUN echo "/nostr-terminal/nostr_console --width=120 --align=left" >> /nostr-terminal/console.sh 39 | RUN PATH=$PATH:/nostr-terminal/ 40 | #RUN chmod 755 /nostr-terminal/nostr_console 41 | WORKDIR /nostr-terminal/nostr-terminal 42 | RUN npm install 43 | #ENTRYPOINT ["/nostr-terminal/nostr_console"] 44 | ENTRYPOINT [ "yarn" ] 45 | 46 | #CMD [ "yarn" ] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nostr_console 2 | Nostr console client using Dart 3 | 4 | This is an experimental or pre-alpha software made to show or know what a Nostr network client would look like. It works 90% of the time everytime; less when relays are not working perfectly. 5 | 6 | 7 | # todo 8 | 9 | * [ ] allow faster startup with an argument or config 10 | * [ ] menu should honour --width, its extending way beyond 11 | * [ ] after going to a dm room, screen doesn't clear 12 | * [ ] in url expansions, the likes string is shown in same line which is wrong 13 | * [ ] fix: users who don't have kind 0 or kind 3 are not searchable in menu 8 and 9 in Social network. 14 | * [ ] kind 7 tags are messed up. for example for reaction: 066cdb716e250069c4078565c9d9046af483c43bbd8497aad9c60d41ec462034 and 137289198ff1c57a14711d87b059e5fc5f9b11b257672503595ac31bad450a22 15 | * [ ] fix count of events shown per relay in app stats 16 | * [-] read prikey from file; create it too using new feature --genkey 17 | * [x] allow special character input, and 256 limit [info](https://www.reddit.com/r/dartlang/comments/xcdsyx/i_am_seeing_that_stdinreadlinesync_returns_only/) 18 | * [x] fix --help that's dated 19 | * [x] support bech32 keys 20 | * [x] (showing tick for now) A F for friend or follow should be shown after each name that's a follow of the logged in user. F1 if the name is follow of a follow, and F2 if next level. 21 | * [x] due to extra color related bytes, reactions in highlighted threads are shifted a lot to left. fix that. 22 | * [x] increase author id to 5 and event id shown to 6 from 3 and 4 respectively 23 | * [x] add new relays ( zbd, coinos, radixrat) 24 | * [x] fix issue where need to go back into main menu to update the feed 25 | * [x] show lightning invoice as qr code 26 | * [x] in mention expansion, if p tag is not found in user store, then its left as #[n], whereas it should be replaced by the pubkey 27 | * [x] notifications should show mentions too ( it does not yet) 28 | * [x] notifications , option 3, is shown only for one entry in whole thread 29 | * [x] hashtag regexp should have underscore (seems to be working fine) 30 | * [x] add more default users. improve who is fetched. 31 | * [x] when seeing a profile, if they have liked something, then likes after their name are shown white 32 | 33 | 34 | # other longer term todo 35 | * [ ] parallel connections to relays in different isolate 36 | * [ ] build appimage for linux use 37 | * [ ] have spam rules file, which user can add and block spam 38 | 39 | 40 | # Running Nostr Console using Docker 41 | 42 | First check out or unzip the code to a directory, `cd` to that directory, and from there type the following commands: 43 | (make sure Docker desktop is running in the background) 44 | 45 | ``` 46 | docker build -t nostr_console . 47 | ``` 48 | 49 | Then run using 50 | ``` 51 | docker run -it nostr_console start 52 | ``` 53 | 54 | ## Prebuilt Docker Images 55 | 56 | Prebuilt docker image from the main branch of this repository can be found [here](https://github.com/vishalxl/nostr_console/pkgs/container/nostr_console). 57 | 58 | `docker pull ghcr.io/vishalxl/nostr_console:main` 59 | 60 | and then 61 | 62 | `docker run -it ghcr.io/vishalxl/nostr_console:main` 63 | 64 | 65 | 66 | # Use 67 | 68 | Easiest way to run nostr_console: Go to releases and get an executable for your platform. 69 | 70 | Otherwise do following: 71 | 1. Install [Flutter](https://docs.flutter.dev/get-started/install) SDK, or [Dart](https://dart.dev/get-dart) SDK 72 | 2. git clone this repository 73 | 3. From the project folder, run command ```dart pub get``` which gets all the dependencies 74 | 4. Run command ```dart run bin/nostr_console.dart```, which will run it with default settings. 75 | 5. Further you can create an executable for your platform by ```dart compile exe bin/nostr_console.dart``` which will create an executable for your platform. You can invoke that exe with required parameters. On Windows, you can create a shortcut to it with your desired command line arguments mentioned in it. 76 | 77 | 78 | Usage: 79 | 80 | ``` 81 | usage: dart run bin/nostr_console.dart [OPTIONS] 82 | 83 | OPTIONS 84 | 85 | -k, --prikey The nsec or hex private key of user you want to 'log in' as. 86 | -p, --pubkey The npub or hex public key of user whose events and feed are shown. When given, 87 | posts/replies can't be sent because for that a private key is needed. 88 | -r, --relay The comma separated relay urls that are used as relays. If given, these are used 89 | rather than the default relays. 90 | -f, --file Read from given file, if it is present, and at the end of the program execution, write 91 | to it all the events (including the ones read, and any new received). Even if not given, 92 | the default is to read from and write to all_nostr_events.txt . Can be turned off by 93 | the --disable-file flag 94 | -d, --days The latest number of days for which events are shown. Default is 1. 95 | --request This request is sent verbatim to the default relay. It can be used to recieve all events 96 | from a relay. If not provided, then events for default or given user are shown. 97 | -s, --disable-file When turned on, even the default filename is not read from. 98 | -t, --translate Translate some of the recent posts using Google translate site ( and not api). Google 99 | is accessed for any translation request only if this flag is present, and not otherwise. 100 | -l, --lnqr Flag, if set any LN invoices starting with LNBC will be printed as a QR code. Will set 101 | width to 140, which can be reset if needed with the --width argument. Wider 102 | space is needed for some qr codes. 103 | -g, --location The given value is added as a 'location' tag with every kind 1 post made. g in shortcut 104 | standing for geographic location. 105 | -h, --help Print help/usage message and exit. 106 | -v, --version Print version and exit. 107 | 108 | UI Options 109 | -a, --align When "left" is given as option to this argument, then the text is aligned to left. By 110 | default the posts or text is aligned to the center of the terminal. 111 | -w, --width This specifies how wide you want the text to be, in number of columns. Default is 96. 112 | Cant be less than 60. 113 | -m, --maxdepth The maximum depth to which the threads can be displayed. Minimum is 2 and 114 | maximum allowed is 12. 115 | -c, --color Color option can be green, cyan, white, black, red and blue. 116 | 117 | Advanced 118 | -y, --difficulty The difficulty number in bits, only for kind 1 messages. Tne next larger number divisible 119 | by 4 is taken as difficulty. Can't be more than 32 bits, because otherwise it typically 120 | takes too much time. Minimum and default is 0, which means no difficulty. 121 | -e, --overwrite Will over write the file with all the events that were read from file, and all newly 122 | received. Is useful when the file has to be cleared of old unused events. A backup should 123 | be made just in case of original file before invoking. 124 | 125 | ``` 126 | 127 | # Command line examples 128 | 129 | To 'login' as a user with private key K: 130 | 131 | ``` 132 | nostr_console.exe --prikey=K 133 | ``` 134 | 135 | To get ALL the latest messages on relays for last 3 days (on bash shell which allows backtick execution), for user with private key K: 136 | 137 | ``` 138 | nostr_console.exe --prikey=K --request=`echo "[\"REQ\",\"l\",{\"since\":$(date -d '-3 day' +%s)}]"` 139 | ``` 140 | 141 | To get all encrypted messages: 142 | ``` 143 | ./nostr_console_elf64 --prikey=K --request='["REQ","cn",{"limit":20000,"kinds":[104,140,141,142],"since":1663417739}]' # run on linux/bash 144 | ``` 145 | 146 | To run unit tests using Dart, in main/top level directory, run: 147 | 148 | ``` 149 | dart run test -r expanded 150 | ``` 151 | 152 | # Troubleshooting 153 | 154 | In case program is not sending events: 155 | 156 | 1. Make sure you are running the latest version. ( versions from 0.2.6 to 0.2.9 were very unstable) 157 | 2. Delete or backup the events file. Specially if its is more than 50 MB or has more than 50k events. 158 | 3. Right after starting, go to social network menu, and press 1 or such menu a couple of times (to print events) to allow some background processing, so that events can be processed. Once all "notifications" or new events have come in, then try sending your event(s) 159 | 160 | In case program is not fetching events: 161 | 1. Give it other or more relays' using --relay argument. 162 | 2. If event file is more than 50 MB, delete/backup it and start again. 163 | 164 | # Configuring Proxy 165 | When you are in an network which blocks outgoing HTTPS (e.g. company firewall), but there is a proxy you can set environment variable before running nostr_console. 166 | Examples below use authentication. Drop username:password if not required. 167 | 168 | ## Linux 169 | ``` 170 | $ export HTTP_PROXY=http://username:password@proxy.example.com:1234 171 | $ export HTTPS_PROXY=http://username:password@proxy.example.com:5678 172 | ``` 173 | To make permanent add to your shell profile, e.g. ~/.bashrc or to /etc/profile.d/ 174 | 175 | ## Windows 176 | ``` 177 | C:\setx HTTP_PROXY=http://username:password@proxy.example.com:1234 178 | C:\setx HTTPS_PROXY=http://username:password@proxy.example.com:5678 179 | ``` 180 | Using [setx](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/setx) to set an environment variable changes the value used in both the current command prompt session and all command prompt sessions that you create after running the command. It does not affect other command shells that are already running at the time you run the command. 181 | 182 | Use [set](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1) to set an environment variable changes the value used until the end of the current command prompt session, or until you set the variable to a different value. 183 | 184 | ## Tor proxy 185 | 186 | TOR can be used as a HTTP proxy with HTTPTunnelPort instead of just SOCKS5. 187 | 188 | # Screenshots 189 | 190 | 2022-12-02 (5) 191 | Showing Social network thread with re-shifting to left where threads are re-alignment to left for easier reading. 192 | 193 | 2022-12-02 (6) 194 | 195 | Public channels overview with menu 196 | 197 | 2022-12-02 (7) 198 | 199 | How public channels look like as of mid late 2022, with --translate flag automatically translating into English. 200 | 201 | 202 | # Contact 203 | 204 | [Nostr Telegram Channel](https://t.me/nostr_protocol) 205 | 206 | [Nostr Console Telegram channel](https://t.me/+YswV5fvfvPwyNmI1) 207 | 208 | Nostr Pulic Channel 52cab2e3e504ad6447d284b85b5cc601ca0613b151641e77facfec851c2ca816 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # reference : https://dart.dev/tools/analysis 2 | 3 | 4 | include: package:lints/recommended.yaml 5 | 6 | analyzer: 7 | exclude: [build/**] 8 | language: 9 | strict-raw-types: true 10 | strict-inference: true 11 | 12 | # not including 13 | # strict-casts: true 14 | 15 | 16 | linter: 17 | rules: 18 | - use_super_parameters 19 | - cancel_subscriptions 20 | - close_sinks 21 | - combinators_ordering 22 | - comment_references 23 | - invalid_case_patterns 24 | - library_annotations 25 | - one_member_abstracts 26 | - only_throw_errors -------------------------------------------------------------------------------- /bin/helloworld.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | // file to show docker issue 4 | 5 | void main(List arguments) async { 6 | stdout.write("Enter your name, anon: "); 7 | 8 | String? name = stdin.readLineSync(); 9 | if( name != null) { 10 | print("Hello $name"); 11 | } else { 12 | print("\nShould not print this if readlineSync works."); 13 | } 14 | } -------------------------------------------------------------------------------- /bin/nostr_console.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:translator/translator.dart'; 3 | import 'package:nostr_console/event_ds.dart'; 4 | import 'package:nostr_console/tree_ds.dart'; 5 | import 'package:nostr_console/relays.dart'; 6 | import 'package:nostr_console/console_ui.dart'; 7 | import 'package:nostr_console/settings.dart'; 8 | import 'package:nostr_console/utils.dart'; 9 | import 'package:nostr_console/user.dart'; 10 | import 'package:nostr_console/nip_019.dart'; 11 | 12 | import 'package:args/args.dart'; 13 | import 'package:logging/logging.dart'; 14 | 15 | // program arguments 16 | const String pubkeyArg = "pubkey"; 17 | const String prikeyArg = "prikey"; 18 | const String lastdaysArg = "days"; 19 | const String relayArg = "relay"; 20 | const String requestArg = "request"; // no abbreviation 21 | const String helpArg = "help"; 22 | const String versionArg = "version"; 23 | const String alignArg = "align"; // can only be "left" 24 | const String widthArg = "width"; 25 | const String maxDepthArg = "maxdepth"; 26 | const String eventFileArg = "file"; 27 | const String disableFileArg = "disable-file"; 28 | const String difficultyArg = "difficulty"; 29 | const String translateArg = "translate"; 30 | const String colorArg = "color"; 31 | const String overWriteFlag = "overwrite"; 32 | const String locationArg = "location"; 33 | const String lnQrFlag = "lnqr"; 34 | 35 | Future main(List arguments) async { 36 | 37 | 38 | final parser = ArgParser()..addOption(requestArg) ..addOption(pubkeyArg, abbr:"p")..addOption(prikeyArg, abbr:"k") 39 | ..addOption(lastdaysArg, abbr:"d") ..addOption(relayArg, abbr:"r") 40 | ..addFlag(helpArg, abbr:"h", defaultsTo: false) 41 | ..addFlag(versionArg, abbr:"v", defaultsTo: false) 42 | ..addOption(alignArg, abbr:"a") 43 | ..addOption(widthArg, abbr:"w")..addOption(maxDepthArg, abbr:"m") 44 | ..addOption(eventFileArg, abbr:"f", defaultsTo: gDefaultEventsFilename)..addFlag(disableFileArg, abbr:"s", defaultsTo: false) 45 | ..addFlag(translateArg, abbr: "t", defaultsTo: false) 46 | ..addOption(colorArg, abbr:"c") 47 | ..addOption(difficultyArg, abbr:"y") 48 | ..addFlag(overWriteFlag, abbr:"e", defaultsTo: false) 49 | ..addOption(locationArg, abbr:"g") 50 | ..addFlag("debug") 51 | ..addFlag(lnQrFlag, abbr:"l", defaultsTo: false); 52 | try { 53 | ArgResults argResults = parser.parse(arguments); 54 | if( argResults[helpArg]) { 55 | printUsage(); 56 | return; 57 | } 58 | 59 | if( argResults[versionArg]) { 60 | printVersion(); 61 | return; 62 | } 63 | 64 | Logger.root.level = Level.ALL; // defaults to Level.INFO 65 | DateTime appStartTime = DateTime.now(); 66 | print("app start time: $appStartTime"); 67 | Logger.root.onRecord.listen((record) { 68 | print('${record.level.name}: ${record.time.difference(appStartTime)}: ${record.message}'); 69 | }); 70 | 71 | // start application 72 | printIntro("Nostr"); 73 | 74 | if( argResults["debug"]) { 75 | gDebug = 1; 76 | } 77 | 78 | if( argResults[overWriteFlag]) { 79 | print("Going to overwrite file at the end of program execution."); 80 | gOverWriteFile = true; 81 | } 82 | 83 | 84 | if( argResults[translateArg]) { 85 | gTranslate = true; 86 | print("Going to translate comments in last $gNumTranslateDays days using Google translate service"); 87 | translator = GoogleTranslator(); 88 | } 89 | 90 | // get location of user if given 91 | if( argResults[locationArg] != null) { 92 | gUserLocation = argResults[locationArg]; 93 | userPrivateKey = ""; 94 | } 95 | 96 | if( gUserLocation.isNotEmpty){ 97 | print("Going to add $gUserLocation as the location tag with each post."); 98 | } 99 | 100 | if( argResults[pubkeyArg] != null) { 101 | userPublicKey = argResults[pubkeyArg]; 102 | if( userPublicKey.length != 64){ 103 | if( !userPublicKey.startsWith("npub")) { 104 | print("A public key should either start with npub ( bech32 format), or it should have a length of 64 bytes( hex format). Exiting."); 105 | return; 106 | } else { 107 | Map npubMap = bech32Decode(userPublicKey); 108 | String? npubPubkey = npubMap["data"]; 109 | if( npubPubkey != null) { 110 | userPublicKey = npubPubkey; 111 | } else { 112 | print("Could not parse the given npub/public key. Exiting."); 113 | return; 114 | } 115 | } 116 | } 117 | userPrivateKey = ""; 118 | } 119 | 120 | // process private key argument, and it overrides what's given in pub key argument, if any pubkey is given 121 | if( argResults[prikeyArg] != null) { 122 | userPrivateKey = argResults[prikeyArg]; 123 | if( userPrivateKey.length != 64){ 124 | if( !userPrivateKey.startsWith("nsec")) { 125 | print("A private key should either start with nsec ( bech32 format), or it should have a length of 64 bytes( hex format). Exiting."); 126 | return; 127 | } else { 128 | Map nsec = bech32Decode(userPrivateKey); 129 | String? nsecKey = nsec["data"]; 130 | if( nsecKey != null) { 131 | userPrivateKey = nsecKey; 132 | } else { 133 | print("Could not parse the given nsec/private key. Exiting."); 134 | return; 135 | } 136 | } 137 | } 138 | 139 | userPublicKey = myGetPublicKey(userPrivateKey); 140 | print("Going to use the provided private key"); 141 | } 142 | 143 | // write informative message in case user is not using proper keys 144 | if( userPublicKey == gDefaultPublicKey) { 145 | print("You should ideally create your own private key and use it with $gWarningColor--prikey$gColorEndMarker program argument. "); 146 | print("Create a private key from ${gWarningColor}astral.ninja, @damusapp, or even from command line using `openssl rand -hex 32`.$gColorEndMarker.\n"); 147 | print("npub/nsec keys can be converted to hex key format using https://damus.io/key"); 148 | } 149 | 150 | // handle relay related argument 151 | if( argResults[relayArg] != null) { 152 | Set userRelayList = Set.from(argResults[relayArg].split(",")); 153 | Set parsedRelays = {}; 154 | for (var relay in userRelayList) { 155 | if(relay.startsWith(RegExp(r'^ws[s]?:\/\/'))) { 156 | parsedRelays.add(relay); 157 | } else { 158 | printWarning("The provided relay entry: \"$relay\" does not start with ws:// or wss://, omitting"); 159 | } 160 | } 161 | 162 | // verify that there is at least one valid relay they provided, otherwise keep defaults 163 | if (parsedRelays.isNotEmpty) { 164 | gListRelayUrls = parsedRelays; 165 | defaultServerUrl = gListRelayUrls.first; 166 | } else { 167 | print("No valid relays were provided, using the default relay list"); 168 | } 169 | } 170 | printSet( gListRelayUrls, "Primary relays that will be used: ", ","); 171 | //print("From among them, default relay: $defaultServerUrl"); 172 | 173 | if( argResults[lastdaysArg] != null) { 174 | gNumLastDays = int.parse(argResults[lastdaysArg]); 175 | print("Going to show posts for last $gNumLastDays days"); 176 | } 177 | 178 | // lnqr will adjust width if needed; but it can be reset with --width because latter is processed afterwards 179 | if( argResults[lnQrFlag]) { 180 | gShowLnInvoicesAsQr = true; 181 | if( gTextWidth < gMinWidthForLnQr) { 182 | gTextWidth = gMinWidthForLnQr; 183 | } 184 | print("Going to show LN invoices as QR code"); 185 | } 186 | 187 | // process --width argument 188 | if( argResults[widthArg] != null) { 189 | int tempTextWidth = int.parse(argResults[widthArg]); 190 | if( tempTextWidth < gMinValidTextWidth ) { 191 | print("Text-width cannot be less than $gMinValidTextWidth. Going to use the defalt value of $gTextWidth"); 192 | } else { 193 | gTextWidth = tempTextWidth; 194 | print("Going to use $gTextWidth columns for text on screen."); 195 | } 196 | } 197 | 198 | 199 | try { 200 | var terminalColumns = gDefaultTextWidth; 201 | if( stdout.hasTerminal ) { 202 | terminalColumns = stdout.terminalColumns; 203 | } 204 | 205 | // can be computed only after textWidth has been found 206 | if( gTextWidth > terminalColumns) { 207 | gTextWidth = terminalColumns - 5; 208 | } 209 | gNumLeftMarginSpaces = (terminalColumns - gTextWidth )~/2; 210 | } on StdoutException catch (e) { 211 | print("Cannot find terminal size. Left aligning by default."); 212 | if( gDebug > 0) log.info(e.message); 213 | gNumLeftMarginSpaces = 0; 214 | } 215 | // undo above if left option is given 216 | if( argResults[alignArg] != null ) { 217 | if( argResults[alignArg] == "left" ) { 218 | print("Going to align to left."); 219 | gAlignment = "left"; 220 | gNumLeftMarginSpaces = 0; 221 | } 222 | } 223 | if( argResults[maxDepthArg] != null) { 224 | int tempMaxDepth = int.parse(argResults[maxDepthArg]); 225 | if( tempMaxDepth < gMinimumDepthAllowed || tempMaxDepth > gMaximumDepthAllowed) { 226 | print("Maximum depth cannot be less than $gMinimumDepthAllowed and cannot be more than $gMaximumDepthAllowed. Going to use the default maximum depth, which is $gDefaultMaxDepth."); 227 | } else { 228 | maxDepthAllowed = tempMaxDepth; 229 | print("Going to take threads to maximum depth of $gNumLastDays days"); 230 | } 231 | } 232 | 233 | if( argResults[colorArg] != null) { 234 | String colorGiven = argResults[colorArg].toString().toLowerCase(); 235 | if( gColorMapForArguments.containsKey(colorGiven)) { 236 | String color = gColorMapForArguments[colorGiven]??""; 237 | if( color == "") { 238 | print("Invalid color."); 239 | } else 240 | { 241 | gCommentColor = color; 242 | stdout.write("Going to use color $colorGiven for text"); 243 | if( colorGiven == "cyan") { 244 | gNotificationColor = greenColor; 245 | stdout.write(". Green as notification color"); 246 | } 247 | stdout.write(".\n"); 248 | } 249 | } else { 250 | print("Invalid color."); 251 | } 252 | } 253 | 254 | if( argResults[difficultyArg] != null) { 255 | gDifficulty = int.parse(argResults[difficultyArg]); 256 | 257 | if( gDifficulty > gMaxDifficultyAllowed) { 258 | print("Difficulty cannot be larger than $gMaxDifficultyAllowed. Going to use difficulty of $gMaxDifficultyAllowed"); 259 | gDifficulty = gMaxDifficultyAllowed; 260 | } 261 | else { 262 | if( gDifficulty < 0) { 263 | print("Difficulty cannot be less than 0. Going to use difficulty of 0 bits."); 264 | } else { 265 | print("Going to use difficulty of value: $gDifficulty bits"); 266 | } 267 | } 268 | } 269 | 270 | if( argResults[disableFileArg]) { 271 | gEventsFilename = ""; 272 | print("Not going to use any file to read/write events."); 273 | } 274 | 275 | String whetherDefault = "the given "; 276 | if( argResults[eventFileArg] != null && !argResults[disableFileArg]) { 277 | if( gDefaultEventsFilename == argResults[eventFileArg]) { 278 | whetherDefault = " default "; 279 | } 280 | 281 | gEventsFilename = argResults[eventFileArg]; 282 | } 283 | 284 | Set initialEvents = {}; // collect all events here and then create tree out of them 285 | 286 | if( gEventsFilename != "") { 287 | stdout.write('Reading events from ${whetherDefault}file.......'); 288 | 289 | // read file events and give the events to relays from where they're picked up later 290 | initialEvents = readEventsFromFile(gEventsFilename); 291 | 292 | // count events 293 | numFileEvents += initialEvents.length; 294 | print("read $numFileEvents events from file $gEventsFilename"); 295 | } 296 | 297 | int limitSelfEvents = 200; 298 | int limitOthersEvents = 4; 299 | int limitPerSubscription = gLimitPerSubscription; 300 | 301 | // if more than 1000 posts have already been read from the file, then don't get too many day's events. Only for last 3 days. 302 | if(numFileEvents > 1000) { 303 | limitSelfEvents = 4; 304 | limitOthersEvents = 3; 305 | gDefaultNumWaitSeconds = gDefaultNumWaitSeconds ~/5; 306 | } else { 307 | printInfoForNewUser(); 308 | } 309 | 310 | // process request string. If this is blank then the application only reads from file and does not connect to internet. 311 | if( argResults[requestArg] != null) { 312 | int numWaitSeconds = gDefaultNumWaitSeconds; 313 | 314 | if( argResults[requestArg] != "") { 315 | stdout.write('Sending request ${argResults[requestArg]} and waiting for events...'); 316 | sendRequest(gListRelayUrls, argResults[requestArg]); 317 | } else { 318 | numWaitSeconds = 0; 319 | gEventsFilename = ""; // so it wont write it back to keep it faster ( and since without internet no new event is there to be written ) 320 | } 321 | 322 | if( userPublicKey!= "") { 323 | getIdAndMentionEvents(gListRelayUrls, {userPublicKey}, limitPerSubscription, getSecondsDaysAgo(limitSelfEvents), getSecondsDaysAgo(limitSelfEvents), "#p", "authors"); 324 | } 325 | 326 | Future.delayed(Duration(milliseconds: numWaitSeconds), () { 327 | Set receivedEvents = getRecievedEvents(); 328 | 329 | initialEvents.addAll(receivedEvents); 330 | 331 | // Create tree from all events read form file 332 | Store node = getTree(initialEvents); 333 | 334 | clearEvents(); 335 | if( gDebug > 0) stdout.write("Total events all kind in created tree: ${node.count()} events\n"); 336 | gStore = node; 337 | mainMenuUi(node); 338 | }); 339 | return; 340 | } 341 | 342 | // the default in case no arguments are given is: 343 | // get a user's events with all default users events 344 | // get mentions for user 345 | // get all kind 0, 3, 4x, 14x events 346 | 347 | // then get the events of user-id's mentioned in p-tags of received events and the contact list 348 | // then display them all 349 | 350 | // get event for user 351 | if( userPublicKey!= "") { 352 | //getIdAndMentionEvents(gListRelayUrls2, {userPublicKey}, limitPerSubscription, getSecondsDaysAgo(limitSelfEvents), getSecondsDaysAgo(limitSelfEvents), "#p", "authors"); 353 | getUserEvents(gListRelayUrls, userPublicKey, limitPerSubscription, getSecondsDaysAgo(limitSelfEvents)); 354 | getMentionEvents(gListRelayUrls, {userPublicKey}, limitPerSubscription, getSecondsDaysAgo(limitSelfEvents), "#p"); 355 | 356 | } 357 | 358 | Set usersFetched = {userPublicKey}; 359 | 360 | stdout.write('Waiting for user posts to come in.....'); 361 | Future.delayed( Duration(milliseconds: gDefaultNumWaitSeconds), () { 362 | initialEvents.addAll(getRecievedEvents()); 363 | clearEvents(); 364 | 365 | 366 | for (var element in initialEvents) { element.eventData.kind == 1? numUserPosts++: numUserPosts;} 367 | numUserPosts -= numFilePosts; 368 | stdout.write("...done\n");//received $numUserPosts new posts made by the user\n"); 369 | 370 | Set userEvents = getOnlyUserEvents(initialEvents, userPublicKey); 371 | //print('Total events fetched till now: ${initialEvents.length}. Total user events fetched: ${userEvents.length}'); 372 | 373 | // get events from channels of user; gets public as well as encrypted channels 374 | Set userChannels = getUserChannels(initialEvents, userPublicKey); 375 | //printSet(userChannels, "user channels: \n", "\n"); 376 | //getIdAndMentionEvents(gListRelayUrls1, userChannels, limitPerSubscription, 0, getSecondsDaysAgo(limitOthersEvents), "#e", "ids"); 377 | 378 | getKindEvents([40, 41], gListRelayUrls, limitPerSubscription, getSecondsDaysAgo(limitSelfEvents)); 379 | getKindEvents([42], gListRelayUrls, 3 * limitPerSubscription, getSecondsDaysAgo(limitOthersEvents)); 380 | 381 | for (var e in initialEvents) { 382 | processKind3Event(e); 383 | } // first process the kind 3 event ; basically populate the global structure that holds this info 384 | 385 | Set contacts = {}; 386 | Set pTags = {}; 387 | 388 | if( userPublicKey != "") { 389 | // get the latest kind 3 event for the user, which has the 'follows' list 390 | Event? contactEvent = getContactEvent(userPublicKey); 391 | 392 | // if contact list was found, get user's feed; also get some default contacts 393 | if (contactEvent != null ) { 394 | if(gDebug > 0) print("In main: found contact list: \n ${contactEvent.originalJson}"); 395 | for (var contact in contactEvent.eventData.contactList) { 396 | contacts.add(contact.contactPubkey); 397 | } 398 | } else { 399 | print("Could not find your contact list."); 400 | } 401 | } 402 | 403 | // fetch extra events for people who don't have too large a follow list 404 | if( contacts.union(gDefaultFollows).length < gMaxPtagsToGet ) { 405 | // calculate top mentioned ptags, and then get the events for those users 406 | pTags = getpTags(initialEvents, gMaxPtagsToGet); 407 | } 408 | 409 | // get only limited number of contacts otherwise relays get less responsive 410 | int maxContactsFetched = 700; 411 | if( contacts.length > maxContactsFetched) { 412 | int i = 0; 413 | contacts.retainWhere((element) => i++ < maxContactsFetched); // retain only first 200, whichever they may be 414 | } 415 | 416 | getMultiUserEvents(gListRelayUrls, contacts.union(gDefaultFollows).union(pTags).difference(usersFetched), 4 * limitPerSubscription, getSecondsDaysAgo(limitOthersEvents)); 417 | usersFetched = usersFetched.union(gDefaultFollows).union(contacts).union(pTags); 418 | 419 | // get meta events of all users fetched 420 | getMultiUserEvents(gListRelayUrls, usersFetched, 10 * limitPerSubscription, getSecondsDaysAgo(limitSelfEvents*100), {0,3}); 421 | //print("fetched meta of ${usersFetched.length}"); 422 | 423 | 424 | void resetRelays() { 425 | relays.closeAll(); 426 | 427 | /*getMultiUserEvents(gListRelayUrls1, usersFetched, 4 * limitPerSubscription, getTimeSecondsAgo(1), {0,3}); 428 | getMultiUserEvents(gListRelayUrls1, contacts.union(gDefaultFollows).union(pTags).difference(usersFetched), 4 * limitPerSubscription, getTimeSecondsAgo(1)); 429 | getKindEvents([40, 41], gListRelayUrls1, limitPerSubscription, getTimeSecondsAgo(1)); 430 | getKindEvents([42], gListRelayUrls1, 3 * limitPerSubscription, getTimeSecondsAgo(1)); 431 | getUserEvents(gListRelayUrls1, userPublicKey, limitPerSubscription, getTimeSecondsAgo(1)); 432 | getMentionEvents(gListRelayUrls1, {userPublicKey}, limitPerSubscription, getTimeSecondsAgo(1), "#p"); 433 | */ 434 | } 435 | 436 | stdout.write('Waiting for feed to come in..............'); 437 | Future.delayed(Duration(milliseconds: gDefaultNumWaitSeconds * 1), () { 438 | 439 | initialEvents.addAll(getRecievedEvents()); 440 | clearEvents(); 441 | 442 | stdout.write("done\n"); 443 | if( gDebug > 0) log.info("Received ptag events events."); 444 | 445 | //resetRelays(); 446 | relays = Relays({}, {}, {}); // reset relay value 447 | 448 | String req = '["REQ","latest_live_all",{"limit":40000,"kinds":[0,1,3,4,5,6,7,40,41,42,104,140,141,142],"since":${getTimeSecondsAgo(gSecsLatestLive).toString()}}]'; 449 | sendRequest(gListRelayUrls, req); 450 | //getMultiUserEvents(gListRelayUrls1, usersFetched, 10 * limitPerSubscription, getSecondsDaysAgo(limitSelfEvents*100), {0,3}); 451 | 452 | // Create tree from all events that's have yet been received/accumulated 453 | Store node = getTree(initialEvents); 454 | gStore = node; 455 | 456 | mainMenuUi(node); 457 | }); 458 | }); 459 | } on FormatException catch (e) { 460 | print(e.message); 461 | return; 462 | } on Exception catch (e) { 463 | print(e); 464 | return; 465 | } 466 | } 467 | 468 | -------------------------------------------------------------------------------- /lib/nip_019.dart: -------------------------------------------------------------------------------- 1 | import 'package:bech32/bech32.dart'; 2 | import 'package:convert/convert.dart'; 3 | 4 | /// bech32-encoded entities 5 | class Nip19 { 6 | static encodePubkey(String pubkey) { 7 | return bech32Encode("npub", pubkey); 8 | } 9 | 10 | static encodePrivkey(String privkey) { 11 | return bech32Encode("nsec", privkey); 12 | } 13 | 14 | static encodeNote(String noteid) { 15 | return bech32Encode("note", noteid); 16 | } 17 | 18 | static String decodePubkey(String data) { 19 | Map map = bech32Decode(data); 20 | if (map["prefix"] == "npub") { 21 | return map["data"]; 22 | } else { 23 | return ""; 24 | } 25 | } 26 | 27 | static String decodePrivkey(String data) { 28 | Map map = bech32Decode(data); 29 | if (map["prefix"] == "nsec") { 30 | return map["data"]; 31 | } else { 32 | return ""; 33 | } 34 | } 35 | 36 | static String decodeNote(String data) { 37 | Map map = bech32Decode(data); 38 | if (map["prefix"] == "note") { 39 | return map["data"]; 40 | } else { 41 | return ""; 42 | } 43 | } 44 | } 45 | 46 | /// help functions 47 | 48 | String bech32Encode(String prefix, String hexData) { 49 | final data = hex.decode(hexData); 50 | final convertedData = convertBits(data, 8, 5, true); 51 | final bech32Data = Bech32(prefix, convertedData); 52 | return bech32.encode(bech32Data); 53 | } 54 | 55 | Map bech32Decode(String bech32Data) { 56 | final decodedData = bech32.decode(bech32Data); 57 | final convertedData = convertBits(decodedData.data, 5, 8, false); 58 | final hexData = hex.encode(convertedData); 59 | 60 | return {'prefix': decodedData.hrp, 'data': hexData}; 61 | } 62 | 63 | List convertBits(List data, int fromBits, int toBits, bool pad) { 64 | var acc = 0; 65 | var bits = 0; 66 | final maxv = (1 << toBits) - 1; 67 | final result = []; 68 | 69 | for (final value in data) { 70 | if (value < 0 || value >> fromBits != 0) { 71 | throw Exception('Invalid value: $value'); 72 | } 73 | acc = (acc << fromBits) | value; 74 | bits += fromBits; 75 | 76 | while (bits >= toBits) { 77 | bits -= toBits; 78 | result.add((acc >> bits) & maxv); 79 | } 80 | } 81 | 82 | if (pad) { 83 | if (bits > 0) { 84 | result.add((acc << (toBits - bits)) & maxv); 85 | } 86 | } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) { 87 | throw Exception('Invalid data'); 88 | } 89 | 90 | return result; 91 | } 92 | -------------------------------------------------------------------------------- /lib/relays.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'dart:convert'; 4 | import 'package:nostr_console/event_ds.dart'; 5 | import 'package:nostr_console/settings.dart'; 6 | import 'package:nostr_console/utils.dart'; 7 | import 'package:web_socket_channel/io.dart'; 8 | import 'package:web_socket_channel/src/exception.dart'; 9 | 10 | class Relay { 11 | String url; 12 | IOWebSocketChannel socket; 13 | Set users; // is used so that duplicate requests aren't sent for same user for this same relay; unused for now 14 | int numReceived; 15 | int numRequestsSent; 16 | Relay(this.url, this.socket, this.users, this.numReceived, this.numRequestsSent); 17 | 18 | void close() { 19 | socket.sink.close().onError((error, stackTrace) => null); 20 | } 21 | 22 | void printInfo() { 23 | print("$url ${getNumSpaces(45 - url.length)} $numReceived ${users.length}"); 24 | } 25 | } 26 | 27 | /* 28 | * @class Relays Contains connections to all relays. 29 | */ 30 | class Relays { 31 | Map relays; 32 | Set rEvents = {}; // current events received. can be used by others. Is cleared after consumption 33 | Set uniqueIdsRecieved = {} ; // id of events received. only for internal usage, so that duplicate events are rejected 34 | Relays(this.relays, this.rEvents, this.uniqueIdsRecieved); 35 | 36 | void closeAll() { 37 | relays.forEach((url, relay) { 38 | relay.close(); 39 | }); 40 | 41 | //relays.clear(); 42 | } 43 | 44 | void printInfo() { 45 | printUnderlined("Server connection info"); 46 | print(" Server Url Num events received: Num users requested"); 47 | for( var key in relays.keys) { 48 | 49 | relays[key]?.printInfo(); 50 | } 51 | } 52 | 53 | factory Relays.relay(String relayUrl) { 54 | IOWebSocketChannel fws = IOWebSocketChannel.connect(relayUrl); 55 | print('In Relay.relay: connecting to relay $relayUrl'); 56 | Map mapRelay = {}; 57 | Relay relayObject = Relay( relayUrl, fws, {}, 0, 0); 58 | mapRelay[relayUrl] = relayObject; 59 | 60 | return Relays(mapRelay, {}, {}); 61 | } 62 | 63 | 64 | void getKindEvents(List kind, String relayUrl, int limit, int sinceWhen) { 65 | kind.toString(); 66 | String subscriptionId = "kind_${kind}_${relayUrl.substring(6)}"; 67 | String request = getKindRequest(subscriptionId, kind, limit, sinceWhen); 68 | 69 | sendRequest(relayUrl, request); 70 | } 71 | /* 72 | * @connect Connect to given relay and get all events for the given publicKey and insert the 73 | * received events in the given List 74 | */ 75 | void getUserEvents(String relayUrl, String publicKey, int limit, int sinceWhen) { 76 | for(int i = 0; i < gBots.length; i++) { // ignore bots 77 | if( publicKey == gBots[i]) { 78 | return; 79 | } 80 | } 81 | 82 | String subscriptionId = "single_user${relays[relayUrl]?.numRequestsSent??""}_${relayUrl.substring(6)}"; 83 | if( relays.containsKey(relayUrl)) { 84 | Set? users = relays[relayUrl]?.users; 85 | if( users != null) { // get a user only if it has not already been requested 86 | // following is too restrictive casuse changed sinceWhen is not considered. TODO improve it 87 | bool alreadyRecevied = false; 88 | for (var user in users) { 89 | if( user == publicKey) { 90 | alreadyRecevied = true; 91 | } 92 | } 93 | 94 | if( alreadyRecevied) { 95 | return; 96 | } 97 | 98 | users.add(publicKey); 99 | } 100 | } 101 | 102 | String request = getUserRequest(subscriptionId, publicKey, limit, sinceWhen); 103 | //print("In relay: getKind events: request = $request"); 104 | sendRequest(relayUrl, request); 105 | } 106 | 107 | void getMentionEvents(String relayUrl, Set ids, int limit, int sinceWhen, String tagToGet) { 108 | for(int i = 0; i < gBots.length; i++) { // ignore bots 109 | if( ids == gBots[i]) { 110 | return; 111 | } 112 | } 113 | 114 | String subscriptionId = "mention${relays[relayUrl]?.numRequestsSent??""}_${relayUrl.substring(6)}"; 115 | 116 | String request = getMentionRequest(subscriptionId, ids, limit, sinceWhen, tagToGet); 117 | sendRequest(relayUrl, request); 118 | } 119 | 120 | void getIdAndMentionEvents(String relayUrl, Set ids, int limit, int idSinceWhen, int mentionSinceWhen, String tagToGet, String idType) { 121 | 122 | String subscriptionId = "id_mention_tag${relays[relayUrl]?.numRequestsSent??""}_${relayUrl.substring(6)}"; 123 | String request = getIdAndMentionRequest(subscriptionId, ids, limit, idSinceWhen, mentionSinceWhen, tagToGet, idType); 124 | sendRequest(relayUrl, request); 125 | } 126 | 127 | 128 | /* 129 | * @connect Connect to given relay and get all events for multiple users/publicKey and insert the 130 | * received events in the given List 131 | */ 132 | void getMultiUserEvents(String relayUrl, List publicKeys, int limit, int sinceWhen, [Set? kind]) { 133 | Set setPublicKeys = publicKeys.toSet(); 134 | 135 | if( relays.containsKey(relayUrl)) { 136 | Set? users = relays[relayUrl]?.users; 137 | if( users != null) { 138 | relays[relayUrl]?.users = users.union(setPublicKeys); 139 | 140 | } 141 | } 142 | 143 | String subscriptionId = "multiple_user${relays[relayUrl]?.numRequestsSent??""}_${relayUrl.substring(6)}"; 144 | String request = getMultiUserRequest( subscriptionId, setPublicKeys, limit, sinceWhen, kind); 145 | sendRequest(relayUrl, request); 146 | } 147 | 148 | /* 149 | * Send the given string to the given relay. Is used to send both requests, and to send evnets. 150 | */ 151 | void sendRequest(String relayUrl, String request) async { 152 | if(relayUrl == "" ) { 153 | if( gDebug != 0) print ("Invalid or empty relay given"); 154 | return; 155 | } 156 | 157 | if( gDebug > 0) print ("\nIn relay.sendRequest for relay $relayUrl"); 158 | 159 | IOWebSocketChannel? fws; 160 | if(relays.containsKey(relayUrl)) { 161 | 162 | fws = relays[relayUrl]?.socket; 163 | relays[relayUrl]?.numRequestsSent++; 164 | } 165 | else { 166 | if(gDebug !=0) print('connecting to $relayUrl'); 167 | 168 | try { 169 | IOWebSocketChannel fws2 = IOWebSocketChannel.connect(relayUrl); 170 | 171 | try { 172 | await fws2.ready; 173 | } catch (e) { 174 | // handle exception here 175 | //print("Error: Failed to connect to relay $relayUrl . Got exception = |${e.toString()}|"); 176 | return; 177 | } 178 | 179 | Relay newRelay = Relay(relayUrl, fws2, {}, 0, 1); 180 | relays[relayUrl] = newRelay; 181 | fws = fws2; 182 | fws2.stream.listen( 183 | (d) { 184 | Event e; 185 | try { 186 | dynamic json = jsonDecode(d); 187 | if( json.length < 3) { 188 | return; 189 | } 190 | newRelay.numReceived++; 191 | 192 | String id = json[2]['id'] as String; 193 | if( uniqueIdsRecieved.contains(id)) { // rEvents is often cleared, but uniqueIdsRecieved contains everything received til now 194 | return; 195 | } 196 | 197 | e = Event.fromJson(d, relayUrl); 198 | 199 | if( rEvents.add(e) ) { 200 | uniqueIdsRecieved.add(id); 201 | } else { 202 | } 203 | } on FormatException { 204 | return; 205 | } catch(err) { 206 | return; 207 | } 208 | }, 209 | onError: (err) { if(gDebug > 0) printWarning("Warning: Error in creating connection to $relayUrl. Kindly check your internet connection. Or maybe only this relay is down."); }, 210 | onDone: () { if( gDebug > 0) print('Info: In onDone'); } 211 | ); 212 | } on WebSocketException { 213 | print('WebSocketException exception for relay $relayUrl'); 214 | return; 215 | } on WebSocketChannelException { 216 | print('WebSocketChannelException exception for relay $relayUrl'); 217 | return; // is presently not used/called 218 | } 219 | on Exception { 220 | printWarning("Invalid event\n"); 221 | } 222 | 223 | catch(err) { 224 | if( gDebug >= 0) printWarning('exception generic $err for relay $relayUrl\n'); 225 | return; 226 | } 227 | } 228 | 229 | if(gDebug > 0) log.info('Sending request: \n$request\n to $relayUrl\n\n'); 230 | fws?.sink.add(request); 231 | } 232 | 233 | 234 | IOWebSocketChannel? getWS(String relay) { 235 | return relays[relay]?.socket; 236 | } 237 | 238 | void printStatus() { 239 | print("In Relays::printStatus. Number of relays = ${relays.length}"); 240 | relays.forEach((key, value) { 241 | print("for relay: $key"); 242 | print("$value\n"); 243 | String? reason = value.socket.closeReason; 244 | print( reason??"reason not found"); 245 | }); 246 | } 247 | } 248 | 249 | Relays relays = Relays({}, {}, {}); 250 | 251 | void getContactFeed(Set relayUrls, Set setContacts, int numEventsToGet, int sinceWhen) { 252 | 253 | List contacts = setContacts.toList(); 254 | for( int i = 0; i < contacts.length; i += gMaxAuthorsInOneRequest) { 255 | 256 | // for last iteration change upper limit 257 | int upperLimit = (i + gMaxAuthorsInOneRequest) > contacts.length? 258 | (contacts.length - i): gMaxAuthorsInOneRequest; 259 | 260 | List groupContacts = []; 261 | for( int j = 0; j < upperLimit; j++) { 262 | groupContacts.add(contacts[i + j]); 263 | } 264 | 265 | for (var relayUrl in relayUrls) { 266 | relays.getMultiUserEvents(relayUrl, groupContacts, numEventsToGet, sinceWhen); 267 | } 268 | 269 | } 270 | 271 | // return contact list for use by caller 272 | return; 273 | } 274 | 275 | void getUserEvents(Set serverUrls, String publicKey, int numUserEvents, int sinceWhen) { 276 | for (var serverUrl in serverUrls) { 277 | relays.getUserEvents(serverUrl, publicKey, numUserEvents, sinceWhen); 278 | } 279 | } 280 | 281 | void getMentionEvents(Set serverUrls, Set ids, int numUserEvents, int sinceWhen, String tagToGet) { 282 | for (var serverUrl in serverUrls) { 283 | relays.getMentionEvents(serverUrl, ids, numUserEvents, sinceWhen, tagToGet); 284 | } 285 | } 286 | 287 | void getIdAndMentionEvents(Set serverUrls, Set ids, int numUserEvents, int idSinceWhen, int mentionSinceWhen, String tagToGet, String idType) { 288 | for (var serverUrl in serverUrls) { 289 | relays.getIdAndMentionEvents(serverUrl, ids, numUserEvents, idSinceWhen, mentionSinceWhen, tagToGet, idType); 290 | } 291 | } 292 | 293 | 294 | getKindEvents(List kind, Set serverUrls, int limit, int sinceWhen) { 295 | for (var serverUrl in serverUrls) { 296 | relays.getKindEvents(kind, serverUrl, limit, sinceWhen); 297 | } 298 | } 299 | 300 | void getMultiUserEvents(Set serverUrls, Set setPublicKeys, int numUserEvents, int sinceWhen, [Set? kind]) { 301 | List publicKeys = setPublicKeys.toList(); 302 | if( gDebug > 0) print("Sending multi user request for ${publicKeys.length} users"); 303 | 304 | for(var serverUrl in serverUrls) { 305 | for( int i = 0; i < publicKeys.length; i+= gMaxAuthorsInOneRequest) { 306 | int getUserRequests = gMaxAuthorsInOneRequest; 307 | if( publicKeys.length - i <= gMaxAuthorsInOneRequest) { 308 | getUserRequests = publicKeys.length - i; 309 | } 310 | List partialList = publicKeys.sublist(i, i + getUserRequests); 311 | relays.getMultiUserEvents(serverUrl, partialList, numUserEvents, sinceWhen, kind); 312 | } 313 | } 314 | } 315 | 316 | // send request for specific events whose id's are passed as list eventIds 317 | void sendEventsRequest(Set serverUrls, Set eventIds) { 318 | if( eventIds.isEmpty) { 319 | return; 320 | } 321 | 322 | String eventIdsStr = getCommaSeparatedQuotedStrs(eventIds); 323 | 324 | String getEventRequest = '["REQ","event_${eventIds.length}",{"ids":[$eventIdsStr]}]'; 325 | if( gDebug > 0) log.info("sending $getEventRequest"); 326 | 327 | for (var url in serverUrls) { 328 | relays.sendRequest(url, getEventRequest); 329 | } 330 | } 331 | 332 | void sendRequest(Set serverUrls, request) { 333 | for (var url in serverUrls) { 334 | relays.sendRequest(url, request); 335 | } 336 | } 337 | 338 | Set getRecievedEvents() { 339 | return relays.rEvents; 340 | } 341 | 342 | void clearEvents() { 343 | relays.rEvents.clear(); 344 | if( gDebug > 0) print("clearEvents(): returning"); 345 | } 346 | 347 | void setRelaysIntialEvents(Set eventsFromFile) { 348 | for (var element in eventsFromFile) {relays.uniqueIdsRecieved.add(element.eventData.id);} 349 | relays.rEvents = eventsFromFile; 350 | } 351 | 352 | -------------------------------------------------------------------------------- /lib/settings.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:logging/logging.dart'; 3 | 4 | // name of executable 5 | const String exename = "nostr_console"; 6 | const String version = "0.3.7-beta"; 7 | 8 | int gDebug = 0; 9 | int gSpecificDebug = 0; 10 | 11 | final log = Logger('ExampleLogger'); 12 | 13 | // for debugging 14 | String gCheckEventId = "xb9e1824fe65b10f7d06bd5f6dfe1ab3eda876d7243df5878ca0b9686d80c0840f"; 15 | 16 | int gMaxEventLenthAccepted = 80000; // max event size. events larger than this are rejected. 17 | 18 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////// encrypted Group settings 19 | const int gSecretMessageKind = 104; 20 | 21 | const int gReplyLengthPrinted = 115; // how much of replied-to comment is printed at max 22 | 23 | const int gNumRoomsShownByDefault = 20; 24 | 25 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////// file related settings 26 | const String gDefaultEventsFilename = "all_nostr_events.txt"; 27 | String gEventsFilename = ""; // is set in arguments, and if set, then file is read from and written to 28 | bool gDontWriteOldEvents = true; 29 | const int gDontSaveBeforeDays = 20; // dont save events older than this many days if gDontWriteOldEvents flag is true 30 | const int gDeletePostsOlderThanDays = 20; 31 | bool gOverWriteFile = false; // overwrite the file, and don't just append. Will write all events in memory. 32 | 33 | const int gDontAddToStoreBeforeDays = 60; // events older than this are not added to the Store of all events 34 | 35 | const int gLimitFollowPosts = 20; // when getting events, this is the since field (unless a fully formed request is given in command line) 36 | const int gLimitPerSubscription = 20000; 37 | 38 | // don't show notifications for events that are older than 5 days and come when program is running 39 | // applicable only for notifications and not for search results. Search results set a flag in EventData and don't use this variable 40 | const int gDontHighlightEventsOlderThan = 4; 41 | 42 | int gDefaultNumWaitSeconds = 12000; // is used in main() 43 | const int gMaxAuthorsInOneRequest = 300; // number of author requests to send in one request 44 | const int gMaxPtagsToGet = 200; // maximum number of p tags that are taken from the comments of feed ( the top most, most frequent) 45 | 46 | const int gSecsLatestLive = 2 * 3600; // the lastst seconds for which to get the latest event in main 47 | int gHoursDefaultPrint = 6; // print latest given hours only 48 | 49 | // global counters of total events read or processed 50 | int numFileEvents = 0, numFilePosts = 0, numUserPosts = 0, numFeedPosts = 0, numOtherPosts = 0; 51 | 52 | 53 | // edited on 29 sept 2024 54 | String defaultServerUrl = "wss://relay.damus.io"; 55 | Set gListRelayUrls = { defaultServerUrl, 56 | "wss://nostr.wine", 57 | "wss://relay.nostr.info", 58 | "wss://nos.lol", 59 | "wss://relay.nostr.band" 60 | }; 61 | 62 | 63 | // well known disposable test private key 64 | const String gDefaultPublicKey = ""; 65 | String userPrivateKey = ""; 66 | String userPublicKey = gDefaultPublicKey; 67 | 68 | // default follows; taken from nostr.io/stats 69 | Set gDefaultFollows = { 70 | // 21 dec 2022 71 | "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", // Jack Dorsey 72 | "c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0", // Mallers 73 | "a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98", // Saylor 74 | "020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e", // Adam Back 75 | "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", // ODELL 76 | "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411", // NVK 77 | "85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204", // Preston 78 | "83e818dfbeccea56b0f551576b3fd39a7a50e1d8159343500368fa085ccd964b", // Jeff Booth 79 | "f728d9e6e7048358e70930f5ca64b097770d989ccd86854fe618eda9c8a38106", // Lopp 80 | "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce", // CARLA 81 | "e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42", // wiz 82 | "472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", // MartyBent 83 | "c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15", // hodlonaut 84 | "1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b", // yegorPetrov 85 | "be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479", // walletofsatoshi 86 | "edcd20558f17d99327d841e4582f9b006331ac4010806efa020ef0d40078e6da", // Natalie Brunell 87 | "eaf27aa104833bcd16f671488b01d65f6da30163b5848aea99677cc947dd00aa", // grubles 88 | "b9003833fabff271d0782e030be61b7ec38ce7d45a1b9a869fbdb34b9e2d2000", // brockm 89 | "51b826cccd92569a6582e20982fd883fccfa78ad03e0241f7abec1830d7a2565", // Jonas Schnelli 90 | "92de68b21302fa2137b1cbba7259b8ba967b535a05c6d2b0847d9f35ff3cf56a", // Susie bdds 91 | "c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11", // walker 92 | "b5db1aacc067a056350c4fcaaa0f445c8f2acbb3efc2079c51aaba1f35cd8465", // Nostrich 93 | 94 | "6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3", // Jesse Powell 95 | 96 | "24e37c1e5b0c8ba8dde2754bcffc63b5b299f8064f8fb928bcf315b9c4965f3b", // lunaticoin 97 | "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0", // martii malmi 98 | "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", // hodlbod 99 | 100 | // pre dec 2022 101 | "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", // damus 102 | "6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964", // dergigi 103 | "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", // jb55 104 | "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf 105 | "2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5", // unclebobmartin 106 | "ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69", // Melvincarvalho 107 | "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f", // scsibug 108 | "9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437", // balas 109 | "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", // Giszmo 110 | "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168", // monlovesmango 111 | "c5072866b41d6b88ab2ffee16ad7cb648f940867371a7808aaa94cf7d01f4188", // randymcmillan 112 | "00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700", // cameri 113 | "dd81a8bacbab0b5c3007d1672fb8301383b4e9583d431835985057223eb298a5", // plantimals 114 | "1c6b3be353041dd9e09bb568a4a92344e240b39ef5eb390f5e9e821273f0ae6f", // johnonchain 115 | "52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd", // semisol 116 | "47bae3a008414e24b4d91c8c170f7fce777dedc6780a462d010761dca6482327", // slaninas 117 | "c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86", // shawn 118 | "b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a", // 0xtr 119 | "f43c1f9bff677b8f27b602725ea0ad51af221344f69a6b352a74991a4479bac3", // manfromhighcastle 120 | "80482e60178c2ce996da6d67577f56a2b2c47ccb1c84c81f2b7960637cb71b78", // Leo 121 | "42a0825e980b9f97943d2501d99c3a3859d4e68cd6028c02afe58f96ba661a9d", // zerosequioso 122 | 123 | "3235036bd0957dfb27ccda02d452d7c763be40c91a1ac082ba6983b25238388c"}; // vishalxl ]; 124 | 125 | 126 | // dummy account pubkey 127 | const String gDummyAccountPubkey = "Non"; 128 | 129 | String gUserLocation = ""; 130 | 131 | const String gLocationNamePrefix = "Location: "; 132 | const String gLocationTagIdSuffix = " #location"; 133 | const String gTTagIdSuffix = " #t"; 134 | 135 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////// UI and Color 136 | const int gMinValidTextWidth = 60; // minimum text width acceptable 137 | const int gDefaultTextWidth = 96; // default text width 138 | int gTextWidth = gDefaultTextWidth; // is changed by --width option 139 | const int gSpacesPerDepth = 6; // constant 140 | int gNumLeftMarginSpaces = 0;// this number is modified in main 141 | String gAlignment = "center"; // is modified in main if --align argument is given 142 | const int gapBetweenTopTrees = 1; 143 | const int gNameLengthInPost = 12; 144 | 145 | // after depth of maxDepthAllowed the thread is re-aligned to left by leftShiftThreadBy 146 | const int gMinimumDepthAllowed = 2; 147 | const int gMaximumDepthAllowed = 12; 148 | const int gDefaultMaxDepth = 5; 149 | int maxDepthAllowed = gDefaultMaxDepth; 150 | const int leftShiftThreadsBy = 3; 151 | 152 | int gMaxLenUnbrokenWord = 8; // lines are broken if space is at end of line for this number of places 153 | 154 | int gMenuWidth = 36; 155 | 156 | int gNameLenDisplayed = 12; 157 | String gValidCheckMark = "✔️"; 158 | List gCheckMarksToRemove = ["✅","✔️"]; 159 | 160 | bool gShowLnInvoicesAsQr = false; 161 | const int gMinWidthForLnQr = 140; 162 | 163 | // event length printed 164 | const int gEventLenPrinted = 6; 165 | 166 | // used in word/event search 167 | const int gMinEventIdLenInSearch = gEventLenPrinted; 168 | 169 | // invalid int handling 170 | int gInvalidInputCount = 0; 171 | const int gMaxInValidInputAccepted = 40; 172 | 173 | // LN settings 174 | const int gMinLud06AddressLength = 10; // used in printProfile 175 | const int gMinLud16AddressLength = 3; // used in printProfile 176 | 177 | const int gMaxEventsInThreadPrinted = 20; 178 | const int gMaxInteger = 100000000000; // used in printTree 179 | String gWarning_TOO_MANY_TREES = "Note: This thread has more replies than those printed. Search for top post by id to see it fully."; 180 | 181 | // https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html#8-colors 182 | // Color related settings 183 | const String defaultTextColor = "green"; 184 | const String greenColor = "\x1B[32m"; // green 185 | const String yellowColor = "\x1B[33m"; // yellow 186 | const String magentaColor = "\x1B[35m"; // magenta 187 | const String cyanColor = "\x1b[36m"; // cyan 188 | const String whiteColor = "\x1b[37m"; // white 189 | const String blackColor = "\x1b[30m"; // black 190 | const String redColor = "\x1B[31m"; // red 191 | const String blueColor = "\x1b[34m"; // blue 192 | 193 | Map gColorMapForArguments = { "green": greenColor, 194 | "cyan" : cyanColor, 195 | "white": whiteColor, 196 | "black": blackColor, 197 | "red" : redColor, 198 | "blue" : blueColor}; 199 | 200 | const String brightBlackColor = "\x1b[90m"; // bright black 201 | const String brightRedColor = "\x1B[91m"; // bright red 202 | const String brightGreenColor = "\x1B[92m"; // bright green 203 | const String brightYellowColor = "\x1B[93m"; // bright yellow 204 | const String brightBlueColor = "\x1B[94m"; // bright blue 205 | const String brightCyanColor = "\x1B[96m"; // bright cyan 206 | const String brightMagentaColor = "\x1B[95m"; // bright magenta 207 | const String brightWhiteColor = "\x1b[97m"; // white 208 | 209 | // 33 yellow, 31 red, 34 blue, 35 magenta. Add 60 for bright versions. 210 | String gCommentColor = greenColor; 211 | String gNotificationColor = cyanColor; // cyan 212 | String gWarningColor = redColor; // red 213 | const String gColorEndMarker = "\x1B[0m"; 214 | 215 | // blue is too bright 216 | /* 217 | e & f are red 218 | c & d are pink 219 | a & b are orange 220 | 8 & 9 are yellow 221 | 6 & 7 are green 222 | 4 & 5 are light blue 223 | 2 & 3 are blue 224 | 0 & 1 are purple 225 | 226 | List nameColorPalette = [brightGreenColor, brightCyanColor, brightYellowColor, brightMagentaColor, 227 | brightBlueColor, brightRedColor, brightBlackColor, brightWhiteColor, 228 | yellowColor, magentaColor, redColor ]; 229 | 230 | List nameColorPalette = [brightMagentaColor, brightBlueColor, brightCyanColor, brightGreenColor, 231 | brightYellowColor, brightRedColor, yellowColor, redColor ]; 232 | */ 233 | 234 | Map pubkeyColor = { '0': magentaColor, '1': brightMagentaColor, 235 | '2': blueColor, '3': brightBlueColor, 236 | '4': cyanColor, '5': brightCyanColor, 237 | '6': brightGreenColor, '7': brightGreenColor, 238 | '8': brightYellowColor,'9': brightYellowColor, 239 | 'a': brightRedColor, 'b': brightRedColor, 240 | 'c': yellowColor, 'd': yellowColor, 241 | 'e': redColor, 'f': redColor 242 | }; 243 | 244 | String getNameColor( String pubkey) { 245 | if( pubkey.isEmpty) { 246 | return brightMagentaColor; 247 | } 248 | 249 | String firstChar = pubkey.substring(0, 1).toLowerCase(); 250 | return pubkeyColor[firstChar]??brightMagentaColor; 251 | } 252 | 253 | // By default the threads that were started in last one day are shown 254 | // this can be changed with 'days' command line argument 255 | const int gDefaultNumLastDays = 1; 256 | int gNumLastDays = gDefaultNumLastDays; 257 | 258 | const bool gWhetherToSendClientTag = true; 259 | 260 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////// bots related settings 261 | // bots ignored to reduce spam 262 | List gBots = [ "3b57518d02e6acfd5eb7198530b2e351e5a52278fb2499d14b66db2b5791c512", // robosats orderbook 263 | "887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072", // bestofhn 264 | "f4161c88558700d23af18d8a6386eb7d7fed769048e1297811dcc34e86858fb2", // bitcoin_bot 265 | "105dfb7467b6286f573cae17146c55133d0dcc8d65e5239844214412218a6c36", // zerohedge 266 | "e89538241bf737327f80a9e31bb5771ccbe8a4508c04f1d1c0ce7336706f1bee", // Bitcoin news 267 | "6a9eb714c2889aa32e449cfbb7854bc9780feed4ff3d887e03910dcb22aa560a", // "bible bot" 268 | 269 | "3104f98515b3aa147d55d9c2951e0f953b829d8724381d8f0d824125d7727634", // 42 spammer 270 | "6bc83d6a806b7a2c3e1fa07d3352402f7b6886b81a975090d6d89bb631c3dad9" 271 | ]; 272 | 273 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////// difficulty related settings 274 | const int gMaxDifficultyAllowed = 32; 275 | int gDifficulty = 0; 276 | 277 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////// channel related settings 278 | const int gNumChannelMessagesToShow = 18; 279 | const int gMaxChannelPagesDisplayed = 50; 280 | 281 | 282 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////// User interface messages 283 | String gDeletedEventMessage = "This post was deleted by its original writer"; 284 | 285 | const String gUsage = """$exename version $version 286 | The nostr console client built using dart. 287 | 288 | usage: $exename [OPTIONS] 289 | 290 | OPTIONS 291 | 292 | -k, --prikey The nsec or hex private key of user you want to 'log in' as. 293 | -p, --pubkey The npub or hex public key of user whose events and feed are shown. When given, 294 | posts/replies can't be sent because for that a private key is needed. 295 | -r, --relay The comma separated relay urls that are used as relays. If given, these are used 296 | rather than the default relays. 297 | -f, --file Read from given file, if it is present, and at the end of the program execution, write 298 | to it all the events (including the ones read, and any new received). Even if not given, 299 | the default is to read from and write to $gDefaultEventsFilename . Can be turned off by 300 | the --disable-file flag 301 | -d, --days The latest number of days for which events are shown. Default is $gDefaultNumLastDays. 302 | --request This request is sent verbatim to the default relay. It can be used to recieve all events 303 | from a relay. If not provided, then events for default or given user are shown. 304 | -s, --disable-file When turned on, even the default filename is not read from. 305 | -t, --translate Translate some of the recent posts using Google translate site ( and not api). Google 306 | is accessed for any translation request only if this flag is present, and not otherwise. 307 | -l, --lnqr Flag, if set any LN invoices starting with LNBC will be printed as a QR code. Will set 308 | width to $gMinWidthForLnQr, which can be reset if needed with the --width argument. Wider 309 | space is needed for some qr codes. 310 | -g, --location The given value is added as a 'location' tag with every kind 1 post made. g in shortcut 311 | standing for geographic location. 312 | -h, --help Print help/usage message and exit. 313 | -v, --version Print version and exit. 314 | 315 | UI Options 316 | -a, --align When "left" is given as option to this argument, then the text is aligned to left. By 317 | default the posts or text is aligned to the center of the terminal. 318 | -w, --width This specifies how wide you want the text to be, in number of columns. Default is $gDefaultTextWidth. 319 | Cant be less than $gMinValidTextWidth. 320 | -m, --maxdepth The maximum depth to which the threads can be displayed. Minimum is $gMinimumDepthAllowed and 321 | maximum allowed is $gMaximumDepthAllowed. 322 | -c, --color Color option can be green, cyan, white, black, red and blue. 323 | 324 | Advanced 325 | -y, --difficulty The difficulty number in bits, only for kind 1 messages. Tne next larger number divisible 326 | by 4 is taken as difficulty. Can't be more than 32 bits, because otherwise it typically 327 | takes too much time. Minimum and default is 0, which means no difficulty. 328 | -e, --overwrite Will over write the file with all the events that were read from file, and all newly 329 | received. Is useful when the file has to be cleared of old unused events. A backup should 330 | be made just in case of original file before invoking. 331 | """; 332 | 333 | const String helpAndAbout = 334 | ''' 335 | HOW TO USE 336 | ---------- 337 | 338 | Check out the main readme, wiki and discussions on github.com/vishalxl/nostr_console 339 | 340 | 341 | EXAMPLES 342 | -------- 343 | 344 | To 'login' as a user with private key K, where K should start with nsec or be a hex key of length 64 bytes. 345 | 346 | \$ nostr_console.exe --prikey=K 347 | 348 | To get ALL the latest messages for last 3 days (on linux bash which allows backtick execution): 349 | 350 | \$ nostr_console.exe --request=`echo "[\\"REQ\\",\\"l\\",{\\"since\\":\$(date -d \\'-3 day\\' +%s)}]"` 351 | 352 | To get the latest messages for user with private key K for last 4 days ( default is 1) from relay R: 353 | 354 | \$ nostr_console.exe --prikey=K --days=4 355 | 356 | To write events to a file ( and later read from it too), for any given private key K: 357 | 358 | \$ nostr_console.exe --file=eventsFile.txt --prikey=K 359 | 360 | PROGRAM ARGUMENTS 361 | ----------------- 362 | 363 | Also seen by giving --help option when invoking the application. 364 | 365 | $gUsage 366 | 367 | KNOWN ISSUES 368 | ------------ 369 | 370 | * On windows terminal, special characters such as accent ( as used in many languages) can't be sent. Emojis can't be sent either. But they can be sent from Linux/Mac. 371 | 372 | See and file bugs here: https://github.com/vishalxl/nostr_console/issues 373 | 374 | ABOUT 375 | ----- 376 | 377 | Nostr console/terminal client. Built using Dart. 378 | Source Code and Binaries: https://github.com/vishalxl/nostr_console 379 | 380 | '''; 381 | 382 | /////////////////////////////////////////////////////////print intro 383 | void printIntro(String msg) { 384 | 385 | String intro = 386 | """ 387 | 388 | ▀█▄ ▀█▀ ▄ 389 | █▀█ █ ▄▄▄ ▄▄▄▄ ▄██▄ ▄▄▄ ▄▄ 390 | █ ▀█▄ █ ▄█ ▀█▄ ██▄ ▀ ██ ██▀ ▀▀ 391 | █ ███ ██ ██ ▄ ▀█▄▄ ██ ██ 392 | ▄█▄ ▀█▄ ▀█▄▄█▀ █▀▄▄█▀ ▀█▄▀ ▄██▄ 393 | 394 | ██████╗ ██████╗ ███╗ ██╗███████╗ ██████╗ ██╗ ███████╗ 395 | ██╔════╝██╔═══██╗████╗ ██║██╔════╝██╔═══██╗██║ ██╔════╝ 396 | ██║ ██║ ██║██╔██╗ ██║███████╗██║ ██║██║ █████╗ 397 | ██║ ██║ ██║██║╚██╗██║╚════██║██║ ██║██║ ██╔══╝ 398 | ╚██████╗╚██████╔╝██║ ╚████║███████║╚██████╔╝███████╗███████╗ 399 | ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚══════╝╚══════╝ 400 | 401 | 402 | """; 403 | 404 | List lines = intro.split("\n"); 405 | 406 | var terminalColumns = gDefaultTextWidth; 407 | 408 | if( stdout.hasTerminal ) { 409 | terminalColumns = stdout.terminalColumns; 410 | } 411 | 412 | for (var line in lines) {print(line.length > terminalColumns ? line.substring(0, terminalColumns) : line );} 413 | 414 | } 415 | 416 | void printInfoForNewUser() { 417 | print("""\nFor new users: The app only gets kind 1 events from people you follow or some popular well known pubkeys. 418 | If you see a message such as 'event not loaded' it implies its from someone you don't follow. Such events 419 | are eventually loaded; however, the ideal way to use this app is to follow people whose posts you want to read or follow.\n"""); 420 | } 421 | 422 | /////////////////////////////////////////////////////////other settings related functions 423 | 424 | void printUsage() { 425 | print(gUsage); 426 | } 427 | void printVersion() { 428 | print(version); 429 | } 430 | 431 | -------------------------------------------------------------------------------- /lib/user.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:nostr_console/event_ds.dart'; 3 | import 'package:nostr_console/utils.dart'; 4 | 5 | // is set intermittently by functions. and used as required. Should be kept in sync as the kind 3 for user are received. 6 | Set gFollowList = {}; 7 | 8 | // From the list of events provided, lookup the lastst contact information for the given user/pubkey 9 | Event? getContactEvent(String pubkey) { 10 | 11 | // get the latest kind 3 event for the user, which lists his 'follows' list 12 | if( gKindONames.containsKey(pubkey)) { 13 | Event? e = (gKindONames[pubkey]?.latestContactEvent); 14 | return e; 15 | } 16 | 17 | return null; 18 | } 19 | 20 | // returns all follows 21 | Set getFollows(String pubkey) { 22 | Set followPubkeys = {}; 23 | 24 | Event? profileContactEvent = getContactEvent(pubkey); 25 | if( profileContactEvent != null) { 26 | for (var x in profileContactEvent.eventData.contactList) { 27 | followPubkeys.add(x.contactPubkey); 28 | } 29 | } 30 | 31 | return followPubkeys; 32 | } 33 | 34 | // returns all mutual follows 35 | Set getMutualFollows(String pubkey) { 36 | Set mutualFollowPubkeys = {}; 37 | 38 | Event? profileContactEvent = getContactEvent(pubkey); 39 | if( profileContactEvent != null) { 40 | for (var x in profileContactEvent.eventData.contactList) { // go over each follow 41 | Event? followContactEvent = getContactEvent(x.contactPubkey); 42 | if( followContactEvent != null) { 43 | for (var y in followContactEvent.eventData.contactList) { // go over the follow's friend list 44 | mutualFollowPubkeys.add(x.contactPubkey); 45 | break; 46 | } 47 | } 48 | } 49 | } 50 | 51 | //print("number of mutual follows being returned: ${mutualFollowPubkeys.length}"); 52 | return mutualFollowPubkeys; 53 | } 54 | 55 | Set getUserChannels(Set userEvents, String userPublicKey) { 56 | Set userChannels = {}; 57 | 58 | for (var event in userEvents) { 59 | if( event.eventData.pubkey == userPublicKey) { 60 | if( [42, 142].contains( event.eventData.kind) ) { 61 | String channelId = event.eventData.getChannelIdForKind4x(); 62 | if( channelId.length == 64) { 63 | userChannels.add(channelId); 64 | } 65 | } else if([40,41,140,141].contains(event.eventData.kind)) { 66 | userChannels.add(event.eventData.id); 67 | } 68 | } 69 | } 70 | 71 | return userChannels; 72 | } 73 | 74 | void addToHistogram(Map histogram, List pTags) { 75 | Set tempPtags = {}; 76 | pTags.retainWhere((x) => tempPtags.add(x)); 77 | 78 | for(int i = 0; i < pTags.length; i++ ) { 79 | String pTag = pTags[i]; 80 | if( histogram.containsKey(pTag)) { 81 | int? val = histogram[pTag]; 82 | if( val != null) { 83 | histogram[pTag] = ++val; 84 | } else { 85 | } 86 | } else { 87 | histogram[pTag] = 1; 88 | } 89 | } 90 | //return histogram; 91 | } 92 | 93 | // return the numMostFrequent number of most frequent p tags ( user pubkeys) in the given events 94 | Set getpTags(Set events, int numMostFrequent) { 95 | List listHistogram = []; 96 | Map histogramMap = {}; 97 | for(var event in events) { 98 | addToHistogram(histogramMap, event.eventData.pTags); 99 | } 100 | 101 | histogramMap.forEach((key, value) {listHistogram.add(HistogramEntry(key, value));/* print("added to list of histogramEntry $key $value"); */}); 102 | listHistogram.sort(HistogramEntry.histogramSorter); 103 | List ptags = []; 104 | for( int i = 0; i < listHistogram.length && i < numMostFrequent; i++ ) { 105 | ptags.add(listHistogram[i].str); 106 | } 107 | 108 | return ptags.toSet(); 109 | } 110 | 111 | Set getOnlyUserEvents(Set initialEvents, String userPubkey) { 112 | Set userEvents = {}; 113 | for (var event in initialEvents) { 114 | if( event.eventData.pubkey == userPubkey) { 115 | userEvents.add(event.eventData.id); 116 | } 117 | } 118 | return userEvents; 119 | } 120 | 121 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:qr/qr.dart'; 3 | import 'package:nostr_console/nip_019.dart'; 4 | 5 | enum enumRoomType { kind4, kind40, kind140, RoomLocationTag, RoomTTag} 6 | 7 | int gMinLnInvoiceLength = 20; // TODO put real value 8 | int gMaxStrLenForQrCode = 600; // in bytes, maximum acceptable length of string that is converted to qr code. for lnbc1 invoices 9 | 10 | String getPostKindFrom(enumRoomType eType) { 11 | switch (eType) { 12 | case enumRoomType.kind4: 13 | return "4"; 14 | case enumRoomType.kind40: 15 | return "42"; 16 | case enumRoomType.kind140: 17 | return "142"; 18 | case enumRoomType.RoomLocationTag: 19 | return "1"; 20 | case enumRoomType.RoomTTag: 21 | return "1"; 22 | } 23 | } 24 | 25 | Set? getTagsFromContent(String content) { 26 | Set? tags; 27 | 28 | String regexp1 = '(#[a-zA-Z0-9_-]+ )|(#[a-zA-Z0-9_-]+)\$'; 29 | RegExp httpRegExp = RegExp(regexp1); 30 | 31 | for( var match in httpRegExp.allMatches(content) ) { 32 | tags ??= {}; 33 | 34 | tags.add( content.substring(match.start + 1, match.end).trim() ); 35 | } 36 | return tags; 37 | } 38 | 39 | 40 | class HistogramEntry { 41 | String str; 42 | int count; 43 | HistogramEntry(this.str, this.count); 44 | static int histogramSorter(HistogramEntry a, HistogramEntry b) { 45 | if( a.count < b.count ) { 46 | return 1; 47 | } if( a.count == b.count ) { 48 | return 0; 49 | } else { 50 | return -1; 51 | } 52 | } 53 | } 54 | 55 | Future myWait(int ms) async { 56 | Future foo1() async { 57 | await Future.delayed(Duration(milliseconds: ms)); 58 | return; 59 | } 60 | await foo1(); 61 | } 62 | 63 | bool isNumeric(String s) { 64 | return double.tryParse(s) != null; 65 | } 66 | 67 | bool isWordSeparater(String s) { 68 | if( s.length != 1) { 69 | return false; 70 | } 71 | return s[0] == ' ' || s[0] == '\n' || s[0] == '\r' || s[0] == '\t' 72 | || s[0] == ',' || s[0] == '.' || s[0] == '-' || s[0] == '('|| s[0] == ')'; 73 | } 74 | 75 | 76 | bool isWhitespace(String s) { 77 | if( s.length != 1) { 78 | return false; 79 | } 80 | return s[0] == ' ' || s[0] == '\n' || s[0] == '\r' || s[0] == '\t'; 81 | } 82 | 83 | extension StringX on String { 84 | 85 | int isChannelPageNumber(int max) { 86 | 87 | if(length < 2 || this[0] != '/') { 88 | return 0; 89 | } 90 | 91 | String rest = substring(1); 92 | 93 | //print("rest = $rest"); 94 | int? n = int.tryParse(rest); 95 | if( n != null) { 96 | if( n < max) { 97 | return n; 98 | } 99 | } 100 | return 0; 101 | } 102 | 103 | isEnglish( ) { 104 | // since smaller words can be smileys they should not be translated 105 | if( length < 10) { 106 | return true; 107 | } 108 | 109 | if( !isLatinAlphabet()) { 110 | return false; 111 | } 112 | 113 | if (isRomanceLanguage()) { 114 | return false; 115 | } 116 | 117 | return true; 118 | } 119 | 120 | isPortugese() { 121 | false; // https://1000mostcommonwords.com/1000-most-common-portuguese-words/ 122 | } 123 | 124 | bool isRomanceLanguage() { 125 | 126 | // https://www.thoughtco.com/most-common-french-words-1372759 127 | Set frenchWords = {"oui", "je", "le", "un", "de", "merci", "une", "ce", "pas"}; // "et" is in 'et al' 128 | Set spanishWords = {"y", "se", "el", "uso", "que", "te", "los", "va", "ser", "si", "por", "lo", "es", "era", "un", "o"}; 129 | Set portugeseWords = {"como", "seu", "que", "ele", "foi", "eles", "tem", "este", "por", "quente", "vai", 130 | "ter", "mas", "ou", "teve", "fora", "é", "te", "mais"}; 131 | 132 | Set romanceWords = frenchWords.union(spanishWords).union(portugeseWords); 133 | for( String word in romanceWords) { 134 | if( toLowerCase().contains(" $word ")) { 135 | return true; 136 | } 137 | } 138 | return false; 139 | } 140 | 141 | isLatinAlphabet({caseSensitive = false}) { 142 | int countLatinletters = 0; 143 | for (int i = 0; i < length; i++) { 144 | final target = caseSensitive ? this[i] : this[i].toLowerCase(); 145 | if ( (target.codeUnitAt(0) > 96 && target.codeUnitAt(0) < 123) || ( isNumeric(target) ) || isWhitespace(target)) { 146 | countLatinletters++; 147 | } 148 | } 149 | 150 | if( countLatinletters < ( 40.0/100 ) * length ) { 151 | return false; 152 | } else { 153 | return true; 154 | } 155 | } 156 | } 157 | 158 | bool isValidHexPubkey(String pubkey) { 159 | if( pubkey.length == 64) { 160 | return true; 161 | } 162 | 163 | return false; 164 | } 165 | 166 | String myPadRight(String str, int width) { 167 | String newStr = ""; 168 | 169 | if( str.length < width) { 170 | newStr = str.padRight(width); 171 | } else { 172 | newStr = str.substring(0, width); 173 | } 174 | return newStr; 175 | } 176 | 177 | // returns tags as string that can be used to calculate event has. called from EventData constructor 178 | String getStrTagsFromJson(dynamic json) { 179 | String str = ""; 180 | 181 | int i = 0; 182 | for( dynamic tag in json ) { 183 | if( i != 0) { 184 | str += ","; 185 | } 186 | 187 | str += "["; 188 | int j = 0; 189 | for(dynamic element in tag) { 190 | if( j != 0) { 191 | str += ","; 192 | } 193 | str += "\"${element.toString()}\""; 194 | j++; 195 | } 196 | str += "]"; 197 | i++; 198 | } 199 | return str; 200 | } 201 | 202 | String addEscapeChars(String str) { 203 | String temp = ""; 204 | //temp = temp.replaceAll("\\", "\\\\"); 205 | temp = str.replaceAll("\"", "\\\""); 206 | return temp.replaceAll("\n", "\\n"); 207 | } 208 | 209 | String unEscapeChars(String str) { 210 | String temp = str.replaceAll("\"", "\\\""); 211 | temp = temp.replaceAll("\n", "\\n"); 212 | return temp; 213 | } 214 | 215 | void printUnderlined(String x) { stdout.write("$x\n${getNumDashes(x.length)}\n");} 216 | 217 | String getNumSpaces(int num) { 218 | String s = ""; 219 | for( int i = 0; i < num; i++) { 220 | s += " "; 221 | } 222 | return s; 223 | } 224 | 225 | String getNumDashes(int num, [String dashType = "-"]) { 226 | String s = ""; 227 | for( int i = 0; i < num; i++) { 228 | s += dashType; 229 | } 230 | return s; 231 | } 232 | 233 | List> getUrlRanges(String s) { 234 | List> urlRanges = []; 235 | String regexp1 = "http[s]*://[a-zA-Z0-9]+([.a-zA-Z0-9/_\\-\\#\\+=\\&\\?]*)"; 236 | 237 | RegExp httpRegExp = RegExp(regexp1); 238 | for( var match in httpRegExp.allMatches(s) ) { 239 | List entry = [match.start, match.end]; 240 | urlRanges.add(entry); 241 | } 242 | 243 | return urlRanges; 244 | } 245 | 246 | // returns true if n is in any of the ranges given in list 247 | int isInRange( int n, List> ranges ) { 248 | for( int i = 0; i < ranges.length; i++) { 249 | if( n >= ranges[i][0] && n < ranges[i][1]) { 250 | return ranges[i][1]; 251 | } 252 | } 253 | return 0; 254 | } 255 | 256 | // https://jpgraph.net/download/manuals/chunkhtml/ch27.html 257 | // both go from 1 to 20 inclusive. index is type. 258 | List qrMaxDataBits = [152, 272, 440, 640, 864, 1088, 1248, 1552, 1856, 2192, 2592, 2960, 3424, 3688, 4184, 4712, 5176, 5768, 6360, 6888]; 259 | List qrModules = [21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 97]; 260 | 261 | // return type and module as entries in a list 262 | List? getTypeAndModule(String str) { 263 | if( qrMaxDataBits.length != qrModules.length) { 264 | return null; 265 | } 266 | 267 | // 5 for padding which it seems to need, otherwise it gives error like 'QrInputTooLongException: Input too long. 2212 > 2192' for a str which is exactly 2192 268 | int strLen = str.length + 5; 269 | for( int i = 0; i < qrModules.length; i++) { 270 | if( strLen * 8 <= qrMaxDataBits[i]) { 271 | return [i+1, qrModules[i]]; 272 | } 273 | } 274 | 275 | return null; 276 | } 277 | 278 | bool sanityChecked(String lnInvoice) { 279 | 280 | if( lnInvoice.length < gMinLnInvoiceLength) { 281 | return false; 282 | } 283 | 284 | if( lnInvoice.substring(0, 4).toLowerCase() != "lnbc") { 285 | return false; 286 | } 287 | 288 | return true; 289 | } 290 | 291 | String expandLNInvoices(String content) { 292 | 293 | String regexp1 = '(lnbc[a-zA-Z0-9]+)'; 294 | RegExp httpRegExp = RegExp(regexp1); 295 | 296 | for( var match in httpRegExp.allMatches(content.toLowerCase()) ) { 297 | String lnInvoice = content.substring(match.start, match.end); 298 | 299 | if( !sanityChecked(lnInvoice)) { 300 | continue; 301 | } 302 | 303 | if( lnInvoice.length > gMaxStrLenForQrCode) { 304 | continue; 305 | } 306 | 307 | String qrStr = ""; 308 | 309 | List? typeAndModule = getTypeAndModule(lnInvoice); 310 | if( typeAndModule == null) { 311 | continue; 312 | } 313 | 314 | qrStr = getPubkeyAsQrString(lnInvoice, typeAndModule[0], typeAndModule[1], ""); 315 | content = "${content.substring(0, match.start)}:-\n\n$qrStr\n\n${content.substring(match.end)}"; 316 | } 317 | 318 | return content; 319 | } 320 | 321 | // https://www.sproutqr.com/blog/qr-code-types 322 | // https://jpgraph.net/download/manuals/chunkhtml/ch27.html 323 | // default 4 and 33 work for pubkey 324 | String getPubkeyAsQrString(String str, [int typeNumber = 4, moduleCount = 33, String leftPadding = " "]) { 325 | String output = ""; 326 | 327 | final qrCode = QrCode(typeNumber, QrErrorCorrectLevel.L) 328 | ..addData(str); 329 | final qrImage = QrImage(qrCode); 330 | 331 | assert( qrImage.moduleCount == moduleCount); 332 | var x = 0; 333 | for (x = 0; x < qrImage.moduleCount -1 ; x += 2) { 334 | output += leftPadding; 335 | for (var y = 0; y < qrImage.moduleCount ; y++) { 336 | 337 | bool topDark = qrImage.isDark(y, x); 338 | bool bottomDark = qrImage.isDark(y, x + 1); 339 | if (topDark && bottomDark) { 340 | output += "█"; 341 | } 342 | else if (topDark ) { 343 | output += "▀"; 344 | } else if ( bottomDark) { 345 | output += "▄"; 346 | } else if( !topDark && !bottomDark) { 347 | output += " "; 348 | } 349 | } 350 | output += "\n"; 351 | } 352 | 353 | if( qrImage.moduleCount %2 == 1) { 354 | output += leftPadding; 355 | for (var y = 0; y < qrImage.moduleCount ; y++) { 356 | bool dark = qrImage.isDark(y, x); 357 | if (dark ) { 358 | output += "▀"; 359 | } else { 360 | output += " "; 361 | } 362 | 363 | } 364 | output += "\n"; 365 | } 366 | 367 | return output; 368 | } 369 | 370 | void clearScreen() { 371 | print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); 372 | } 373 | 374 | // returns a string entered by the user 375 | String getStringFromUser(String prompt, [String defaultValue=""] ) { 376 | String str = ""; 377 | 378 | stdout.write(prompt); 379 | str = (stdin.readLineSync())??""; 380 | 381 | if( str.isEmpty) { 382 | str = defaultValue; 383 | } 384 | return str; 385 | } 386 | 387 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// relay related functions 388 | 389 | // returns list in form ( if 3 sized list) 390 | // "pubkey1","pubkey2","pubkey3" 391 | String getCommaSeparatedQuotedStrs(Set publicKeys) { 392 | String s = ""; 393 | int i = 0; 394 | for(String pubkey in publicKeys) { 395 | s += "\"${pubkey.toLowerCase()}\""; 396 | if( i < publicKeys.length - 1) { 397 | s += ","; 398 | } 399 | i++; 400 | } 401 | return s; 402 | } 403 | 404 | String getCommaSeparatedInts(Set? kind) { 405 | if( kind == null) { 406 | return ""; 407 | } 408 | 409 | if( kind.isEmpty) { 410 | return ""; 411 | } 412 | 413 | String strKind = ""; 414 | int i = 0; 415 | 416 | for (var k in kind) { 417 | String comma = ","; 418 | if( i == kind.length-1) { 419 | comma = ""; 420 | } 421 | strKind = strKind + k.toString() + comma; 422 | i++; 423 | } 424 | 425 | return strKind; 426 | } 427 | 428 | String getKindRequest(String subscriptionId, List kind, int limit, int sinceWhen) { 429 | String strTime = ""; 430 | if( sinceWhen != 0) { 431 | strTime = ', "since":${sinceWhen.toString()}'; 432 | } 433 | var strSubscription1 = '["REQ","$subscriptionId",{"kinds":['; 434 | var strSubscription2 ='], "limit":$limit$strTime } ]'; 435 | 436 | String strKind = getCommaSeparatedInts(kind.toSet()); 437 | 438 | String strRequest = strSubscription1 + strKind + strSubscription2; 439 | return strRequest; 440 | } 441 | 442 | String getUserRequest(String subscriptionId, String publicKey, int numUserEvents, int sinceWhen, [Set? kind]) { 443 | Set kind = {}; 444 | kind = kind; 445 | 446 | String strKind = getCommaSeparatedInts(kind); 447 | 448 | String strKindSection = ""; 449 | if( strKind.isNotEmpty) { 450 | strKindSection = '"kinds":[$strKind],'; 451 | } 452 | 453 | String strTime = ""; 454 | if( sinceWhen != 0) { 455 | strTime = ', "since": ${sinceWhen.toString()}'; 456 | } 457 | var strSubscription1 = '["REQ","$subscriptionId",{ "authors": ["'; 458 | var strSubscription2 ='"],$strKindSection"limit": $numUserEvents $strTime } ]'; 459 | String request = strSubscription1 + publicKey.toLowerCase() + strSubscription2; 460 | return request; 461 | } 462 | 463 | String getMentionRequest(String subscriptionId, Set ids, int numUserEvents, int sinceWhen, String tagToGet) { 464 | String strTime = ""; 465 | if( sinceWhen != 0) { 466 | strTime = ', "since": ${sinceWhen.toString()}'; 467 | } 468 | var strSubscription1 = '["REQ","$subscriptionId",{ "$tagToGet": ['; 469 | var strSubscription2 ='], "limit": $numUserEvents $strTime } ]'; 470 | return strSubscription1 + getCommaSeparatedQuotedStrs(ids) + strSubscription2; 471 | } 472 | 473 | String getIdAndMentionRequest(String subscriptionId, Set ids, int numUserEvents, int idSinceWhen, int mentionSinceWhen, String tagToGet, String idString) { 474 | String idStrTime = "", mentionStrTime = ""; 475 | if( idSinceWhen != 0) { 476 | idStrTime = ', "since": ${idSinceWhen.toString()}'; 477 | } 478 | 479 | if( mentionSinceWhen != 0) { 480 | mentionStrTime = ', "since": ${mentionSinceWhen.toString()}'; 481 | } 482 | 483 | var strSubscription1 = '["REQ","$subscriptionId",{ "$tagToGet": ['; 484 | var strSubscription2 ='], "limit": $numUserEvents $idStrTime } ]'; 485 | String req = '["REQ","$subscriptionId",{ "$tagToGet": [${getCommaSeparatedQuotedStrs(ids)}], "limit": $numUserEvents $mentionStrTime},{"$idString":[${getCommaSeparatedQuotedStrs(ids)}]$idStrTime}]'; 486 | return req; 487 | } 488 | 489 | 490 | String getMultiUserRequest(String subscriptionId, Set publicKeys, int numUserEvents, int sinceWhen, [Set? kind]) { 491 | String strTime = ""; 492 | if( sinceWhen != 0) { 493 | strTime = ', "since": ${sinceWhen.toString()}'; 494 | } 495 | 496 | String strKind = getCommaSeparatedInts(kind); 497 | 498 | String strKindSection = ""; 499 | if( strKind.isNotEmpty) { 500 | strKindSection = '"kinds":[$strKind],'; 501 | } 502 | 503 | var strSubscription1 = '["REQ","$subscriptionId",{ "authors": ['; 504 | var strSubscription2 ='],$strKindSection"limit": $numUserEvents $strTime } ]'; 505 | String s = ""; 506 | s = getCommaSeparatedQuotedStrs(publicKeys); 507 | String request = strSubscription1 + s + strSubscription2; 508 | return request; 509 | } 510 | 511 | // ends with a newline 512 | void printSet( Set toPrint, [ String prefix = "", String separator = ""]) { 513 | stdout.write(prefix); 514 | 515 | int i = 0; 516 | for (var element in toPrint) { 517 | if( i != 0) { 518 | stdout.write(separator); 519 | } 520 | 521 | stdout.write(element); 522 | i++; 523 | } 524 | stdout.write("\n"); 525 | } 526 | 527 | 528 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nostr_console 2 | description: A multi-platform nostr client built for terminal/console 3 | version: 0.3.6-beta 4 | homepage: https://github.com/vishalxl/nostr_console 5 | 6 | # 0.3.6 7 | # 8 | 9 | # 0.3.5 10 | # fix for crash in issue #70 11 | # improved highlighted events code in 2->1 12 | # in 2->1 printed popular accounts with follows 13 | # difficulty limit set to 32 14 | # added three new relays and removed some older ones 15 | # fixed fetching of contact names ( now all are fetched whereas previously they werent) 16 | 17 | # 0.3.4 18 | # improved logic that only new events from follows are shown; this reduces the flood of notifications seen 19 | # longer-named follows were not getting tick mark at end. fixed it, now they get tick marks in posts, channels and in one liners for channels 20 | # fix lud06/16 qr code printing in profiles 21 | # ask user y/n to avoid overwriting contact list if no contact list is seen for user 22 | # saved dm's sent to user 23 | 24 | # 0.3.3 25 | # Linux arm64 build added; docker images pushed to github store; and improving of build process by @AaronDewes 26 | # added display_name and website field support for reading and updating kind 0 , or profile 27 | # color fix - where likes to right of a notification-like were shown in white rather than as colored text 28 | 29 | # 0.3.2 30 | # added build for ubuntu arm 64, and mac arm 64 31 | # fixed or improved mention expansion 32 | # displyed global feed. which has all latest in last 2 hours 33 | # in incoming notifications, only showed notifications for follows. 34 | # In writing events, only writing follow's events. and the ones they interact with. 35 | # now friends have a tick; no tick for defaults 36 | # fixed likes colors issue for notification likes 37 | 38 | # after tag 39 | # fixed sdk for arm64 build; otherwise only x86 was being built for all 40 | # notification color fix for git bash; color for likes is not being closed at end 41 | 42 | # 0.3.1 43 | # added nostr.ch as another default relay to sync with anigma 44 | # printed only 20 maximum events in a thread to reduce screen spam from long threads. Only in search results are all threads printed; and a thread or event can be searched by 6 digit id-prefix of the event thats mentioned. 45 | # improved user notifications , menu 2 -> 3, now likes are shown as notifications and less informative 46 | # improved 2 -> 5. better printing. and fixed white after reaction highlighed issue 47 | # improved 2 -> 6 too, now follows posts get highlighed, and so do their reactions. 48 | # mentions get highlighed in above menus 49 | # if too many wrong menu inputs are given ( >40) then program exits without saving any new events. for issue #49 50 | # showed lud06 and lud16, if any, in profile as qr code 51 | 52 | # after tagging 53 | # improved notification count and display with recent clipped-thread change 54 | # fixed issue where had to go to main menu from SN menu to get notifications ; related: also got notifications in other menus so now on following someone, that event is processed in this menu itself 55 | # added ligning prefix in profile 56 | 57 | 58 | # 0.3.0 59 | # added check marks; added more default users 60 | # changed fetch logic: after fetching all friends contacts etc, then reset the relays, and fetched ALL the events in last 2 hours. but not closing connection right now of old relays. 61 | # fixed URI exception for NIP05 62 | # sorted lists printed in profile ( radixrat) 63 | # sorted tree children - now posts get printed in sorted order 64 | # --lnqr print LNBC invoices as qr code 65 | # only maximum 500 contacts are fetched. 66 | # fetching logic: first all follows, ~50 well known accounts, and top tagged people are fetched for few days. Then all live events are fetched for last 2 hous and ongoing. 67 | 68 | # test fix 69 | 70 | #0.2.9 71 | # improved fetching logic 72 | # added more relays; minor tweaks in relay set usage 73 | 74 | ## after taggin 75 | # only maximum 500 contacts are fetched ( at random) 76 | 77 | # 0.2.8 78 | # reduced items fetched. 23/12 79 | # reduced items more evening 23/12 80 | # reduced more evening 23/12 81 | # channel fetches for 3-4 days 82 | 83 | 84 | #0.2.7 85 | # improved relay, fetching logic and added more default pubkeys to fetch 86 | # incresed user id lenth to 5, and event id len to 6 in SN 87 | 88 | # after tagging 89 | # fixed new issue of taking longer time when file was already there 90 | # increased channel fetches from 2 days from half a day 91 | 92 | 93 | environment: 94 | sdk: '>=2.17.3 <4.0.0' 95 | 96 | 97 | 98 | dev_dependencies: 99 | lints: ^3.0.0 100 | test: ^1.21.4 101 | dependencies: 102 | args: ^2.3.1 103 | bip340: ^0.3.0 104 | crypto: ^3.0.2 105 | intl: ^0.19.0 106 | translator: ^1.0.0 107 | web_socket_channel: ^2.2.0 108 | logging: ^1.0.2 109 | kepler: ^1.0.3 110 | qr: ^3.0.1 111 | pointycastle: any 112 | http: any 113 | bech32: ^0.2.2 114 | convert: ^3.1.1 115 | -------------------------------------------------------------------------------- /scripts/announce.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # writes hello to a group 4 | # echo '\n' ; for line in `cowsay hi` ; do echo -e "${line}\\\n" ; done 5 | 6 | IFS=$'\n' 7 | channel=25e5c 8 | # { echo -e "3\n1\n${channel}\nHello, this is a random test.\nx\nx\nx" ; cat /dev/stdin; } | ./nostr_console_ubuntu_x64 --prikey=`openssl rand -hex 32` 9 | 10 | 11 | # \n\n.____ \n< hi > \n ---- \n \\\n ^__^ \n (oo)_______ \n (__) )/ \n ||----w | \n || || \n 12 | message="" 13 | message=$message'\n' ; for line in `cowsay hi` ; do message=$message"${line} \n" ; done 14 | echo $message 15 | { echo -e "3\n1\n${channel}\nHello, this is a random test.\nx\nx\nx" ; cat /dev/stdin; } | dart run ../bin/nostr_console.dart --prikey=`openssl rand -hex 32` 16 | -------------------------------------------------------------------------------- /scripts/gotoChannel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # * tested on Linux/Ubuntu 4 | # * will go to the channel mentioned in this variable; change it to go to that channel 5 | # * arguments passed to this script are passed to the nostr_console 6 | 7 | 8 | #channel=52ca nostr console channel 9 | 10 | channel=25e5c # nostr channel 11 | { echo -e "3\n1\n${channel}" ; cat /dev/stdin; } | dart run ../bin/nostr_console.dart $@ 12 | 13 | -------------------------------------------------------------------------------- /scripts/output_test_servers.txt: -------------------------------------------------------------------------------- 1 | Requesting all events in last 1 hours with a limit of 300 by executing the following command for each: 2 | Getting all events, with limit 300, from servers in last 1 hours by running command: 3 | echo ["REQ","l",{"since":1709514739,"limit":300}] | websocat 2> /dev/null | wc -l 4 | 5 | 6 | Testing wss://relay.damus.io : 301 7 | Testing wss://nostr-2.zebedee.cloud : 0 8 | Testing wss://nostr.zebedee.cloud : 0 9 | Testing wss://nostr.coinos.io : 0 10 | Testing wss://nostr-01.bolt.observer : 100 11 | Testing wss://nostr-relay.wlvs.space : 0 12 | Testing wss://nostr-pub.wellorder.net : 0 13 | Testing wss://nos.lol : 301 14 | Testing wss://nostr-relay-dev.wlvs.space : 0 15 | Testing wss://nostr.semisol.dev : 302 16 | Testing wss://relay.nostr.info : 0 17 | Testing wss://relay.snort.social : 301 18 | Testing wss://nostr.wine : 2 19 | Testing wss://eden.nostr.land : 156 20 | Testing wss://relay.current.fyi : 301 21 | Testing wss://relay.nostr.band : 301 22 | Testing wss://offchain.pub : 301 23 | Testing wss://nostr.relayer.se : 0 24 | Testing wss://relay1.nostrchat.io : 301 25 | Testing wss://relay2.nostrchat.io : 301 26 | Testing wss://nostr.radixrat.com : 0 27 | Testing wss://relay.nostr.ch : 0 28 | Testing wss://nostr.rdfriedl.com : 0 29 | Testing wss://relay.nostr.scot : 0 30 | Testing wss://knostr.neutrine.com : 0 31 | Testing wss://nostr.mom : 301 32 | Testing wss://nostr.drss.io : 0 33 | Testing wss://nostr.delo.software : 0 34 | Testing wss://nostr.zaprite.io : 0 35 | Testing wss://sg.qemura.xyz : 0 36 | Testing wss://nostr.zerofeerouting.com : 0 37 | Testing wss://nostr.satsophone.tk : 0 38 | Testing wss://relay.oldcity-bitcoiners.info : 0 39 | Testing wss://nostr.swiss-enigma.ch : 301 40 | Testing wss://nostr.onsats.org : 0 41 | Testing wss://nostr-relay.digitalmob.ro : 0 42 | Testing wss://offchain.pub : 301 43 | Testing wss://relay.valireum.net : 0 44 | Testing wss://nostr-relay.trustbtc.org : 0 45 | Testing wss://relay.stoner.com : 29 46 | Testing wss://nostr.w3ird.tech : 0 47 | Testing wss://nostr.bongbong.com : 0 48 | Testing wss://nostr.hugo.md : 0 49 | Testing wss://nostr.slothy.win : 10 50 | Testing wss://nostr.robotechy.com : 0 51 | Testing wss://nostr.nodeofsven.com : 301 52 | Testing wss://nostrrelay.com : 1 53 | Testing wss://nostr.mwmdev.com : 0 54 | Testing wss://nostr.sandwich.farm : 0 55 | Testing wss://brb.io : 0 56 | Testing wss://dummyurl.example.com : 0 57 | -------------------------------------------------------------------------------- /scripts/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | * **test_servers.sh** - will send a standard request to all servers and display the number of responses received in return. Useful to find which servers are working well. 4 | * **configfile.cfg** - has a list of nostr relays that are to be tested/used by other scripts 5 | * **send_request.sh** - sends the given request ( on command line) to all the servers in configfile 6 | -------------------------------------------------------------------------------- /scripts/relay_list_all.txt: -------------------------------------------------------------------------------- 1 | 2 | nostr_relays=( 3 | 4 | # some popular relays 5 | "wss://relay.damus.io" 6 | 7 | "wss://nostr-2.zebedee.cloud" 8 | "wss://nostr.zebedee.cloud" 9 | "wss://nostr.coinos.io" 10 | "wss://nostr-01.bolt.observer" 11 | 12 | "wss://nostr-relay.wlvs.space" 13 | "wss://nostr-relay-dev.wlvs.space" 14 | 15 | "wss://nostr-pub.wellorder.net" 16 | "wss://nos.lol" 17 | 18 | 19 | "wss://nostr.semisol.dev" 20 | "wss://relay.nostr.info" 21 | 22 | # snort default relays 23 | "wss://relay.snort.social" 24 | "wss://nostr.wine" 25 | 26 | # iris default relays 27 | "wss://eden.nostr.land" 28 | "wss://relay.current.fyi" 29 | "wss://relay.nostr.band" 30 | "wss://offchain.pub" 31 | "wss://nostr.relayer.se" 32 | 33 | # nostr chat relays 34 | "wss://relay1.nostrchat.io" 35 | "wss://relay2.nostrchat.io" 36 | 37 | "wss://nostr.radixrat.com" 38 | "wss://relay.nostr.ch" 39 | "wss://nostr.rdfriedl.com" 40 | "wss://relay.nostr.scot" 41 | "wss://knostr.neutrine.com" 42 | 43 | "wss://nostr.mom" 44 | 45 | "wss://nostr.drss.io" 46 | "wss://nostr.delo.software" 47 | "wss://nostr.zaprite.io" 48 | "wss://sg.qemura.xyz" 49 | 50 | "wss://nostr.zerofeerouting.com" 51 | "wss://nostr.satsophone.tk" 52 | "wss://relay.oldcity-bitcoiners.info" 53 | 54 | "wss://nostr.swiss-enigma.ch" 55 | "wss://nostr.onsats.org" 56 | "wss://nostr-relay.digitalmob.ro" 57 | 58 | "wss://relay.valireum.net" 59 | "wss://nostr-relay.trustbtc.org" 60 | "wss://relay.stoner.com" 61 | "wss://nostr.w3ird.tech" 62 | "wss://nostr.bongbong.com" 63 | "wss://nostr.hugo.md" 64 | 65 | "wss://nostr.slothy.win" 66 | "wss://nostr.robotechy.com" 67 | "wss://nostr.nodeofsven.com" 68 | "wss://nostrrelay.com" 69 | "wss://nostr.mwmdev.com" 70 | "wss://nostr.sandwich.farm" 71 | 72 | "wss://brb.io" 73 | 74 | "wss://dummyurl.example.com") 75 | 76 | 77 | 78 | # reference 79 | # nostr.info 80 | # https://nostr.watch/relays/find 81 | 82 | #"wss://nostr.openchain.fr" 83 | #"wss://nostr.shawnyeager.net" 84 | #"wss://relay.nostr.info" 85 | -------------------------------------------------------------------------------- /scripts/relay_list_best.txt: -------------------------------------------------------------------------------- 1 | 2 | nostr_relays=( 3 | 4 | "wss://relay.snort.social" 5 | "wss://relay.damus.io" 6 | "wss://nostr-01.bolt.observer" 7 | "wss://nos.lol" 8 | "wss://nostr.wine" 9 | 10 | "wss://relay.current.fyi" 11 | "wss://relay.nostr.band" 12 | "wss://offchain.pub" 13 | 14 | "wss://relay2.nostrchat.io" 15 | "wss://nostr.swiss-enigma.ch" 16 | "wss://offchain.pub" 17 | "wss://relay.stoner.com" 18 | 19 | 20 | # info site 21 | "wss://nostr.bitcoiner.social" 22 | "wss://nostr.fmt.wiz.biz" 23 | "wss://nostr.oxtr.dev" 24 | "wss://nostr.roundrockbitcoiners.com" 25 | "wss://nostr.vulpem.com" 26 | "wss://relay.nostr.band" 27 | "wss://soloco.nl" 28 | ) 29 | -------------------------------------------------------------------------------- /scripts/relay_list_nostr_info.txt: -------------------------------------------------------------------------------- 1 | # https://nostr.info/relays/ >> info relay list.txt 2 | # echo "nostr_relays=( "; for relay in `grep -o -E "wss://[a-z0-9.\-]+" info\ relay\ list.txt` ; do echo "\"$relay\""; done ; echo ")" 3 | 4 | 5 | nostr_relays=( 6 | 7 | "wss://relayable.org" 8 | "wss://lightningrelay.com" 9 | "wss://nostr.wine" 10 | "wss://at.nostrworks.com" 11 | "wss://brb.io" 12 | "wss://btc.klendazu.com" 13 | "wss://deschooling.us" 14 | "wss://knostr.neutrine.com" 15 | "wss://nos.lol" 16 | "wss://nostr-01.bolt.observer" 17 | "wss://nostr3.actn.io" 18 | "wss://nostr.bch.ninja" 19 | "wss://nostr.bitcoiner.social" 20 | "wss://nostr.cercatrova.me" 21 | "wss://nostr.easydns.ca" 22 | "wss://nostr.einundzwanzig.space" 23 | "wss://nostr.fmt.wiz.biz" 24 | "wss://nostr.middling.mydns.jp" 25 | "wss://nostr.mom" 26 | "wss://nostr.nodeofsven.com" 27 | "wss://nostr.noones.com" 28 | "wss://nostr.orangepill.dev" 29 | "wss://nostr.oxtr.dev" 30 | "wss://nostr.pobblelabs.org" 31 | "wss://nostr-pub.semisol.dev" 32 | "wss://nostr-relay.bitcoin.ninja" 33 | "wss://nostrrelay.com" 34 | "wss://nostr-relay.derekross.me" 35 | "wss://nostr-relay.schnitzel.world" 36 | "wss://nostr.roundrockbitcoiners.com" 37 | "wss://nostr.sectiontwo.org" 38 | "wss://nostr.semisol.dev" 39 | "wss://nostr.slothy.win" 40 | "wss://nostr.swiss-enigma.ch" 41 | "wss://nostr-verified.wellorder.net" 42 | "wss://nostr-verif.slothy.win" 43 | "wss://nostr.vulpem.com" 44 | "wss://relay.damus.io" 45 | "wss://relay.farscapian.com" 46 | "wss://relay.lexingtonbitcoin.org" 47 | "wss://relay.minds.com" 48 | "wss://relay.n057r.club" 49 | "wss://relay.nostr.band" 50 | "wss://relay.nostr.bg" 51 | "wss://relay.nostrid.com" 52 | "wss://relay.nostr.nu" 53 | "wss://relay.nostr.ro" 54 | "wss://relay.oldcity-bitcoiners.info" 55 | "wss://relay-pub.deschooling.us" 56 | "wss://relay.snort.social" 57 | "wss://relay.sovereign-stack.org" 58 | "wss://relay.stoner.com" 59 | "wss://nostr.mining.sc" 60 | "wss://nostr.cheeserobot.org" 61 | "wss://soloco.nl" 62 | ) -------------------------------------------------------------------------------- /scripts/send_request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # two events from jb55 and jack 4 | # ./send_request.sh '["REQ","nnn",{"limit":3,"ids":["76443db69b9851219ff96ed02d6e5dfe83d85214c64e10520b1706b729f19ebe", "6ca7cc0fabb88e62567cfcc24f57142cdb5acf63a98267d67c138851fb061ef1"]}]' 5 | 6 | configfile="./relay_list_all.txt" 7 | 8 | if [[ $# -eq 0 ]] ; then 9 | echo 'Usage: ./send_request.sh ' 10 | echo 'nostr relays filename contains list of relays to use; its default value is relay_list_all.txt' 11 | exit 1 12 | fi 13 | 14 | if [[ $# -eq 2 ]] ; then 15 | configfile=$2 16 | fi 17 | 18 | 19 | echo Going to use $configfile for list of relays to use. 20 | source ./$configfile 21 | 22 | # ./send_request.sh '["REQ","name",{"ids":["b10180"]}]' 23 | # ./send_request.sh '["REQ","nnn",{"limit":2,"ids":["d8aa6787834de19f0cb61b2aeef94886b2284f36f768bf8b5cc7533988346997"]}]' 24 | 25 | echo for loop 26 | for relay in ${nostr_relays[@]}; 27 | do 28 | >&2 echo -e "\n\n------------Sending $1 to $relay---------------------------\n" 29 | >&2 echo "echo $1 | websocat $relay " ; 30 | echo "$1" | websocat -B 300000 $relay 31 | 32 | done 33 | -------------------------------------------------------------------------------- /scripts/test_servers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | configfile="./relay_list_all.txt" 4 | 5 | if [[ $# -eq 1 ]] ; then 6 | if [ $1=="--help" ]; then 7 | echo 'Usage: ./test_servers.sh ' 8 | echo 'nostr relays filename contains list of relays to test; its default value is relay_list_all.txt' 9 | exit 1 10 | fi 11 | 12 | configfile=$1 13 | fi 14 | 15 | source $configfile 16 | 17 | limit=300 18 | numHours=1 19 | 20 | #echo -e "Requesting all events in last $numHours hours with a limit of $limit by executing the following command for each:" 21 | sinceSeconds=`date -d "-$numHours hour" +%s` ; 22 | 23 | #N=2 24 | #inLastNDays=`date -d "$N days" +%s` 25 | #echo "Events in last $N days" 26 | #req="[\"REQ\",\"id_mention_#p_nostr.coinos.io\",{\"#p\":[\"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2\"],\"limit\":20000,\"since\":$inLastNDays},{\"authors\":[\"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2\"]}]" 27 | 28 | 29 | req="[\"REQ\",\"l\",{\"since\":$sinceSeconds,\"limit\":$limit}]"; 30 | 31 | echo -e "Getting all events, with limit $limit, from servers in last $numHours hours by running command: " 32 | echo -e " echo $req | websocat 2> /dev/null | wc -l \n\n"; 33 | 34 | for relay in ${nostr_relays[@]}; 35 | do 36 | printf "Testing %-40s: " "$relay" 37 | echo "$req" | websocat -B 300000 $relay 2> /dev/null | wc -l 38 | 39 | done 40 | 41 | -------------------------------------------------------------------------------- /test/nostr_console_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:nostr_console/event_ds.dart'; 2 | import 'package:nostr_console/settings.dart'; 3 | import 'package:nostr_console/utils.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:nostr_console/tree_ds.dart'; 6 | import 'package:nostr_console/relays.dart'; 7 | 8 | 9 | EventData exampleEdata = EventData("id1", "pubkey", 1111111, 1, "content", [], [], [], [[]], {}); 10 | EventData exampleEdataChild = EventData("id2", "pubkey", 1111111, 1, "content child", [], [], [], [[]], {}); 11 | 12 | Event exampleEvent = Event('event', 'id3', exampleEdata, ['relay name'], "[json]"); 13 | Event exampleEventChild = Event('event', 'id4', exampleEdataChild, ['relay name'], "[json]"); 14 | 15 | Store exampleStore = Store([], {}, [], [], [], [], {}); 16 | Tree exampleTree = Tree.withoutStore(exampleEvent, []); 17 | 18 | //bool skipTest = true; 19 | 20 | Relays relays = Relays({}, {}, {}); 21 | 22 | void main() { 23 | 24 | test('invalid_relay', () async { 25 | 26 | String req = '["REQ","latest_live_all",{"limit":40000,"kinds":[0,1,3,4,5,6,7,40,41,42,104,140,141,142],"since":${getTimeSecondsAgo(gSecsLatestLive).toString()}}]'; 27 | sendRequest({"wss://invalidurl1234123134.com"}, req); 28 | 29 | }); 30 | 31 | 32 | test('printEventNode', () { 33 | Store store = exampleStore; 34 | Tree tree = exampleTree; 35 | Tree treeChild = Tree.withoutStore(exampleEvent, []); 36 | 37 | tree.setStore(store); 38 | treeChild.setStore(store); 39 | tree.children.add(treeChild); 40 | //store.printStoreTrees(0, DateTime.now().subtract(Duration(days:1)), selectorTrees_all); 41 | }); 42 | 43 | test('createNodeTree_ordered', () { 44 | 45 | Event exampleEvent1 = Event.fromJson('["EVENT","latest",{"id":"167063f491c41b7b8f79bc74f318e8a8b0a802bf8364b8bb7d19c887d59ec5de","pubkey":"137d948a0eee45e6cd113faaad934fcf17a97de2236c655b70650d4252daa9d3","created_at":1659722388,"kind":1,"tags":[],"content":"nostr is not federated is it? this is like a global feed of all nostr freaks?","sig":"6db0b287015d9529dfbacef91561cb4e32afd6968edd8454867b8482bde01452e17b6f3de69bffcb2d9deba2a52d3c9ff82e04f7b18eb32428daf7eab5fd27c5"}]', ""); 46 | Event exampleEvent2 = Event.fromJson('["EVENT","latest",{"id":"f3a267ecbb631012da618de620bc1fe265f6429f412359bf02330b437cf88e67","pubkey":"137d948a0eee45e6cd113faaad934fcf17a97de2236c655b70650d4252daa9d3","created_at":1659722463,"kind":1,"tags":[["e","167063f491c41b7b8f79bc74f318e8a8b0a802bf8364b8bb7d19c887d59ec5de"]],"content":"I don’t get the technical stuff about relays and things","sig":"9f68031687214a24862226f291e3baadd956dc14ba9c5c552f8c881a40aacd34feda667ef4e4b09711cd43950eec2d272d5b11bd7636de5f457f38f31eaff398"}]', ""); 47 | Event exampleEvent3 = Event.fromJson('["EVENT","latest",{"id":"dfc5765da281c0ad99cb8693fc98c87f0f86ad56042a414f06f19d41c1315fc3","pubkey":"137d948a0eee45e6cd113faaad934fcf17a97de2236c655b70650d4252daa9d3","created_at":1659722537,"kind":1,"tags":[["e","167063f491c41b7b8f79bc74f318e8a8b0a802bf8364b8bb7d19c887d59ec5de"],["e","f3a267ecbb631012da618de620bc1fe265f6429f412359bf02330b437cf88e67"]],"content":"different clients make sense to me. I can use different clients to access nostr but is just one giant soup like twitter","sig":"d4fdc288e3cb95fc5ab46177fc0982d2aaa3b028eef6649f8200500da9c2e9a16c7a0462638afef7635bfea3094ec10901de759a48e362b60cb08f7e6585e02f"}]', ""); 48 | 49 | Set listEvents = {exampleEvent1, exampleEvent2, exampleEvent3}; 50 | 51 | Store node = Store.fromEvents(listEvents); 52 | //node.printStoreTrees(0, DateTime.now().subtract(Duration(days: 1000)), (a) => true); 53 | //print("========================="); 54 | }); 55 | 56 | test('createNodeTree_unordered1', () { 57 | /** 58 | ▄──────────── 59 | █ 137: nostr is not federated is it? this is like a global feed of all nostr freaks? 60 | |id: 1670 , 11:29 PM Aug 5 61 | 62 | 137: I don’t get the technical stuff about relays and things 63 | |id: f3a2 , 11:31 PM Aug 5 64 | 65 | 137: different clients make sense to me. I can use different clients to 66 | access nostr but is just one giant soup like twitter 67 | |id: dfc5 , 11:32 PM Aug 5 68 | █ 69 | ────────────▀ 70 | * */ 71 | 72 | Event exampleEvent1 = Event.fromJson('["EVENT","latest",{"id":"167063f491c41b7b8f79bc74f318e8a8b0a802bf8364b8bb7d19c887d59ec5de","pubkey":"137d948a0eee45e6cd113faaad934fcf17a97de2236c655b70650d4252daa9d3","created_at":1659722388,"kind":1,"tags":[],"content":"nostr is not federated is it? this is like a global feed of all nostr freaks?","sig":"6db0b287015d9529dfbacef91561cb4e32afd6968edd8454867b8482bde01452e17b6f3de69bffcb2d9deba2a52d3c9ff82e04f7b18eb32428daf7eab5fd27c5"}]', ""); 73 | Event exampleEvent2 = Event.fromJson('["EVENT","latest",{"id":"f3a267ecbb631012da618de620bc1fe265f6429f412359bf02330b437cf88e67","pubkey":"137d948a0eee45e6cd113faaad934fcf17a97de2236c655b70650d4252daa9d3","created_at":1659722463,"kind":1,"tags":[["e","167063f491c41b7b8f79bc74f318e8a8b0a802bf8364b8bb7d19c887d59ec5de"]],"content":"I don’t get the technical stuff about relays and things","sig":"9f68031687214a24862226f291e3baadd956dc14ba9c5c552f8c881a40aacd34feda667ef4e4b09711cd43950eec2d272d5b11bd7636de5f457f38f31eaff398"}]', ""); 74 | Event exampleEvent3 = Event.fromJson('["EVENT","latest",{"id":"dfc5765da281c0ad99cb8693fc98c87f0f86ad56042a414f06f19d41c1315fc3","pubkey":"137d948a0eee45e6cd113faaad934fcf17a97de2236c655b70650d4252daa9d3","created_at":1659722537,"kind":1,"tags":[["e","167063f491c41b7b8f79bc74f318e8a8b0a802bf8364b8bb7d19c887d59ec5de"],["e","f3a267ecbb631012da618de620bc1fe265f6429f412359bf02330b437cf88e67"]],"content":"different clients make sense to me. I can use different clients to access nostr but is just one giant soup like twitter","sig":"d4fdc288e3cb95fc5ab46177fc0982d2aaa3b028eef6649f8200500da9c2e9a16c7a0462638afef7635bfea3094ec10901de759a48e362b60cb08f7e6585e02f"}]', ""); 75 | 76 | Set listEvents = { exampleEvent3, exampleEvent2, exampleEvent1}; 77 | 78 | Store node = Store.fromEvents(listEvents); 79 | expect(node.topPosts.length, 1); 80 | expect ( node.topPosts[0].children.length, 1); 81 | expect ( node.topPosts[0].children[0].children.length, 1); 82 | 83 | //node.printTree(0, DateTime.now().subtract(Duration(days:1000)), selectorTrees_all); // will test for ~1000 days 84 | }); 85 | 86 | test('make_paragraph', () { 87 | gTextWidth = 120; 88 | //print(gNumLeftMarginSpaces); 89 | //print(gTextWidth); 90 | 91 | String paragraph = """ 92 | 1 Testing paragraph with multiple lines. Testing paragraph with multiple lines. Testing paragraph with multiple lines. Testing paragraph with multiple lines. 93 | 2 Testing paragraph with multiple lines. Testing paragraph with multiple lines. Testing paragraph with multiple lines. 94 | 3 Testing paragraph with multiple lines. 95 | 96 | 5 Testing paragraph with multiple lines. Testing paragraph with multiple lines. Testing paragraph with multiple lines. 97 | 6 Testing paragraph with multiple lines. 98 | 7 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 89 words 99 | 8 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 90 words 100 | 9 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 91 words 101 | 10 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 92 words 102 | 103 | 104 | 11 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 89 words 105 | 106 | 107 | 108 | 12 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 90 words 109 | 110 | 111 | 112 | 13 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 91 words 113 | 114 | 115 | 116 | 14 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 92 words 117 | 118 | 119 | 120 | 121 | a"""; 122 | 123 | 124 | String expectedResult = 125 | """ 126 | 1 Testing paragraph with multiple lines. Testing paragraph with multiple lines. Testing 127 | paragraph with multiple lines. Testing paragraph with multiple lines. 128 | 2 Testing paragraph with multiple lines. Testing paragraph with multiple lines. Testing 129 | paragraph with multiple lines. 130 | 3 Testing paragraph with multiple lines. 131 | 132 | 5 Testing paragraph with multiple lines. Testing paragraph with multiple lines. Testing 133 | paragraph with multiple lines. 134 | 6 Testing paragraph with multiple lines. 135 | 7 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 89 words 136 | 8 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 90 137 | words 138 | 9 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 91 139 | words 140 | 10 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 92 141 | words 142 | 143 | 144 | 11 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 89 words 145 | 146 | 147 | 148 | 12 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 90 149 | words 150 | 151 | 152 | 153 | 13 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 91 154 | words 155 | 156 | 157 | 158 | 14 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 92 159 | words 160 | 161 | 162 | 163 | 164 | a"""; 165 | 166 | String res = makeParagraphAtDepth(paragraph, 30); 167 | expect( res, expectedResult); 168 | }); 169 | 170 | test('break_line ', () { 171 | gTextWidth = 120; 172 | 173 | String paragraph = """ 174 | 1 Testing paragraph with breaks in lines. Testing paragraph with multiple lines. Testing paragraph with multiple lines. Testing paragraph with multiple lines. 175 | 8 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 90 words 176 | 9 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 91 words 177 | 10 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 92 words"""; 178 | 179 | 180 | String expectedResult = 181 | """1 Testing paragraph with breaks in lines. Testing paragraph with multiple lines. Testing 182 | paragraph with multiple lines. Testing paragraph with multiple lines. 183 | 8 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 90 184 | words 185 | 9 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 91 186 | words 187 | 10 Testing paragraph with multiple lines. Testing paragraph with multiple lines. 92 188 | words"""; 189 | 190 | String res = makeParagraphAtDepth(paragraph, 30); 191 | expect( res, expectedResult); 192 | }); 193 | 194 | 195 | test('url_break1 ', () { 196 | gTextWidth = 92; 197 | 198 | //print("\n\nbreak_url_dash test"); 199 | 200 | String paragraph = """ 201 | https://github.com/vishalxl/nostr_console/releases/tag/v0.0.7-beta"""; 202 | 203 | 204 | String expectedResult = 205 | """https://github.com/vishalxl/nostr_console/releases/tag/v0.0.7-beta"""; 206 | 207 | String res = makeParagraphAtDepth(paragraph, 30); 208 | //print(res); 209 | expect( res, expectedResult); 210 | }); 211 | 212 | 213 | test('url_break2 ', () { 214 | gTextWidth = 92; 215 | 216 | //print("123456789|123456789|123456789|123456789|123456789|123456789|123456789|123456789|123456789|"); 217 | List urls = ["https://news.bitcoin.com/former-us-treasury-secretary-larry-summers-compares-ftx-collapse-to-enron-fraud/", 218 | "https://chromium.googlesource.com/chromium/src/net/+/259a070267d5966ba5ce4bbeb0a9c17b854f8000", 219 | " https://i.imgflip.com/71o242.jpg", 220 | " https://twitter.com/diegokolling/status/1594706072622845955?t=LB5Pn51bhj3BhIoke26kGQ&s=19", 221 | "11 https://github.com/nostr-protocol/nips/blob/master/16.md#ephemeral-events", 222 | "https://res.cloudinary.com/eskema/image/upload/v1669030722/306072883_413474904244526_502927779121754777_n.jpg_l6je2d.jpg"]; 223 | 224 | for (var url in urls) { 225 | String res = makeParagraphAtDepth(url, 30); 226 | //print(url); print(res);print(""); 227 | expect( res, url); 228 | } 229 | }); 230 | 231 | 232 | test('event_file_read', () async { 233 | Set initialEvents = {}; // collect all events here and then create tree out of them 234 | 235 | 236 | String inputFilename = 'test_event_file.csv'; 237 | initialEvents = readEventsFromFile(inputFilename); 238 | 239 | int numFilePosts = 0; 240 | // count events 241 | for (var element in initialEvents) { element.eventData.kind == 1? numFilePosts++: numFilePosts;} 242 | //print("read $numFilePosts posts from file $gEventsFilename"); 243 | expect(numFilePosts, 3486, reason:'Verify right number of kind 1 posts'); 244 | 245 | Store node = getTree(initialEvents); 246 | 247 | expect(0, node.getNumDirectRooms(), reason:'verify correct number of direct chat rooms created'); 248 | 249 | int numKind4xChannels = 0; 250 | for (var channel in node.channels) { 251 | channel.roomType == enumRoomType.kind40? numKind4xChannels++:1; 252 | } 253 | 254 | int numTTagChannels = 0; 255 | for (var channel in node.channels) { 256 | channel.roomType == enumRoomType.RoomTTag? numTTagChannels++:1; 257 | } 258 | 259 | int numLocationTagChannels = 0; 260 | for (var channel in node.channels) { 261 | channel.roomType == enumRoomType.RoomLocationTag? numLocationTagChannels++:1; 262 | } 263 | 264 | expect(78, numKind4xChannels, reason: 'verify correct number of public channels created of kind 4x'); 265 | expect(41, numTTagChannels, reason: 'verify correct number of public channels created of T tag type'); 266 | expect(2, numLocationTagChannels, reason: 'verify correct number of public channels created of Location tag'); 267 | 268 | expect(3046, node.getNumMessagesInChannel('25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb'), 269 | reason:'verify a public channel has correct number of messages'); 270 | //node.printStoreTrees(0, DateTime.now().subtract(Duration(days: 105)), (a) => true); 28 dec 2022 271 | 272 | }); 273 | 274 | test('utils_fns', () async { 275 | 276 | String content1 = '#bitcoin #chatgpt #u-s-a #u_s_a #1947 #1800'; 277 | Set? tags = getTagsFromContent(content1); 278 | //print(tags); 279 | expect(tags?.length, 6); 280 | expect(tags?.contains("bitcoin"), true); 281 | 282 | String pubkeyQrCodeResult1 = 283 | """ █▀▀▀▀▀█ ██▄▄▀ ▄▄ █ ▄▀ █▀▀▀▀▀█ 284 | █ ███ █ █▄█ ██▄ ▄▄ ██▀▀ █ ███ █ 285 | █ ▀▀▀ █ ▀ ▀ █▀▄█▄███ ██ █ ▀▀▀ █ 286 | ▀▀▀▀▀▀▀ ▀ ▀▄█ █▄▀ █ █▄█▄▀ ▀▀▀▀▀▀▀ 287 | █ ▀██▄▀▄▀▄▀ █ ▀ ▀▄█▀██ ▀▄▀▄▄▄█▀▀█ 288 | ▄▄█ ▀▄ ▄▄█ ▀█▀█▀▄ ▄▀▀▄▄▄▀▀▀█▀ 289 | ▄█▄ █▀ █▄ ▀▀▄█▀▀███ ▀▀▄ ▀ ▄▄▄ 290 | ▀ ██▀▀▀ ▀ ▄▄▄ █▀█▄▀ ▄██ ▀▀██▀▀ 291 | ▀█▄▄▀█▄ ▄▀▄▀ ▀ ▄▄▄ █ █▄▄▀▀▀███ 292 | ▄ █▀█▄▀▄▄▄ ▄▀█▄█▀ ▀ ██▀█▀█▄▀█ ▀▄█ 293 | ▄▀▀ ▀▀ █▄▄ ▀▀▄▄▄ ▄▀█▄▄▀ ▄▄ ▄ ▄ 294 | ▄▄▄▀ ▀ ▄█▀█ ▀ ██ █▀█▄ █ ▄▀██ ▀ 295 | ▀ ▀▀▀█▀ ▄▄ █ ▀▀ ▀▀▀ █▀▀▀█ █▄ 296 | █▀▀▀▀▀█ ▀█▀▄▄▄▀█▀ ▀▀▀█ ▀ █▄██▄ 297 | █ ███ █ █▄██▀▄▀ ▀▀▀▄▄ ▄▄▀█▀██▄ ██ 298 | █ ▀▀▀ █ ▀█▀▄ ▄█▀███ ▀ ▄ ▀▀▀▄█ ▀ 299 | ▀▀▀▀▀▀▀ ▀▀▀ ▀▀ ▀ ▀▀ ▀ ▀ \n"""; 300 | 301 | String profilePubkey1 = "add06b88bd78c5cbb2cd990e873adfba3eaf8e0217d3208be2be770eb506d430"; 302 | expect (pubkeyQrCodeResult1, getPubkeyAsQrString(profilePubkey1), reason: "testing qr code function"); 303 | 304 | String lnQrCodeResult1 = """:-\n\n█▀▀▀▀▀█ █▀▄█▄▄█▀ █ ▄▄ ▀▄ ▀ ▀█▀ ▀▄▀ ██ █▀ ▄█▀███ █▀ ▀▄ █▀▀▀▀▀█ 305 | █ ███ █ ▄ ▀ ▄▀█▄▄▄▀▀ ▀▀▄▄██▄▄██▄▄█▄▄ ▄▀▄▀ ▀ █████▄▀ █ ███ █ 306 | █ ▀▀▀ █ ▀▀█▄█▄▄▀▀▀█▀ ▀ █▄█▄██▀▀▀█ ▄▀▀ ███ █▄▄ ▄▀▀▄█▄ ▀ █ ▀▀▀ █ 307 | ▀▀▀▀▀▀▀ █ ▀▄█▄▀ ▀ █ █ █ ▀ ▀ ▀▄█ ▀ █ ▀ ▀ █ ▀▄▀ ▀ ▀ █▄█▄▀ ▀ ▀▀▀▀▀▀▀ 308 | ▄█▀ ██▀█▄▄ █ ▄██▀▀█▄▀▀ ▄ ▄ ██ █▀█▀█▀ █ ▀▀██▄▄█ █▄▀██▀██ ▀█ ▀▄█▀ 309 | █▄ ▀██▀ █▀ ▄▄▀▄█▀█▀▀ ▀██▄ █▄█ ▄█▄██ ▀▄█▀ ▀█▀▀▀ █ ▄█ ▄▄█▀█▀▄▄▄▄█ 310 | █ ▄ ▄▀ ▄ ▄▀▀█▄▄▀█▀ █▀▀ █▀██▀▀▄▄▄▄▄▄▄▀▀██▄█▀▀█▀█▄██ █ ▄ █ █▀ █▄▄ 311 | █▄ ▀▀▀▀██▀▄█ ▄██▀█ █▄█▄▄▄▀ ▀▀▄▀ ▀▀███ ▄█▄▄▄▄▄ ▄ ▄▄ ▀▀▀ ▀▄ 312 | ▀ ▄ ▄▀██▀▄▀ ▀█ ▀█ ▄▄▄█▀ ▄▀▄ ▄▄ ▄█ ▄▀▄▀▀▀▄▄ ▄▀▄▀▀▄▀▀▄ ▄█ ▄██ 313 | ▄█▄▀█▄▀ ██▀ █▀█▄▀█▀▄█▄▀▀███▀█▀▀█▀▀▄█▄▄▀▀█▀▀███▀▄███ ▀ ▀▀▀▀▀ ▀█▀▀█ 314 | ▀ ▀▀▀▀ █ █▀█ ▄▄▄█▀ ▀ █ ▄▀ ▀█▄█▄▄▄█ ▄▀▄█▄ █▀▄▄▀ ▀ ▄█▄▀▄▀▀█▀▄ 315 | ▄ ▄█▄▄▀▀▀▀ █▀▄▀▀ █▀▀▀▄ ▄▀ █▄▄▀█▄▀▀▀▀ ▀▀▀▄██ ▀█▄▀█▄█ ▄▀▄▀█▀ █ 316 | █▀▄▀▀ ▀▄▄ ▄▄█▄▀▄▄ ▄ ▄▄▄▀ ▄ ▄ █ ▄ ▀▀ █ ▄ █ ▄▄▀ ▄▀ ▄ █▄ ▄█▄ 317 | ▄ ▀▀█▄▀▀██▀ ▀█ ▀ ▄▀▀▀█▀ ▄█▄▀▀▄████▀▄██▄ ▄██▀█▀▀ █ ▀▄ ▀▄▀▀▀ ▄▀ 318 | █▄██ █▀▀ ▄▄▄ █▀ █▄▄▀ ▄█▄▄▀ ▄█ ██▄ ▄▀▄█▀▄█ █▄▄▄▄ ▄█▀▀▀ █ ▄▄ ▀▄▀▄▀█ 319 | ▀▀▄▀█▀▀▀█ ▄▄▄▄▄▄ █▀▄▄ ▀ ▄ ▄▄█▀▀▀█▀▄█▀█▄ ▄▄▄ ▄█▄█▀▀ █▀▀▀██▀▀█ 320 | ██▀ █ ▀ █ ▄█ ▄██ ▄▀ ▀▀ █ ██ █ ▀ █▀ █▄ ██▄█ ▄▀ ▄▄▄▄▀▄▀█ ▀ ██▄ ▀ 321 | █▄▀▀█▀▀█▀ ▀ ▄▀ █▄ ▀▄█▀ ▄▄ ▀▀▀▀▀██ █▀▀██ ▀ ▀█▀█▀ ██ ▀█▀▀█ ▄ ▀ 322 | ▀▄▄ ▀█▀█▀ ▀██▄▄█▀▀███▄▀█▀▄██▀▀█▀▀▀▄█▀▄ █▀▀█▀█▀▀███▀▀▀▀▀▀█▀▄███ ▀█ 323 | ▀█▄▀█▄▀ █ ▄▀▀▄▀▄▄█ ▄ █ █ ▀█ ▄▄▄ ▀▄ ▀ ▀█▄ ▄▄▄ ▀ █▀ ▀▄▄▄ █▄█▀ 324 | █ ▄▄▄ ▀▄▀█▄▀█▀▀█ █ ▄█▄▄▀▀▀▀█▄▄█ ▀ █▀▀▀██ ▄█▄▀▀▄██▀▄█ ▄▀▄▀█▄▀█ ▀█ 325 | ██▄▀▀█▀▄ ▄ █ ▀█ ▄█▄ ▄██ █ ▄ ▄ ▄▄ ▀█ ▄█ ▀▀ ██▄ ▄█▄ ▀▀ 326 | █▄█▄██▀▀ ▀▀▄█▀ █▀▀██ ▄█▄▀█▄██▀▄█▄▀█▄▀ ▀█▀██▄ █▄▀▄▄▀▀▀██▄ ▀▄█ ██▀ 327 | █▄▀▄ ▀▀▀ ▄▀▄ ▀ ▀ ▄ ▄▀ ▀██▀▄ █ ▄ █▄▀██▀▄ ▄▄ █▀ ▀▄ █▄▀▄▄ █▀ █ ██ █ 328 | ▄ ▄ █▀█▀▀▄█▀▄▀▀▀███▀ █▀█▄ █▀ ███▀▀▀ ██████▀▀█▀██▀▀█████▄▀▀▀████ 329 | ▀▀▄▀▀▄▀ █▄ ▄█ ▀▄ ▄ ▄▀ ▀ ██▄ █▄ ▄▄▄█▀▄▀ ▄▄▄▄▀█▄█ ▀█ ▀ ██▄ 330 | ▀█▄▄▄█▀▄█ ▀▀█▀█▀▄▀█ █▀▄█▀▀ ▀▄█▀▄█ █▀█ ▀▀ ▄ ▀ ▀█▀▀█ ▀▄ ▀▀▄██▀█▀▄ 331 | ▄ ▀█ ▀▀ ▀ ▀▄ ▄▀ ▀ █▄ ▀ ▀█▄ ▄ █▄▄▄ ▄ █ █▀▄ ▀▄▄▄▄ 332 | ▀▀ ▀ ▀▀▀ ██▄ █▀ ▀▀ █ ▀▄ ██ ██▀▀▀█ ▀ █▄█▀▀▀▀▄ ▀ ▀ ▄▄ █▀▀▀██ 333 | █▀▀▀▀▀█ █ ▀ ██▄▄▄ ▀▄▄██▄█▄ █ ▀ ██▄ ▀▀ █▀▄█▀ ██ ██▀▄█▄█ ▀ █ █ ▀ 334 | █ ███ █ ▀▄█▀█▄▀█▀ █▀▀ ▀▀ ▄▀██▀▀███ █ ▄ ▀▄▄▄█▄▄ ▄█ ▄█▀█▀▀▀▀█▀▀ 335 | █ ▀▀▀ █ █▀██▄▄▄▄▄ ▄█ ▄█ ▄ ██ █▄▄█ █▄▄▄▄▄█ █▀ █▄ ▀ ▀▄▄█ ▄▀▄▄ 336 | ▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀▀ ▀ ▀ ▀ ▀▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀▀ 337 | \n\n"""; 338 | 339 | String lnInvoice1 = "lnbc30n1p3689h4sp54ft7dn46clu4h8lyey2zj2hfvp07e2ekcrmceeq4gxmw9ml2pwuspp5zfup7rmneu47f34qznatwcmkexdkl78ppntms9y8vgj75cyzvh5qdq2f38xy6t5wvxqyjw5qcqpjrzjqvhxqvs0ulx0mf5gp6x2vw047capck4pxqnsjv0gg8a4zaegej6gxzadnsqqj3cqqqqqqqqqqqqqqqqqyg9qyysgqv5cg4cly6sr2q4n0vkfcgmgxd5egdrztt8pn4003thqzr8sn5e8swdxw4g75jr233hyr2p655xgwh98jh3pkn3kranjkg0ysrwze44qpqmeq35"; 340 | expect (expandLNInvoices(lnInvoice1),lnQrCodeResult1, reason: "testing ln qr code function"); 341 | 342 | }); 343 | 344 | 345 | 346 | return ; 347 | 348 | } // end main 349 | 350 | --------------------------------------------------------------------------------