add fetch-events

This commit is contained in:
@s.roertgen 2024-11-20 16:06:12 +01:00
commit 0cc04aa611
14 changed files with 883 additions and 0 deletions

29
.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
.calva/output-window/
.calva/repl.calva-repl
.classpath
.clj-kondo/.cache
.cpcache
.eastwood
.factorypath
.hg/
.hgignore
.java-version
.lein-*
.lsp/.cache
.lsp/sqlite.db
.nrepl-history
.nrepl-port
.portal/vs-code.edn
.project
.rebel_readline_history
.settings
.socket-repl-port
.sw*
.vscode
*.class
*.jar
*.swp
*~
/checkouts
/classes
/target

24
CHANGELOG.md Normal file
View file

@ -0,0 +1,24 @@
# Change Log
All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).
## [Unreleased]
### Changed
- Add a new arity to `make-widget-async` to provide a different widget shape.
## [0.1.1] - 2024-11-15
### Changed
- Documentation on how to make the widgets.
### Removed
- `make-widget-sync` - we're all async, all the time.
### Fixed
- Fixed widget maker to keep working when daylight savings switches over.
## 0.1.0 - 2024-11-15
### Added
- Files from the new template.
- Widget maker public API - `make-widget-sync`.
[Unreleased]: https://github.com/nostr-clj/nostr-clj/compare/0.1.1...HEAD
[0.1.1]: https://github.com/nostr-clj/nostr-clj/compare/0.1.0...0.1.1

214
LICENSE Normal file
View file

@ -0,0 +1,214 @@
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from
a Contributor if it was added to the Program by such Contributor itself or
anyone acting on such Contributor's behalf. Contributions do not include
additions to the Program which: (i) are separate modules of software
distributed in conjunction with the Program under their own license
agreement, and (ii) are not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility
to acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are offered
by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained within
the Program.
Each Contributor must identify itself as the originator of its Contribution,
if any, in a manner that reasonably allows subsequent Recipients to identify
the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a
manner which does not create potential liability for other Contributors.
Therefore, if a Contributor includes the Program in a commercial product
offering, such Contributor ("Commercial Contributor") hereby agrees to defend
and indemnify every other Contributor ("Indemnified Contributor") against any
losses, damages and costs (collectively "Losses") arising from claims,
lawsuits and other legal actions brought by a third party against the
Indemnified Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program in
a commercial product offering. The obligations in this section do not apply
to any claims or Losses relating to any actual or alleged intellectual
property infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim, and
b) allow the Commercial Contributor to control, and cooperate with the
Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim
at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If
that Commercial Contributor then makes performance claims, or offers
warranties related to Product X, those performance claims and warranties are
such Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a
court requires any other Contributor to pay any damages as a result, the
Commercial Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all risks
associated with its exercise of rights under this Agreement , including but
not limited to the risks and costs of program errors, compliance with
applicable laws, damage to or loss of data, programs or equipment, and
unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of the
remainder of the terms of this Agreement, and without further action by the
parties hereto, such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted
under Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and
does not cure such failure in a reasonable period of time after becoming
aware of such noncompliance. If all Recipient's rights under this Agreement
terminate, Recipient agrees to cease use and distribution of the Program as
soon as reasonably practicable. However, Recipient's obligations under this
Agreement and any licenses granted by Recipient relating to the Program shall
continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to
time. No one other than the Agreement Steward has the right to modify this
Agreement. The Eclipse Foundation is the initial Agreement Steward. The
Eclipse Foundation may assign the responsibility to serve as the Agreement
Steward to a suitable separate entity. Each new version of the Agreement will
be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly
stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
licenses to the intellectual property of any Contributor under this
Agreement, whether expressly, by implication, estoppel or otherwise. All
rights in the Program not expressly granted under this Agreement are
reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.

63
README.md Normal file
View file

