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"}
|
||||
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"}
|
||||
|
||||
64
src/core.clj
64
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
|
||||
|
||||
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)))
|
||||
Loading…
Reference in New Issue
Block a user