feat: support seen/unseen state via tailscale
This commit is contained in:
parent
7c36ac5c3c
commit
2ce1a3e8f2
1
deps.edn
1
deps.edn
@ -4,6 +4,7 @@
|
|||||||
ring-logger/ring-logger {:mvn/version "1.1.1"}
|
ring-logger/ring-logger {:mvn/version "1.1.1"}
|
||||||
metosin/reitit {:mvn/version "0.9.1"}
|
metosin/reitit {:mvn/version "0.9.1"}
|
||||||
org.babashka/http-client {:mvn/version "0.4.22"}
|
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 {:mvn/version "1.1.0"}
|
||||||
com.taoensso/telemere-slf4j {:mvn/version "1.1.0"}
|
com.taoensso/telemere-slf4j {:mvn/version "1.1.0"}
|
||||||
hiccup/hiccup {:mvn/version "2.0.0"}
|
hiccup/hiccup {:mvn/version "2.0.0"}
|
||||||
|
|||||||
64
src/core.clj
64
src/core.clj
@ -1,12 +1,13 @@
|
|||||||
(ns core
|
(ns core
|
||||||
(:require
|
(:require
|
||||||
|
[ebird]
|
||||||
|
[tailscale]
|
||||||
[bling.core :as bling]
|
[bling.core :as bling]
|
||||||
[bling.hifi]
|
[bling.hifi]
|
||||||
[cheshire.core :as json]
|
[cheshire.core :as json]
|
||||||
[clojure.java.io :refer [reader]]
|
[clojure.java.io :refer [reader]]
|
||||||
[clojure.math :as math]
|
[clojure.math :as math]
|
||||||
[clojure.string :refer [lower-case]]
|
[clojure.string :refer [lower-case]]
|
||||||
[ebird]
|
|
||||||
[hiccup.page :refer [html5]]
|
[hiccup.page :refer [html5]]
|
||||||
[reitit.ring :as ring]
|
[reitit.ring :as ring]
|
||||||
[ring.adapter.jetty]
|
[ring.adapter.jetty]
|
||||||
@ -37,8 +38,8 @@
|
|||||||
nearby-info))))
|
nearby-info))))
|
||||||
|
|
||||||
(defn add-observation* [bird time certainty]
|
(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]
|
(defn add-observation [{bird-name :bird certainty :certainty} time]
|
||||||
(let [?species (@known-species bird-name)]
|
(let [?species (@known-species bird-name)]
|
||||||
@ -59,21 +60,21 @@
|
|||||||
(add-observations birds now)
|
(add-observations birds now)
|
||||||
{:status 200}))
|
{:status 200}))
|
||||||
|
|
||||||
(defn summarise-species [s]
|
(defn summarise-species [user-id s]
|
||||||
(let [bird (:bird (first s))]
|
(let [bird (:bird (first s))]
|
||||||
{:bird bird
|
{:bird bird
|
||||||
:certainty (apply max (map :certainty s))
|
:certainty (apply max (map :certainty s))
|
||||||
:count (count s)
|
:count (count s)
|
||||||
:nearby (get-nearby bird)
|
:nearby (get-nearby bird)
|
||||||
|
:unseen (not (every? #(contains? (:seen-by %) user-id) s))
|
||||||
:rote-liste (get-in conservation-info [(:latin bird) :status])
|
:rote-liste (get-in conservation-info [(:latin bird) :status])
|
||||||
:first-seen (apply min (map #(.toEpochSecond (.atZone (:time %) (java.time.ZoneId/of "Europe/Berlin"))) s))}))
|
: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
|
(->> observations
|
||||||
(filter #(.isBefore since (:time %)))
|
|
||||||
(group-by #(get-in % [:bird :latin]))
|
(group-by #(get-in % [:bird :latin]))
|
||||||
(vals)
|
(vals)
|
||||||
(map summarise-species)
|
(map (partial summarise-species user-id))
|
||||||
(sort-by :first-seen)))
|
(sort-by :first-seen)))
|
||||||
|
|
||||||
|
|
||||||
@ -106,9 +107,12 @@
|
|||||||
[:head
|
[:head
|
||||||
[:link {:rel "preconnect" :href "https://fonts.googleapis.com"}]
|
[:link {:rel "preconnect" :href "https://fonts.googleapis.com"}]
|
||||||
[:link {:rel "preconnect" :href "https://fonts.gstatic" :crossorigin true}]
|
[: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"}]
|
[:link {:rel "stylesheet"
|
||||||
[:style (str "li.closed::marker {content: '⛦ '; font-size: 1.2em; color:" (colours :blue) "}"
|
: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"}]
|
||||||
"li.open::marker {content: '⚝ '; font-size: 1.2em; color:" (colours :blue) "}")]
|
[: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]
|
[:script {:type "text/javascript"} script]
|
||||||
[:title "🐦 Vogel-Himbeere 🍓"]])
|
[:title "🐦 Vogel-Himbeere 🍓"]])
|
||||||
|
|
||||||
@ -129,10 +133,16 @@
|
|||||||
:extremely-rare [:orange "!!"]
|
:extremely-rare [:orange "!!"]
|
||||||
:critical [:orange "c"]}
|
:critical [:orange "c"]}
|
||||||
(update-vals (partial apply template-concern*))))
|
(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)]
|
(let [observation-count (:observation-count nearby)]
|
||||||
[:li.closed
|
[:li.closed
|
||||||
{:onclick "onExpand(this)"}
|
{:onclick "onExpand(this)"
|
||||||
|
:class [(if unseen :unseen :seen)]}
|
||||||
[:span {:style (when (or (not observation-count) (= 0 observation-count))
|
[:span {:style (when (or (not observation-count) (= 0 observation-count))
|
||||||
"text-decoration-style:double;text-decoration-line:underline;text-decoration-skip-ink:all;")}
|
"text-decoration-style:double;text-decoration-line:underline;text-decoration-skip-ink:all;")}
|
||||||
(or (get-in bird [:name :de])
|
(or (get-in bird [:name :de])
|
||||||
@ -164,9 +174,21 @@
|
|||||||
nil)} (double (/ (math/round (* 10000 certainty)) 100.0))]
|
nil)} (double (/ (math/round (* 10000 certainty)) 100.0))]
|
||||||
" %"]]))
|
" %"]]))
|
||||||
(def date-formatter (java.time.format.DateTimeFormatter/ofPattern "dd MMM HH:mm"))
|
(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)
|
(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
|
{:status 200
|
||||||
:headers {"charset" "utf-8"
|
:headers {"charset" "utf-8"
|
||||||
"Content-Type" "text/html; charset=utf-8"}
|
"Content-Type" "text/html; charset=utf-8"}
|
||||||
@ -179,12 +201,18 @@
|
|||||||
"margin-right: auto;"
|
"margin-right: auto;"
|
||||||
"font-family: \"B612 Mono\";"
|
"font-family: \"B612 Mono\";"
|
||||||
"background-color:" (:background colours) ";"
|
"background-color:" (:background colours) ";"
|
||||||
|
"display: flex;"
|
||||||
|
"flex-direction: column;"
|
||||||
|
"min-height: 100vh;"
|
||||||
"color:" (:text colours) ";")}
|
"color:" (:text colours) ";")}
|
||||||
[:h1 "Birds today 🐦"
|
[:header [:h1 "Birds today 🐦"
|
||||||
[:span {:style (str "font-size:0.3em;margin-left:3em;color:" (colours :brown))}
|
[: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"))]]
|
(str "last updated " (if @last-update
|
||||||
[:ul
|
(.format @last-update date-formatter)
|
||||||
(map template-observation observations)]]))}))
|
"never"))]]]
|
||||||
|
[:ul {:style "flex:1"}
|
||||||
|
(map template-observation summary)]
|
||||||
|
[:footer "(c) fey naomi, user: " [:i (get-in whois [:user-profile :display-name])]]]))}))
|
||||||
|
|
||||||
(def router
|
(def router
|
||||||
(wrap-with-logger
|
(wrap-with-logger
|
||||||
|
|||||||
11
src/tailscale.clj
Normal file
11
src/tailscale.clj
Normal file
@ -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)))
|
||||||
2
todos.md
2
todos.md
@ -1,7 +1,5 @@
|
|||||||
- Add links to wikipedia
|
- Add links to wikipedia
|
||||||
- Add button to remove low likelihood
|
- Add button to remove low likelihood
|
||||||
- Send summary email in the morning
|
- Send summary email in the morning
|
||||||
- Tidy memory periodically
|
|
||||||
- Split log and notification
|
- Split log and notification
|
||||||
- Get user for fun & profit
|
|
||||||
- persistence
|
- persistence
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user