@ -0,0 +1,63 @@
# nostr-clj
Do nostr stuff with Clojure.
Heavy WIP.
## Usage
FIXME: write usage documentation!
### Connection
- `url`: a string
- `on-message-handler`: a function
- `on-open-handler`: a function
```clojure
(ns.nostr.core)
(connect { :uri "wss://relay.org"
:on-message-handler (fn [res] (println (nth res 2)))})
```
Invoke a library API function from the command-line:
$ clojure -X nostr-clj.nostr-clj/foo :a 1 :b '"two"'
{:a 1, :b "two"} "Hello, World!"
Run the project's tests (they'll fail until you edit them):
$ clojure -T:build test
Run the project's CI pipeline and build a JAR (this will fail until you edit the tests to pass):
$ clojure -T:build ci
This will produce an updated `pom.xml` file with synchronized dependencies inside the `META-INF`
directory inside `target/classes` and the JAR in `target`. You can update the version (and SCM tag)
information in generated `pom.xml` by updating `build.clj`.
Install it locally (requires the `ci` task be run first):
$ clojure -T:build install
Deploy it to Clojars -- needs `CLOJARS_USERNAME` and `CLOJARS_PASSWORD` environment
variables (requires the `ci` task be run first):
$ clojure -T:build deploy
Your library will be deployed to net.clojars.nostr-clj/nostr-clj on clojars.org by default.
## License
Copyright © 2024 Laoc
_EPLv1.0 is just the default for projects generated by `deps-new`: you are not_
_required to open source this project, nor are you required to use EPLv1.0!_
_Feel free to remove or change the `LICENSE` file and remove or update this_
_section of the `README.md` file!_
Distributed under the Eclipse Public License version 1.0.

69
build.clj Normal file
View file

@ -0,0 +1,69 @@
(ns build
(:refer-clojure :exclude [test])
(:require [clojure.tools.build.api :as b]
[deps-deploy.deps-deploy :as dd]))
(def lib 'net.clojars.laoc/nostr)
(def version "0.0.1-SNAPSHOT")
#_ ; alternatively, use MAJOR.MINOR.COMMITS:
(def version (format "1.0.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(defn test "Run all the tests." [opts]
(let [basis (b/create-basis {:aliases [:test]})
cmds (b/java-command
{:basis basis
:main 'clojure.main
:main-args ["-m" "cognitect.test-runner"]})
{:keys [exit]} (b/process cmds)]
(when-not (zero? exit) (throw (ex-info "Tests failed" {}))))
opts)
(defn- pom-template [version]
[[:description "FIXME: my new library."]
[:url "https://github.com/sroertgen/nostr-clj"]
[:licenses
[:license
[:name "Eclipse Public License"]
[:url "http://www.eclipse.org/legal/epl-v10.html"]]]
[:developers
[:developer
[:name "Laoc"]]]
[:scm
[:url "https://github.com/sroertgen/nostr-clj"]
[:connection "scm:git:https://github.com/sroertgen/nostr-clj.git"]
[:developerConnection "scm:git:ssh:git@github.com:sroertgen/nostr-clj.git"]
[:tag (str "v" version)]]])
(defn- jar-opts [opts]
(assoc opts
:lib lib :version version
:jar-file (format "target/%s-%s.jar" lib version)
:basis (b/create-basis {})
:class-dir class-dir
:target "target"
:src-dirs ["src"]
:pom-data (pom-template version)))
(defn ci "Run the CI pipeline of tests (and build the JAR)." [opts]
(test opts)
(b/delete {:path "target"})
(let [opts (jar-opts opts)]
(println "\nWriting pom.xml...")
(b/write-pom opts)
(println "\nCopying source...")
(b/copy-dir {:src-dirs ["resources" "src"] :target-dir class-dir})
(println "\nBuilding JAR..." (:jar-file opts))
(b/jar opts))
opts)
(defn install "Install the JAR locally." [opts]
(let [opts (jar-opts opts)]
(b/install opts))
opts)
(defn deploy "Deploy the JAR to Clojars." [opts]
(let [{:keys [jar-file] :as opts} (jar-opts opts)]
(dd/deploy {:installer :remote :artifact (b/resolve-path jar-file)
:pom-file (b/pom-path (select-keys opts [:lib :class-dir]))}))
opts)

20
deps.edn Normal file
View file

@ -0,0 +1,20 @@
{:paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
cheshire/cheshire {:mvn/version "5.13.0"}
hato/hato {:mvn/version "1.0.0"}}
:aliases
{:nREPL
{:extra-deps
{nrepl/nrepl {:mvn/version "1.3.0"}}
:main-opts ["-m" "nrepl.cmdline"
"-b" "0.0.0.0"
"-p" "12345"]}
:test
{:extra-paths ["test"]
:extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}}
:build {:deps {io.github.clojure/tools.build
{:mvn/version "0.10.5"}
slipset/deps-deploy {:mvn/version "0.2.2"}}
:ns-default build}}}

3
doc/intro.md Normal file
View file

@ -0,0 +1,3 @@
# Introduction to nostr-clj/nostr-clj
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)

0
resources/.keep Normal file
View file

82
src/nostr/core.clj Normal file
View file

@ -0,0 +1,82 @@
(ns nostr.core
(:require [hato.websocket :as ws]
[cheshire.core :as json])
(:import [java.nio CharBuffer]))
(defn connect
"Establishes a websocket connection and handles events using user-defined handlers.
Args:
- `uri`: The websocket URI to connect to.
- `handlers`: A map containing the following keys:
- `:on-open-handler`: (fn [ws] ...) called when the connection opens.
- `:on-message-handler`: (fn [ws parsed-msg] ...) called for each message.
- `:on-close-handler`: (fn [ws status reason] ...) called when the connection closes.
Returns:
- The websocket instance with your handlers."
[uri {:keys [on-message-handler on-open-handler on-close-handler]}]
(let [ws @(ws/websocket uri
{:on-message (fn [ws msg last?]
(let [msg-str (if (instance? CharBuffer msg)
(str msg)
msg)
parsed (json/parse-string msg-str true)]
(on-message-handler ws parsed)))
:on-open (fn [ws]
(on-open-handler ws))
:on-close (fn [ws status reason]
(on-close-handler ws status reason))})]
ws))
(defn subscribe [ws msg]
(ws/send! ws (json/generate-string msg)))
(defn send! [ws msg]
(ws/send! ws (json/generate-string msg)))
(defn close! [ws]
(ws/close! ws))
(defn fetch-events
"Args:
- `uri`: URI of relay to connect to
- `filter`: Filter for query.
Returns:
A vector of found events.
Example:
`(fetch-events 'wss://my-relay.org' {:kinds [30142]})`
"
[uri filter]
(let [resources (atom [])
result-promise (promise)]
(connect uri
{:on-message-handler (fn [ws msg]
(case (first msg)
"EVENT" (let [event (nth msg 2 nil)]
(swap! resources conj event)) ;; Add to resources
"EOSE" (do
(deliver result-promise @resources) ;; Deliver resources to the promise
(close! ws))))
:on-open-handler (fn [ws]
(send! ws ["REQ" (random-uuid) filter]))})
@result-promise))
(comment
(let [ws (connect "ws://localhost:7778"
{:on-message-handler (fn [ws msg]
(println "Received:" msg)
msg)
:on-open-handler (fn [ws]
(println "WS:" ws))})]
(subscribe ws {:kinds [30142]
:limit 2})
(subscribe ws {:kinds [1]
:limit 2})))
(comment
(+ 1 1 1 1)
(str "Hello " "Basti"))

200
src/nostr/edufeed.clj Normal file
View file

@ -0,0 +1,200 @@
(ns nostr.edufeed
(:require [cheshire.core :as json]
[clojure.string :as str]
[nostr.core :as nostr]))
(defn get-key [coll key]
(or (get coll key) ""))
(defn transform-name [e]
["name" e])
(defn transform-description [e]
["description" e])
(defn transform-image [e]
["image" e])
(defn transform-creator [creator]
(map (fn [e]
["creator" (get-key e :id) (get-key e :name) (get-key e :type)])
creator))
(defn transform-contributor [contributor]
(map (fn [e]
["contributor" (get-key e :id) (get-key e :name) (get-key e :type)])
contributor))
(defn transform-publisher [publisher]
(map (fn [e]
["publisher" (get-key e :id) (get-key e :name) (get-key e :type)])
publisher))
(defn transform-keywords [keywords]
(into ["keywords"] keywords))
(defn transform-about [about]
(let [langs (keys (get about :prefLabel))]
(map (fn [k]
["about" (get about :id "") (get-in about [:prefLabel k]) (name k)])
langs)))
(defn transform-learningResourceType [learningResourceType]
(let [langs (keys (get learningResourceType :prefLabel))]
(map (fn [k]
["learningResourceType" (get learningResourceType :id "") (get-in learningResourceType [:prefLabel k]) (name k)])
langs)))
(defn transform-inLanguage [l]
(into ["inLanguage"] l))
(defn transform-isAccessibleForFree [a]
["isAccessibleForFree" (str a)])
(defn transform-type [t]
(into ["type"] t))
(defn transform-datePublished [d]
["datePublished" d])
(defn transform-dateCreated [d]
["dateCreated" d])
(defn transform-conditionsOfAccess [c]
(let [langs (keys (get c :prefLabel))]
(map (fn [k]
["conditionsOfAccess" (get-key c :id) (get-in c [:prefLabel k]) (name k)])
langs)))
(defn transform-license [l]
["license" (:id l)])
(defn transform-value [k v]
(case k
:name [(transform-name v)]
:description [(transform-description v)]
:image [(transform-image v)]
:creator (transform-creator v)
:keywords [(transform-keywords v)]
:about (mapcat transform-about v)
:inLanguage [(transform-inLanguage v)]
:isAccessibleForFree [(transform-isAccessibleForFree v)]
:type [(transform-type v)]
:datePublished [(transform-datePublished v)]
:dateCreated [(transform-dateCreated v)]
:conditionsOfAccess (transform-conditionsOfAccess v)
:license [(transform-license v)]
:contributor (transform-contributor v)
:publisher (transform-publisher v)
:learningResourceType (mapcat transform-learningResourceType v)
:id [["r" v] ["d" v] ["id" v]]
[]))
(defn transform-amb-to-30142-event [amb]
(let [tags (mapcat (fn [[k v]]
(transform-value k v))
amb)
raw-event {:kind 30142
:content ""
:tags tags}]
raw-event))
(comment
(let [jfile (slurp "resources/test-data.json")
decoded (json/parse-string jfile true)]
; (transform-amb-to-30142-event (select-keys decoded [:learningResourceType])))
(transform-amb-to-30142-event decoded)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Now the other way around ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn transform-skos-fields [data]
(->> data
(reduce (fn [acc [_, id label lang]]
(update acc id
(fn [m] (update m :prefLabel assoc (keyword lang) label))))
{})
(map (fn [[id {:keys [prefLabel]}]] {:id id, :prefLabel prefLabel}))
(into [])))
(defmulti extract-tag (fn [tags key] key))
(defmethod extract-tag :default [tags key]
(second (first (filter #(= key (first %)) tags))))
(defmethod extract-tag "creator" [tags _]
(map (fn [e]
(let [id (second e)
name (nth e 2)
type (get e 3 "Person")]
(cond-> {:name name}
(not (str/blank? id)) (assoc :id id)
(not (nil? type)) (assoc :type type))))
(filter #(= "creator" (first %)) tags)))
(defmethod extract-tag "type" [tags _]
(rest (first (filter #(= key (first %)) tags))))
(defn extract-tag-array [tags key]
(nthrest (first (filter #(= key (first %)) tags)) 1))
(defn convert-30142-to-nostr-amb
"Args:
- `event`
- `skip-raw-event`: boolean don't include the raw event info in result
- `amb-id`: boolean return the resource id instead of event id"
([event] (convert-30142-to-nostr-amb event true))
([event skip-raw-event] (convert-30142-to-nostr-amb event skip-raw-event false))
([event skip-raw-event amb-id]
(let [tags (:tags event)
amb-result {;; TODO context sollte auch bei der Konvertierung gespeichert
;; und hier entsprechend ausgegeben werden
(keyword "@context") ["https://w3id.org/kim/amb/context.jsonld",
"https://schema.org",
{(keyword "@language") "de"}]
:id (if amb-id (extract-tag tags "r") (:id event)) ;; TODO this needs to be amb id for "real amb"
:type (let [type (extract-tag-array tags "type") ]
(if (nil? (seq type))
["LearningResource"]
type))
:name (extract-tag tags "name")
:creator (extract-tag tags "creator")
:description (extract-tag tags "description")
:keywords (extract-tag-array tags "keywords")
:about (transform-skos-fields (filter #(= "about" (first %)) tags))}]
(if skip-raw-event
amb-result
(assoc amb-result
:event_raw event
:event_created_at (:created_at event)
:event_kind (:kind event)
:event_pubkey (:pubkey event)
:resource_id (extract-tag tags "r"))))))
(comment
(def test-event {:id "1234"
:kind 30142
:pubkey "12345"
:created_at 123456
:content ""
:tags [["r" "https://oersi.org/resources/aHR0cHM6Ly9hdi50aWIuZXUvbWVkaWEvNjY5ODM="]
["id" "https://oersi.org/resources/aHR0cHM6Ly9hdi50aWIuZXUvbWVkaWEvNjY5ODM="]
; ["type" "CreativeWork" "LearningResource"]
["name", "Wurzeln konstruieren: Die Schnecke des Pythagoras"],
["creator", "test-uri", "Christian Spannagel"],
["creator", "", "Maxi Muster"],
["image", "https://av.tib.eu/thumbnail/66983"]
["about", "https://w3id.org/kim/hochschulfaechersystematik/n37", "Mathe", "de"],
["about", "https://w3id.org/kim/hochschulfaechersystematik/n37", "Mathematics", "en"],
["about", "https://...", "Geometrie", "de"],
["resourceType", "https://...", "Video", "de"],
["inLanguage", "de"],
["keywords", "Pythagoras", "Geometrie"],
["license", "https://creativecommons.org/licenses/by/3.0", "cc-by"],
["source", "https://av.tib.eu/", "TIB AV-Portal"]]})
(convert-30142-to-nostr-amb test-event true true))

View file

@ -0,0 +1,17 @@
(ns nostr.elliptic-signature
(:import (schnorr Schnorr)))
(defn do-sign [message private-key aux-rand]
(Schnorr/sign message private-key aux-rand))
(defn do-verify [message public-key signature]
(try
(Schnorr/verify message public-key signature)
(catch Exception e
(println 1 'do-verify 'exception e)
false)))
(defn get-pub-key
"private-key is byte array. Returns byte array."
[private-key]
(Schnorr/genPubKey private-key))

View file

@ -0,0 +1,33 @@
(ns nostr.event-composer
(:require [nostr.events :as events]
[nostr.elliptic-signature :as ecc]
[nostr.util :as util]))
(defn body->event
"Copied from uncle bobs more speech client.
(https://github.com/unclebob/more-speech/)
Adds pubkey, created-at, id, and sig to the partially composed body,
which must include kind, tags, and content. The body is put into an
EVENT wrapper that is ready to send.
Private Key should be a hex-string"
([body private-key]
(let [private-key (util/hex-string->bytes private-key )
public-key (util/bytes->hex-string (ecc/get-pub-key private-key))
now (util/get-now)
body (assoc body :pubkey public-key
:created_at now)
[id body] (events/make-id-with-pow 12 body)
aux-rand (util/num->bytes 32 (biginteger (System/currentTimeMillis)))
signature (ecc/do-sign id private-key aux-rand)
event (assoc body :id (util/bytes->hex-string id)
:sig (util/bytes->hex-string signature))]
["EVENT" event])))
(comment
(let [private-key "9109ffc9fcddb1086e153ed23ff91c4f69f726178bf959f09b1f29c7569e24a1"
body {:content "hello"
:kind 30142
:tags [["r" "hello"]]}]
(body->event body private-key )
)
)

30
src/nostr/events.clj Normal file
View file

@ -0,0 +1,30 @@
(ns nostr.events
(:require [nostr.util :as util]
[cheshire.core :as json]
)
(:import (java.nio.charset StandardCharsets)))
(defn make-id
"copied from uncle bob's more speech.
returns byte array of id given the clojure form of the body"
[{:keys [pubkey created_at kind tags content]}]
(let [id-event (json/generate-string [0 pubkey created_at kind tags content])
id (util/sha-256 (.getBytes id-event StandardCharsets/UTF_8))]
id)
)
(defn- pow2 [n] (reduce * (repeat n 2N)))
(defn make-id-with-pow
"returns byte array and updated body of id given the clojure form of the body, and the
POW constraint given in the number of preceding binary zeroes."
[pow body]
(let [limit (pow2 (- 256 pow))]
(loop [nonce 0]
(let [body (update-in body [:tags] concat [[:nonce (str nonce) (str pow)]])
id (make-id body)
id-num (util/bytes->num id)]
(if (< id-num limit)
[id body]
(recur (inc nonce)))))))

99
src/nostr/util.clj Normal file
View file

@ -0,0 +1,99 @@
(ns nostr.util
(:import (java.awt Toolkit)
(java.awt.datatransfer StringSelection)
(java.security MessageDigest SecureRandom)))
;; most of this is copied from uncle bob more speech client
(defn num->bytes
"Returns the byte-array representation of n.
The array will have the specified length."
[length n]
(let [a (.toByteArray (biginteger n))
l (count a)
zeros (repeat (- length l) (byte 0))]
(if (> l length)
(byte-array (drop (- l length) (seq a)))
(byte-array (concat zeros a)))))
(defn bytes->num
"Returns a BigInteger from a byte-array."
^BigInteger [^bytes bytes]
(BigInteger. 1 bytes))
(defn bytes->hex-string
"Returns a string containing the hexadecimal
representation of the byte-array. This is the
inverse of hex-string->bytes."
[byte-array]
(let [byte-seq (for [i (range (alength byte-array))] (aget byte-array i))
byte-strings (map #(apply str (take-last 2 (format "%02x" %))) byte-seq)]
(apply str (apply concat byte-strings))))
(defn ^bytes hex-string->bytes
"returns a byte-array containing the bytes described
by the hex-string. This is the inverse of bytes->hex-string."
[hex-string]
(let [byte-strings (map #(apply str %) (partition 2 hex-string))
byte-vector (map #(Integer/parseInt % 16) byte-strings)]
(byte-array byte-vector))
)
(defn hex-string->num
"returns BigInteger from a hex string"
[hex-string]
(-> hex-string hex-string->bytes bytes->num))
(defn unhexify [hex-string]
(bigint (hex-string->num hex-string)))
(defn num32->hex-string [n]
"converts a number to a 32 byte hex-string"
(->> n (num->bytes 32) bytes->hex-string))
(defn hexify [n]
(num32->hex-string n))
(defn bytes=
"compares two byte arrays for equality."
[^bytes b1 ^bytes b2]
(assert (= (alength b1) (alength b2)) "bytes= args not same size.")
(loop [index 0]
(if (= index (alength b1))
true
(if (= (aget b1 index) (aget b2 index))
(recur (inc index))
false)))
)
(defn sha-256
"Returns the sha256 hash of the message.
Both the message and the hash are byte-arrays."
^bytes [^bytes message]
(let [digest (MessageDigest/getInstance "SHA-256")]
(.digest digest message)))
(defn xor-string [pw s]
(let [pwd (map int (cycle pw))
sc (map int s)
cipher-ints (map bit-xor pwd sc)]
(apply str (map char cipher-ints))))
(defn xor-bytes [^bytes a ^bytes b]
(assert (= (alength a) (alength b)) "byte-wise-xor: arguments not same size.")
(let [result (byte-array (alength a))]
(doseq [i (range (alength a))]
(aset result i (byte (bit-xor (aget a i) (aget b i)))))
result))
(defn make-private-key []
(let [gen (SecureRandom.)
key-bytes (byte-array 32)
_ (.nextBytes gen key-bytes)]
key-bytes))
(defn get-now-ms []
(System/currentTimeMillis))
(defn get-now []
(quot (get-now-ms) 1000))