From 2ce1a3e8f2f6000a289ff0c6dcbfd5771f19c6d3 Mon Sep 17 00:00:00 2001 From: Fey Naomi Schrewe Date: Fri, 3 Oct 2025 18:23:52 +0200 Subject: [PATCH] feat: support seen/unseen state via tailscale --- deps.edn | 1 + src/core.clj | 64 ++++++++++++++++++++++++++++++++++------------- src/tailscale.clj | 11 ++++++++ todos.md | 2 -- 4 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 src/tailscale.clj diff --git a/deps.edn b/deps.edn index 6405881..5eae707 100644 --- a/deps.edn +++ b/deps.edn @@ -4,6 +4,7 @@ ring-logger/ring-logger {:mvn/version "1.1.1"} metosin/reitit {:mvn/version "0.9.1"} org.babashka/http-client {:mvn/version "0.4.22"} + babashka/process {:mvn/version "0.6.23"} com.taoensso/telemere {:mvn/version "1.1.0"} com.taoensso/telemere-slf4j {:mvn/version "1.1.0"} hiccup/hiccup {:mvn/version "2.0.0"} diff --git a/src/core.clj b/src/core.clj index 97df23f..bfd60e3 100644 --- a/src/core.clj +++ b/src/core.clj @@ -1,12 +1,13 @@ (ns core (:require + [ebird] + [tailscale] [bling.core :as bling] [bling.hifi] [cheshire.core :as json] [clojure.java.io :refer [reader]] [clojure.math :as math] [clojure.string :refer [lower-case]] - [ebird] [hiccup.page :refer [html5]] [reitit.ring :as ring] [ring.adapter.jetty] @@ -37,8 +38,8 @@ nearby-info)))) (defn add-observation* [bird time certainty] - (swap! observations #(conj % {:bird bird :time time :certainty certainty})) - (bling/callout {:type :info} (str "Heard bird " (get-in bird [:name :en]) " (certainty " (* 100 certainty) "%)"))) + (bling/callout {:type :info} (str "Heard bird " (get-in bird [:name :en]) " (certainty " (* 100 certainty) "%)")) + (swap! observations #(conj % {:bird bird :time time :certainty certainty :seen-by #{}}))) (defn add-observation [{bird-name :bird certainty :certainty} time] (let [?species (@known-species bird-name)] @@ -59,21 +60,21 @@ (add-observations birds now) {:status 200})) -(defn summarise-species [s] +(defn summarise-species [user-id s] (let [bird (:bird (first s))] {:bird bird :certainty (apply max (map :certainty s)) :count (count s) :nearby (get-nearby bird) + :unseen (not (every? #(contains? (:seen-by %) user-id) s)) :rote-liste (get-in conservation-info [(:latin bird) :status]) :first-seen (apply min (map #(.toEpochSecond (.atZone (:time %) (java.time.ZoneId/of "Europe/Berlin"))) s))})) -(defn summarise-observations [observations since] +(defn summarise-observations [observations user-id] (->> observations - (filter #(.isBefore since (:time %))) (group-by #(get-in % [:bird :latin])) (vals) - (map summarise-species) + (map (partial summarise-species user-id)) (sort-by :first-seen))) @@ -106,9 +107,12 @@ [:head [:link {:rel "preconnect" :href "https://fonts.googleapis.com"}] [:link {:rel "preconnect" :href "https://fonts.gstatic" :crossorigin true}] - [:link {:rel "stylesheet" :href "https://fonts.googleapis.com/css2?family=B612+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Cinzel:wght@400..900&display=swap"}] - [:style (str "li.closed::marker {content: '⛦ '; font-size: 1.2em; color:" (colours :blue) "}" - "li.open::marker {content: '⚝ '; font-size: 1.2em; color:" (colours :blue) "}")] + [:link {:rel "stylesheet" + :href "https://fonts.googleapis.com/css2?family=B612+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Cinzel:wght@400..900&display=swap"}] + [:style (str + "li.closed::marker {content: '⛦ '; font-size: 1.2em;}" + "li.open::marker {content: '⚝ '; font-size: 1.2em;}" + "li.unseen::marker {color:" (colours :blue) ";}")] [:script {:type "text/javascript"} script] [:title "🐦 Vogel-Himbeere 🍓"]]) @@ -129,10 +133,16 @@ :extremely-rare [:orange "!!"] :critical [:orange "c"]} (update-vals (partial apply template-concern*)))) -(defn template-observation [{bird :bird certainty :certainty count :count nearby :nearby concern :rote-liste}] +(defn template-observation [{bird :bird + certainty :certainty + count :count + nearby :nearby + concern :rote-liste + unseen :unseen}] (let [observation-count (:observation-count nearby)] [:li.closed - {:onclick "onExpand(this)"} + {:onclick "onExpand(this)" + :class [(if unseen :unseen :seen)]} [:span {:style (when (or (not observation-count) (= 0 observation-count)) "text-decoration-style:double;text-decoration-line:underline;text-decoration-skip-ink:all;")} (or (get-in bird [:name :de]) @@ -164,9 +174,21 @@ nil)} (double (/ (math/round (* 10000 certainty)) 100.0))] " %"]])) (def date-formatter (java.time.format.DateTimeFormatter/ofPattern "dd MMM HH:mm")) -(defn list-observation [_] + +(defn filter-outdated [since obs] + (filter #(.isBefore since (:time %)) obs)) +(defn set-viewed [id since observations] + (->> observations + (map (fn [ob] (assoc ob :seen-by (conj (:seen-by ob) id)))) + (filter-outdated since) + (into []))) + +(defn list-observation [{ip :remote-addr}] (let [since (.minusDays (java.time.LocalDateTime/now) 1) - observations (summarise-observations @observations since)] + whois (tailscale/whois ip) + user-id (get-in whois [:user-profile :id]) + summary (summarise-observations (filter-outdated since @observations) user-id)] + (swap! observations (partial set-viewed user-id since)) {:status 200 :headers {"charset" "utf-8" "Content-Type" "text/html; charset=utf-8"} @@ -179,12 +201,18 @@ "margin-right: auto;" "font-family: \"B612 Mono\";" "background-color:" (:background colours) ";" + "display: flex;" + "flex-direction: column;" + "min-height: 100vh;" "color:" (:text colours) ";")} - [:h1 "Birds today 🐦" + [:header [:h1 "Birds today 🐦" [:span {:style (str "font-size:0.3em;margin-left:3em;color:" (colours :brown))} - (str "last updated " (if @last-update (.format @last-update date-formatter) "never"))]] - [:ul - (map template-observation observations)]]))})) + (str "last updated " (if @last-update + (.format @last-update date-formatter) + "never"))]]] + [:ul {:style "flex:1"} + (map template-observation summary)] + [:footer "(c) fey naomi, user: " [:i (get-in whois [:user-profile :display-name])]]]))})) (def router (wrap-with-logger diff --git a/src/tailscale.clj b/src/tailscale.clj new file mode 100644 index 0000000..607949f --- /dev/null +++ b/src/tailscale.clj @@ -0,0 +1,11 @@ +(ns tailscale + (:require [cheshire.core :as json] + [babashka.process :refer [shell]] + [camel-snake-kebab.core :as camel])) + +(defn whois [ip] + (try + (-> (shell {:out :string} "tailscale" "whois" "--json" (str ip)) + :out + (json/parse-string (comp keyword camel/->kebab-case))) + (catch Exception _ nil))) diff --git a/todos.md b/todos.md index 0fec0be..213bb26 100644 --- a/todos.md +++ b/todos.md @@ -1,7 +1,5 @@ - Add links to wikipedia - Add button to remove low likelihood - Send summary email in the morning -- Tidy memory periodically - Split log and notification -- Get user for fun & profit - persistence