Compare commits
No commits in common. "7fc8cd9dcdf1b480d462335cc6bd30b7d035abfa" and "57b4a13a7ce5934cf02096dcd1e3b3ba58a7f979" have entirely different histories.
7fc8cd9dcd
...
57b4a13a7c
9 changed files with 21 additions and 109 deletions
4
deps.edn
4
deps.edn
|
|
@ -15,9 +15,7 @@
|
||||||
;; json
|
;; json
|
||||||
cheshire/cheshire {:mvn/version "6.0.0"}
|
cheshire/cheshire {:mvn/version "6.0.0"}
|
||||||
;; metosin/muuntaja {:mvn/version "0.6.11"}
|
;; metosin/muuntaja {:mvn/version "0.6.11"}
|
||||||
org.clojure/clojure {:mvn/version "1.12.1"}
|
org.clojure/clojure {:mvn/version "1.12.1"}}
|
||||||
;; websockets
|
|
||||||
hato/hato {:mvn/version "1.0.0"}}
|
|
||||||
|
|
||||||
:aliases
|
:aliases
|
||||||
{;; Run with clj -T:build function-in-build
|
{;; Run with clj -T:build function-in-build
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
[clj-http.client :as http-client]
|
[clj-http.client :as http-client]
|
||||||
[micro-blog.pocket-base :as pb]
|
[micro-blog.pocket-base :as pb]
|
||||||
[micro-blog.utils :as utils]
|
[micro-blog.utils :as utils]
|
||||||
[cheshire.core :as json]
|
|
||||||
[micro-blog.is-tech]
|
[micro-blog.is-tech]
|
||||||
[taoensso.telemere :as tel]
|
[taoensso.telemere :as tel]
|
||||||
[micro-blog.config :refer [config]]))
|
[micro-blog.config :refer [config]]))
|
||||||
|
|
@ -83,11 +82,10 @@
|
||||||
(map #(vector (:fullsize %) (:alt %)) images)))
|
(map #(vector (:fullsize %) (:alt %)) images)))
|
||||||
|
|
||||||
(defn transform-post [post]
|
(defn transform-post [post]
|
||||||
(tel/log! {:level :info :data {:post post}} "Transforming post")
|
|
||||||
(hash-map :source :blue_sky
|
(hash-map :source :blue_sky
|
||||||
:fullPost post
|
:fullPost post
|
||||||
:remoteId (:cid post)
|
:remoteId (:cid post)
|
||||||
:isTech (micro-blog.is-tech/is-tech? (json/generate-string (:record post)))
|
:isTech (micro-blog.is-tech/is-tech? (:record post))
|
||||||
:authorId (get-in post [:author :handle])
|
:authorId (get-in post [:author :handle])
|
||||||
:tags (extract-tags post)
|
:tags (extract-tags post)
|
||||||
:images (extract-images post)
|
:images (extract-images post)
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,7 @@
|
||||||
:mastodon-host "MASTODON_BASE_URL"
|
:mastodon-host "MASTODON_BASE_URL"
|
||||||
:mastodon-account-id "MASTODON_ACCOUNT_ID"
|
:mastodon-account-id "MASTODON_ACCOUNT_ID"
|
||||||
:api-host "API_HOST"
|
:api-host "API_HOST"
|
||||||
:api-port "API_PORT"
|
:api-port "API_PORT"})
|
||||||
;; :nostr-fetcher-npub "NOSTR_FETCHER_NPUB"
|
|
||||||
:nostr-fetcher-id "NOSTR_FETCHER_PUB_HEX"
|
|
||||||
:nostr-id "NOSTR_ID"
|
|
||||||
:nostr-relay "NOSTR_RELAY"})
|
|
||||||
|
|
||||||
(defn- load-config []
|
(defn- load-config []
|
||||||
(merge (read-string (slurp "config.edn"))
|
(merge (read-string (slurp "config.edn"))
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
[clj-http.client :as client]))
|
[clj-http.client :as client]))
|
||||||
|
|
||||||
(defn is-tech? [post-text]
|
(defn is-tech? [post-text]
|
||||||
(when (not (string? post-text)) (throw (ex-info "Post text must be a string" {:post-text post-text})))
|
|
||||||
(let [url (str (:mistral-host @config) "/v1/conversations")
|
(let [url (str (:mistral-host @config) "/v1/conversations")
|
||||||
headers {"Content-Type" "application/json"
|
headers {"Content-Type" "application/json"
|
||||||
"Accept" "application/json"
|
"Accept" "application/json"
|
||||||
|
|
@ -13,7 +12,7 @@
|
||||||
body {:inputs post-text
|
body {:inputs post-text
|
||||||
:stream false
|
:stream false
|
||||||
:agent_id (@config :mistral-agent-id)}]
|
:agent_id (@config :mistral-agent-id)}]
|
||||||
(tel/log! {:level :info :data {:url url :agent_id (:agent_id body) :post-text post-text}} "making request to mistral agent")
|
(tel/log! {:level :info :data {:url url :agent_id (:agent_id body)}} "making request to mistral agent")
|
||||||
(->
|
(->
|
||||||
(client/post url {:headers headers
|
(client/post url {:headers headers
|
||||||
:form-params body
|
:form-params body
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
[taoensso.telemere :as tel]
|
[taoensso.telemere :as tel]
|
||||||
[micro-blog.logging.main :as logging]
|
[micro-blog.logging.main :as logging]
|
||||||
micro-blog.api
|
micro-blog.api
|
||||||
[micro-blog.nostr :as nostr]
|
|
||||||
[micro-blog.blue-sky :as blue-sky]
|
[micro-blog.blue-sky :as blue-sky]
|
||||||
[micro-blog.mastodon :as masto]))
|
[micro-blog.mastodon :as masto]))
|
||||||
|
|
||||||
|
|
@ -16,8 +15,6 @@
|
||||||
(logging/setup-logging)
|
(logging/setup-logging)
|
||||||
(tel/log! :info "Setting up API")
|
(tel/log! :info "Setting up API")
|
||||||
(micro-blog.api/start)
|
(micro-blog.api/start)
|
||||||
(tel/log! :info "Setting up nostr scraper")
|
|
||||||
(nostr/start)
|
|
||||||
(tel/log! :info "Setting up crons")
|
(tel/log! :info "Setting up crons")
|
||||||
(doseq [[i cron] (map-indexed vector crons)]
|
(doseq [[i cron] (map-indexed vector crons)]
|
||||||
(let [start (.plus (Instant/now) (Duration/ofMinutes (* i 5)))]
|
(let [start (.plus (Instant/now) (Duration/ofMinutes (* i 5)))]
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
[:media_attachments [:vector [:map
|
[:media_attachments [:vector [:map
|
||||||
[:url :string]
|
[:url :string]
|
||||||
[:type [:= "image"]]
|
[:type [:= "image"]]
|
||||||
[:description [:maybe :string]]]]]]])
|
[:description :string]]]]]])
|
||||||
|
|
||||||
(defn get-posts-until-id [id]
|
(defn get-posts-until-id [id]
|
||||||
(let [limit 10
|
(let [limit 10
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
:remoteId (:id raw-post)
|
:remoteId (:id raw-post)
|
||||||
:authorId (get-in raw-post [:account :id])
|
:authorId (get-in raw-post [:account :id])
|
||||||
:tags (map :name (:tags raw-post))
|
:tags (map :name (:tags raw-post))
|
||||||
:images (map (fn [img] [(:url img) (or (:description img) "")]) (:media_attachments raw-post))
|
:images (map (fn [img] [(:url img) (:description img)]) (:media_attachments raw-post))
|
||||||
:posted (:created_at raw-post)))
|
:posted (:created_at raw-post)))
|
||||||
|
|
||||||
(defn save-post [post]
|
(defn save-post [post]
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
(ns micro-blog.nostr
|
|
||||||
(:require
|
|
||||||
[micro-blog.pocket-base :as pb]
|
|
||||||
[micro-blog.is-tech]
|
|
||||||
[taoensso.telemere :as tel]
|
|
||||||
[hato.websocket :as ws]
|
|
||||||
[cheshire.core :as json]
|
|
||||||
[clojure.string :as str]
|
|
||||||
[micro-blog.config :refer [config]])
|
|
||||||
(:import
|
|
||||||
[java.time Instant OffsetDateTime ZoneOffset]
|
|
||||||
[java.time.format DateTimeFormatter]))
|
|
||||||
|
|
||||||
(defn pb-date-to-unix-timestamp-seconds [date-str]
|
|
||||||
(-> date-str
|
|
||||||
(str/replace " " "T")
|
|
||||||
(Instant/parse)
|
|
||||||
(.getEpochSecond)))
|
|
||||||
|
|
||||||
(defn nostr-date-to-pb [ts]
|
|
||||||
(let [instant (java.time.Instant/ofEpochSecond ts)
|
|
||||||
formatter (java.time.format.DateTimeFormatter/ofPattern "yyyy-MM-dd HH:mm:ss.SSSX")
|
|
||||||
zoned (.atZone instant java.time.ZoneOffset/UTC)]
|
|
||||||
(.format formatter zoned)))
|
|
||||||
|
|
||||||
(defn last-post-timestamp []
|
|
||||||
(pb-date-to-unix-timestamp-seconds (:posted (pb/get-latest-post-by-source :nostr))))
|
|
||||||
|
|
||||||
(defn transform-post [post]
|
|
||||||
(tel/log! {:level :info :data {:post post}} "Transforming nostr post")
|
|
||||||
(hash-map :source :nostr
|
|
||||||
:fullPost post
|
|
||||||
:remoteId (get post "id")
|
|
||||||
:isTech (micro-blog.is-tech/is-tech? (get post "content"))
|
|
||||||
:authorId (get post "pubkey")
|
|
||||||
:tags (reduce (fn [acc tag]
|
|
||||||
(let [tag-type (first tag)
|
|
||||||
tag-value (second tag)]
|
|
||||||
(if (= tag-type "t")
|
|
||||||
(conj acc tag-value)
|
|
||||||
acc))) [] (get post "tags"))
|
|
||||||
:images []
|
|
||||||
:posted (nostr-date-to-pb (get post "created_at"))))
|
|
||||||
|
|
||||||
(defn process-msg [raw-msg]
|
|
||||||
(let [msg (json/parse-string (.toString raw-msg))]
|
|
||||||
(tel/log! {:level :info :data {:msg msg}} "Processing nostr message")
|
|
||||||
(let [msg-type (first msg) ;; ex: EVENT
|
|
||||||
event (nth msg 2)]
|
|
||||||
(when (and (= (get event "kind") 1) (= msg-type "EVENT"))
|
|
||||||
(-> event
|
|
||||||
transform-post
|
|
||||||
pb/save-post)))))
|
|
||||||
|
|
||||||
(def socket (atom nil))
|
|
||||||
(defn connect []
|
|
||||||
(tel/log! :info "Opening websocket connection to nostr relay")
|
|
||||||
(reset! socket @(ws/websocket (@config :nostr-relay)
|
|
||||||
{:headers {"User-Agent" "micro-blog-fetcher"}
|
|
||||||
:on-message (fn [_ws msg _last?]
|
|
||||||
(process-msg msg))
|
|
||||||
:on-close (fn [_ws status reason]
|
|
||||||
(tel/log! {:level :warn :data {:status status :reason reason}} "WebSocket connection closed"))})))
|
|
||||||
|
|
||||||
(defn subscribe-to-author [pubkey since]
|
|
||||||
(let [sub-id (@config :nostr-fetcher-id)
|
|
||||||
filter {:kinds [1] :authors [pubkey] :since since}
|
|
||||||
msg (json/generate-string ["REQ" sub-id filter])]
|
|
||||||
(.get (ws/send! @socket msg))))
|
|
||||||
|
|
||||||
(defn close []
|
|
||||||
(ws/close! @socket))
|
|
||||||
|
|
||||||
(defn start []
|
|
||||||
(connect)
|
|
||||||
(subscribe-to-author (@config :nostr-id) (last-post-timestamp)))
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
(ns micro-blog.pocket-base
|
(ns micro-blog.pocket-base
|
||||||
(:require
|
(:require
|
||||||
|
[clojure.pprint :refer [pprint]]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[clj-http.client :as http-client]
|
[clj-http.client :as http-client]
|
||||||
[malli.core :as m]
|
[malli.core :as m]
|
||||||
|
|
@ -43,10 +44,6 @@
|
||||||
(defn valid-source? [source]
|
(defn valid-source? [source]
|
||||||
(m/validate source-enum source))
|
(m/validate source-enum source))
|
||||||
|
|
||||||
(def post-schema [:map
|
|
||||||
[:id :string]
|
|
||||||
[:remoteId :string]])
|
|
||||||
|
|
||||||
(defn get-all-posts-by-source
|
(defn get-all-posts-by-source
|
||||||
([source] (get-all-posts-by-source source [] 1))
|
([source] (get-all-posts-by-source source [] 1))
|
||||||
([source carry page]
|
([source carry page]
|
||||||
|
|
@ -71,11 +68,14 @@
|
||||||
(concat carry rows)
|
(concat carry rows)
|
||||||
(get-all-posts-by-source source (concat carry rows) (inc page))))))
|
(get-all-posts-by-source source (concat carry rows) (inc page))))))
|
||||||
|
|
||||||
(defn get-latest-post-by-source [source]
|
(defn get-latest-post-remote-id-by-source [source]
|
||||||
(let [res-schema
|
(let [res-schema
|
||||||
[:map
|
[:map
|
||||||
[:items
|
[:items
|
||||||
[:vector post-schema]]]]
|
[:vector
|
||||||
|
[:map
|
||||||
|
[:id string?]
|
||||||
|
[:remoteId string?]]]]]]
|
||||||
(when (not (valid-source? source))
|
(when (not (valid-source? source))
|
||||||
(throw (ex-info "Invalid source" {:source source})))
|
(throw (ex-info "Invalid source" {:source source})))
|
||||||
(as->
|
(as->
|
||||||
|
|
@ -85,17 +85,17 @@
|
||||||
"perPage" 1
|
"perPage" 1
|
||||||
:sort "-posted"
|
:sort "-posted"
|
||||||
:filter (str "source = '" (name source) "'")
|
:filter (str "source = '" (name source) "'")
|
||||||
;; :fields (str/join "," ["remoteId" "id"])
|
:fields (str/join "," ["remoteId" "id"])
|
||||||
"skipTotal" true}
|
"skipTotal" true}
|
||||||
:content-type :json
|
:content-type :json
|
||||||
:as :json}) x
|
:as :json}) x
|
||||||
(:body x)
|
(:body x)
|
||||||
(utils/validate-with-throw x res-schema)
|
(if (m/validate res-schema x)
|
||||||
(-> x :items first))))
|
x
|
||||||
|
(do
|
||||||
(defn get-latest-post-remote-id-by-source [source]
|
(m/explain res-schema x)
|
||||||
(tel/log! {:level :info :data {:source source}} "Fetching latest post remote ID for source")
|
(throw (ex-info "Res does not follow schema" {:res x}))))
|
||||||
(:remoteId (get-latest-post-by-source source)))
|
(-> x :items first :remoteId))))
|
||||||
|
|
||||||
(defn post-with-remote-id-already-saved? [remote-id]
|
(defn post-with-remote-id-already-saved? [remote-id]
|
||||||
(->
|
(->
|
||||||
|
|
@ -185,10 +185,11 @@
|
||||||
[:remoteId :string]
|
[:remoteId :string]
|
||||||
[:authorId :string]
|
[:authorId :string]
|
||||||
[:posted :string]]]
|
[:posted :string]]]
|
||||||
|
|
||||||
(utils/validate-with-throw post save-post-schema)
|
(utils/validate-with-throw post save-post-schema)
|
||||||
(tel/log! {:level :info :data {:remoteId (:remoteId post)}} "Post passed save validation")
|
(tel/log! {:level :info :data {:remoteId (:remoteId post)}} "Post passed save validation")
|
||||||
(if (post-with-remote-id-already-saved? (:remoteId post))
|
(if (post-with-remote-id-already-saved? (:remoteId post))
|
||||||
(tel/log! {:level :warn :data {:remoteId (:remoteId post)}} "Post already saved, skipping")
|
(println "post already saved")
|
||||||
(try
|
(try
|
||||||
(http-client/post (str (@config :pocket-base-host) "/api/collections/micro_blog_posts/records")
|
(http-client/post (str (@config :pocket-base-host) "/api/collections/micro_blog_posts/records")
|
||||||
{:headers {"Authorization" (get-login-token-with-cache)}
|
{:headers {"Authorization" (get-login-token-with-cache)}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
[malli.core :as m]))
|
[malli.core :as m]))
|
||||||
|
|
||||||
(defn validate-with-throw [value schema]
|
(defn validate-with-throw [value schema]
|
||||||
(tel/log! {:level :debug :data {:value value :schema schema}} "Validating value")
|
|
||||||
(if (m/validate schema value)
|
(if (m/validate schema value)
|
||||||
value
|
value
|
||||||
(do
|
(do
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue