diff options
| author | eltrac <hi@eltr.ac> | 2026-09-21 21:34:34 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-09-23 10:15:19 +0800 |
| commit | 8e616731cd8ef5ace2bfaa3546a5a8bff6699ce0 (patch) | |
| tree | 8e851d476440385a909597f1906e281b00758231 /src | |
| parent | 4d74123bfe80e14ac8f11e9950b64723a070f33a (diff) | |
processor: refactor processor; replace mf2j with mf2clj
- replace mf2j with mf2clj
- remove utils.clj completely
- refactored processor, use Chain of Responsibilities pattern and pass
around maps to reuse data (so we don't have to make a separate HTTP
request just to parse microformats, etc.)
- test: add more test cases to verifier test
- test: add basic test cases to all processing steps
- add more logging to each processing steps. good for debugging.
Diffstat (limited to 'src')
| -rw-r--r-- | src/weepinbell/core.clj | 8 | ||||
| -rw-r--r-- | src/weepinbell/http/downloader.clj (renamed from src/weepinbell/utils.clj) | 27 | ||||
| -rw-r--r-- | src/weepinbell/microformats/parser.clj | 38 | ||||
| -rw-r--r-- | src/weepinbell/microformats/wrapper.clj | 7 | ||||
| -rw-r--r-- | src/weepinbell/processor.clj | 65 | ||||
| -rw-r--r-- | src/weepinbell/processor/janitor.clj | 14 | ||||
| -rw-r--r-- | src/weepinbell/processor/starter.clj | 39 | ||||
| -rw-r--r-- | src/weepinbell/processor/storage_checker.clj | 16 | ||||
| -rw-r--r-- | src/weepinbell/processor/verifier.clj (renamed from src/weepinbell/verifier.clj) | 122 | ||||
| -rw-r--r-- | src/weepinbell/processor/wrapper.clj (renamed from src/weepinbell/wrapper.clj) | 178 | ||||
| -rw-r--r-- | src/weepinbell/processor/writer.clj | 17 | ||||
| -rw-r--r-- | src/weepinbell/storage/edn_storage.clj | 13 | ||||
| -rw-r--r-- | src/weepinbell/ui.clj | 11 |
13 files changed, 278 insertions, 277 deletions
diff --git a/src/weepinbell/core.clj b/src/weepinbell/core.clj index d9a7c9b..4c867ef 100644 --- a/src/weepinbell/core.clj +++ b/src/weepinbell/core.clj @@ -2,7 +2,7 @@ "Main component that loads resources and start other components." (:require [org.httpkit.server :refer [run-server]] [clojure.spec.alpha :as s] - [weepinbell.processor :as processor] + [weepinbell.processor.starter :refer [start-processor]] [weepinbell.handler :refer [app]] [weepinbell.config :refer [load-config]] [weepinbell.webhook :as webhook] @@ -31,7 +31,11 @@ (defn -main [& _] ;; Start webmention processor - (processor/start wmc hooks store config) + (start-processor + {:wmc wmc + :callbacks hooks + :storage store + :config config}) ;; Start receiver server ;; Check for a PORT environment variable, default to 3000 (let [port (Integer/parseInt (or (:port config) (System/getenv "PORT") "3000"))] diff --git a/src/weepinbell/utils.clj b/src/weepinbell/http/downloader.clj index 0b106d5..0e817a6 100644 --- a/src/weepinbell/utils.clj +++ b/src/weepinbell/http/downloader.clj @@ -1,16 +1,7 @@ -;; DEPRECATED !!! - -(ns weepinbell.utils - "This componets is deprecated. - All functions in this namespace will be moved or rewritten." - (:require [weepinbell.http.client :as client] - [clojure.string :as string] - [clojure.walk :as walk] +(ns weepinbell.http.downloader + (:require [taoensso.timbre :as timbre] [clojure.java.io :as io] - [taoensso.timbre :as timbre])) - -(defn javamap->map [map] - (->> (seq map) (into {}) walk/keywordize-keys)) + [weepinbell.http.client :as client])) (defn download-file "Download file to destination. Return true if success, false if failed." @@ -36,15 +27,3 @@ (timbre/error "Download error:" (.getMessage e)) false))) -(defn file-exists? [path] - (.exists (io/file path))) - -(defn format-list [coll] - (let [total (count coll)] - (cond - (= total 0) "" - (= total 1) (first coll) - :else (str (clojure.string/join ", " (butlast coll)) - " and " - (last coll))))) - diff --git a/src/weepinbell/microformats/parser.clj b/src/weepinbell/microformats/parser.clj deleted file mode 100644 index 4a256c3..0000000 --- a/src/weepinbell/microformats/parser.clj +++ /dev/null @@ -1,38 +0,0 @@ -;; DEPRECATED !!! - -(ns weepinbell.microformats.parser - "Deprecated. See issue #20 - https://codeberg.org/eltrac/weepinbell/issues/20" - (:require [clojure.string :refer [includes? lower-case]] - [taoensso.timbre :as timbre]) - (:import [com.kylewm.mf2j Mf2Parser] - [java.net URI])) - -(defn parse - "Parse microformats from a URL." - [url] - (let [max-retries 3] - (loop [attempt 1] - (let [result (try - (let [parser (doto (Mf2Parser.) - (.setIncludeAlternates true) - (.setIncludeRelUrls true))] - {:ok true :data (.parse parser (URI. url))}) - (catch Exception e - {:ok false :exception e}))] - (if (:ok result) - (:data result) - (let [e (:exception result) - msg (.getMessage e)] - (if (and (<= attempt max-retries) - (or (instance? java.net.SocketTimeoutException e) - (includes? (lower-case (str msg)) "timeout"))) - (let [wait-time (* attempt 2000)] - (timbre/warn (format "MF2 Parsing timeout for %s (attempt %d/%d). Retrying in %dms..." - url attempt max-retries wait-time)) - (Thread/sleep wait-time) - (recur (inc attempt))) - (do - (timbre/warn (format "MF2 Parsing failed for %s after %d attempts. Error: %s" - url attempt (or msg "Unknown error"))) - nil)))))))) diff --git a/src/weepinbell/microformats/wrapper.clj b/src/weepinbell/microformats/wrapper.clj index a004aea..c511749 100644 --- a/src/weepinbell/microformats/wrapper.clj +++ b/src/weepinbell/microformats/wrapper.clj @@ -3,8 +3,7 @@ (ns weepinbell.microformats.wrapper "Deprecated. See issue #20 https://codeberg.org/eltrac/weepinbell/issues/20" - (:require [weepinbell.utils :refer [javamap->map]] - [taoensso.timbre :as timbre] + (:require [taoensso.timbre :as timbre] [clojure.spec.alpha :as s])) ;; spec definition @@ -31,9 +30,7 @@ "Use keyword get get a property value from a microformats item." [entry prop] (-> entry - javamap->map :properties - javamap->map (get prop))) (defn- get-prop-first @@ -76,7 +73,7 @@ (let [data {:author (get-card-info (get-prop-first entry :author)) :type (get-entry-type entry) :published (get-prop-first entry :published) - :content (javamap->map (get-prop-first entry :content))}] + :content (get-prop-first entry :content)}] (when-not (s/valid? ::mf-h-entry data) (timbre/warn "microformats h-entry in bad form:" data)) data))) diff --git a/src/weepinbell/processor.clj b/src/weepinbell/processor.clj deleted file mode 100644 index 0be10cd..0000000 --- a/src/weepinbell/processor.clj +++ /dev/null @@ -1,65 +0,0 @@ -(ns weepinbell.processor - "Process in-coming webmentions. - If no such webmention exists and request is verified, create. - If exists and verification fails, delete it. - If exists and verification passes, update it." - (:require [clojure.core.async :refer [go-loop <!]] - [weepinbell.wrapper :as wrapper] - [weepinbell.verifier :refer [verify-mention]] - [weepinbell.utils.digest :refer [digest-short]] - [taoensso.timbre :as timbre])) - -(defn process-new-webmention - "Store new webmention if verifed. Do nothing otherwise." - [id source target callbacks storage config] - (when (verify-mention source target) - (timbre/info "new webmention" id "received.") - (let [data (wrapper/wrap source target (:base-url config))] - ((:append storage) data) - ((:on-create callbacks) data)))) - -(defn process-existing-webmention - "Update existing Webmention when verified, delete it otherwise." - [wm-data callbacks storage config] - (let [webmention (:webmention wm-data) - id (:id webmention) - source (:source webmention) - target (:target webmention)] - (timbre/info "webmention" id "already exists.") - (if (verify-mention source target) - (let [data (wrapper/wrap source target (:base-url config))] - ((:update storage) source target data) - ((:on-update callbacks) data) - (timbre/info "webmention" id "updated.")) - (do - ((:delete storage) source target) - ((:on-delete callbacks) wm-data) - (timbre/warn "webmention" id "no longer valid, deleted."))))) - -(defn process-webmention - "Process webmention one webmention request. - Validate source's Content-Type, inclusion of target URL. - If validated, create webmention when no identical is found, - update or delete if webmention already exists. - - Callbacks and storage are maps of functions, to avoid - depending on technical details directly." - [wm callbacks storage config] - (let [{:keys [s t]} wm - id (digest-short (str s t))] - ((:on-receive callbacks) {:webmention {:id id :source s :target t}}) - ;; check if such webmention exists - (if-let [wm-data ((:get storage) s t)] - ;; if so, update it or delete it - (process-existing-webmention wm-data callbacks storage config) - ;; if not, write to data - (process-new-webmention id s t callbacks storage config)))) - -(defn start - "Start a go-loop that recursively calls to process-webmention." - [wmc callbacks storage config] - (go-loop [wm (<! wmc)] - (timbre/info "webmention found in queue, processing.") - (process-webmention wm callbacks storage config) - (recur (<! wmc)))) - diff --git a/src/weepinbell/processor/janitor.clj b/src/weepinbell/processor/janitor.clj new file mode 100644 index 0000000..905c481 --- /dev/null +++ b/src/weepinbell/processor/janitor.clj @@ -0,0 +1,14 @@ +(ns weepinbell.processor.janitor
+ (:require [taoensso.timbre :as timbre]))
+
+(defn clean
+ [data storage callbacks]
+ (when (:ok? data)
+ (let [{:keys [verified? new? id old-data]} data]
+ (if (and (not new?) (not verified?))
+ (do ;; delete old unverified webmention
+ ((:delete storage) id)
+ ((:on-delete callbacks) old-data)
+ (timbre/warn "webmention" id "no longer valid, deleted.")
+ (-> data (assoc :ok? false)))
+ (-> data (assoc :ok? true))))))
diff --git a/src/weepinbell/processor/starter.clj b/src/weepinbell/processor/starter.clj new file mode 100644 index 0000000..384b4ac --- /dev/null +++ b/src/weepinbell/processor/starter.clj @@ -0,0 +1,39 @@ +(ns weepinbell.processor.starter
+ (:require [clojure.spec.alpha :as s]
+ [clojure.core.async :refer [go-loop <!]]
+ [taoensso.timbre :as timbre]
+ [weepinbell.utils.digest :refer [digest]]
+ [weepinbell.storage.spec]
+ [weepinbell.processor.storage-checker :refer [check-storage]]
+ [weepinbell.processor.verifier :refer [verify]]
+ [weepinbell.processor.janitor :refer [clean]]
+ [weepinbell.processor.wrapper :refer [wrap]]
+ [weepinbell.processor.writer :refer [write]]))
+
+(s/def ::wmc (partial instance? clojure.core.async.impl.channels.ManyToManyChannel))
+(s/def ::callbacks
+ (s/keys :req-un [::on-receive ::on-create ::on-delete ::on-update]))
+(s/def ::storage (partial s/valid? :weepinbell.storage.spec/vtable))
+(s/def ::config
+ (s/keys :req-un [::base-url ::allowed-target-domains]))
+(s/def ::processor-meta
+ (s/keys :req-un [::wmc ::callbacks ::storage ::config]))
+
+(defn start-processor
+ [metadata]
+ {:pre [(s/valid? ::processor-meta metadata)]}
+ (let [{:keys [wmc callbacks storage config]} metadata]
+ (go-loop [wm (<! wmc)]
+ (let [{:keys [s t]} wm
+ id (digest (str s t))]
+ (timbre/info "webmention found in queue, processing.")
+ ((:on-receive callbacks) {:webmention {:id id :source s :target t}})
+ (-> {:id id
+ :wm wm
+ :ok? true}
+ (check-storage storage)
+ verify
+ (clean storage callbacks)
+ (wrap config)
+ (write storage callbacks))
+ (recur (<! wmc))))))
diff --git a/src/weepinbell/processor/storage_checker.clj b/src/weepinbell/processor/storage_checker.clj new file mode 100644 index 0000000..c8aacaf --- /dev/null +++ b/src/weepinbell/processor/storage_checker.clj @@ -0,0 +1,16 @@ +(ns weepinbell.processor.storage-checker
+ (:require [taoensso.timbre :as timbre]))
+
+(defn check-storage
+ ;; data: {:id ..., :wm {:s ... :t ...}, :ok? true}
+ [data storage]
+ (when (:ok? data)
+ (timbre/info "checking if webmention" (:id data) "already exists")
+ (let [{:keys [id wm]} data
+ {:keys [s t]} wm
+ old-data (or ((:get storage) id)
+ ((:get storage) s t))]
+ (-> data
+ (assoc :old-data old-data)
+ (assoc :new? (or (nil? old-data) (empty? old-data)))
+ (assoc :ok? true)))))
diff --git a/src/weepinbell/verifier.clj b/src/weepinbell/processor/verifier.clj index 06bf3fc..9a561ed 100644 --- a/src/weepinbell/verifier.clj +++ b/src/weepinbell/processor/verifier.clj @@ -1,55 +1,67 @@ -(ns weepinbell.verifier - "Verifies in-coming webmentions after receiver checks URL. - This makes sure: - 1) source contains target URL. - 2) source respond with 2** status code. - 3) source's Content-Type is either HTML or plain text." - (:require [clojure.string :refer [includes? trim lower-case split]] - [weepinbell.http.client :as client] - [weepinbell.utils.digest :refer [digest-short]] - [taoensso.timbre :as timbre])) - -(defn- verify-headers [headers] - (when-let [content-type (:content-type headers)] - (-> content-type - (split #";") - first - trim - lower-case - (#{"text/html" "text/plain"})))) - -(defn verify-mention - "Get source and follow redirects to check if target link is present." - [source target] - ;; get first 1MB of source URL and verify - (let [{:keys [status body headers error]} (client/get! source {:headers {"Range" "bytes=0-1048575"}}) - target-headers (:headers (client/head! target)) - id (digest-short (str source target))] - (timbre/info "verifying webmention" id - (str "(" "source: " source ", target: " target ")")) - (cond - ;; handle error - error - (do - (timbre/error "error verifying webmention:" error) - false) - ;; verify HTTP status - (or (not status) (< status 200) (>= status 300)) - (do - (timbre/warn "invalid source of" id ", status:" status) - false) - ;; verify Content-Type - (or (not (verify-headers headers)) - (not (verify-headers target-headers))) - (do - (timbre/warn "source or target of" id "is neither HTML nor plain text content.") - false) - ;; verify inclusion of target URL - ;; TODO needs to be more robust and performant - (not (includes? (str body) target)) - (do - (timbre/warn "invalid source of" id ": target not found in source body.") - false) - ;; true if all verified - :else true))) - +(ns weepinbell.processor.verifier
+ "Verifies in-coming webmentions after receiver checks URL.
+ This makes sure:
+ 1) source contains target URL.
+ 2) source respond with 2** status code.
+ 3) source's Content-Type is either HTML or plain text."
+ (:require [clojure.string :refer [includes? trim lower-case split]]
+ [weepinbell.http.client :as client]
+ [taoensso.timbre :as timbre]))
+
+(defn- verify-headers [headers]
+ (when-let [content-type (:content-type headers)]
+ (-> content-type
+ (split #";")
+ first
+ trim
+ lower-case
+ (#{"text/html" "text/plain"}))))
+
+(defn- verify-mention
+ "Get source and follow redirects to check if target link is present."
+ [id source-resp target-resp source target]
+ ;; get first 1MB of source URL and verify
+ (let [{:keys [status body headers error]} source-resp
+ target-headers (:headers target-resp)]
+ (timbre/info "verifying webmention" id
+ (str "(" "source: " source ", target: " target ")"))
+ (cond
+ ;; handle error
+ error
+ (do
+ (timbre/error "error verifying webmention:" error)
+ false)
+ ;; verify HTTP status
+ (or (not status) (< status 200) (>= status 300))
+ (do
+ (timbre/warn "invalid source of" id ", status:" status)
+ false)
+ ;; verify Content-Type
+ (or (not (verify-headers headers))
+ (not (verify-headers target-headers)))
+ (do
+ (timbre/warn "source or target of" id "is neither HTML nor plain text content.")
+ false)
+ ;; verify inclusion of target URL
+ ;; TODO needs to be more robust and performant
+ (not (includes? (str body) target))
+ (do
+ (timbre/warn "invalid source of" id ": target not found in source body.")
+ false)
+ ;; true if all verified
+ :else true)))
+
+(defn verify
+ [data]
+ (when (:ok? data)
+ (let [{:keys [id wm new?]} data
+ {:keys [s t]} wm
+ source-resp (client/get s {:headers {"Range" "bytes=0-1048575"}})
+ target-resp (client/head t)
+ verified? (verify-mention id @source-resp @target-resp s t)]
+ (-> data
+ (assoc :verified? verified?)
+ (assoc :source-html (:body @source-resp))
+ ;; don't continue if new webmention request is not verified
+ (assoc :ok? (or (not new?) verified?))))))
+
diff --git a/src/weepinbell/wrapper.clj b/src/weepinbell/processor/wrapper.clj index 39f4ba2..9eadcaf 100644 --- a/src/weepinbell/wrapper.clj +++ b/src/weepinbell/processor/wrapper.clj @@ -1,82 +1,96 @@ -(ns weepinbell.wrapper - "Wrapper component wraps a qualified webmention while processing, used by processor. - It adds microformats and extra data and return complete webmention map ready for storage." - (:require [weepinbell.utils :refer [javamap->map - file-exists? - download-file]] - [weepinbell.utils.url :refer [valid-http-url?]] - [weepinbell.microformats.parser :as mf-parser] - [taoensso.timbre :as timbre] - [weepinbell.utils.digest :refer [digest]] - [clojure.spec.alpha :as s])) - -;;;; spec definitions -(s/def ::source valid-http-url?) -(s/def ::target valid-http-url?) -(s/def ::id (s/and string? - #(let [len (count %)] - (or (= len 32) - (= len 8))))) -(s/def ::modified int?) -(s/def ::webmention - (s/keys :req-un [::source ::target ::id ::modified])) - -;; NOTE current microformats parser is unstable -;; will have to postpone microformats spec definition -(s/def ::microformats map?) - -(s/def ::proxied-photo valid-http-url?) -(s/def ::extra - (s/keys :opt-un [::proxied-photo])) - -(s/def ::webmention-wrapped - (s/keys :req-un [::webmention] - :opt-un [::microformats ::extra])) - -;;;; utilities -(defn- find-mf-photo [mf] - (or (-> mf - (get-in [:properties "author"]) - first - (get-in ["properties" "photo"]) - first) - (-> mf (get-in [:properties "photo"]) first))) - -(defn- proxy-mf-photo [raw-photo base-url] - (if raw-photo - (let [photo-url (str raw-photo) - file-ext (or (re-find #"\.(?:jpg|jpeg|png|gif|webp)" photo-url) ".jpg") - filename (str (digest photo-url) file-ext) - local-path (str "data/avatars/" filename) - public-path (str "/avatars/" filename)] - ;; download photo if not exists - (when-not (file-exists? local-path) - (timbre/info "Downloading microformats photo to proxy external image.") - (download-file photo-url local-path)) - (str base-url public-path)) - nil)) - -;;;; main function -(defn wrap - "Parse microformats and collect essential info. - Wrap source, target and collected data into a map and return it." - [source target base-url] - (let [mf (-> source - mf-parser/parse - javamap->map - :items first - javamap->map) - wm {:source source - :target target - :id (digest (str source target)) - :modified (System/currentTimeMillis)} - proxied-photo (-> mf - find-mf-photo - (proxy-mf-photo base-url)) - data {:webmention wm - :microformats mf - :extra {:proxied-photo proxied-photo}}] - (when-not (s/valid? ::webmention-wrapped data) - (timbre/warn "webmention data corrupted before writing to storage" data)) - data)) - +(ns weepinbell.processor.wrapper
+ "Wrapper component wraps a qualified webmention while processing, used by processor.
+ It adds microformats and extra data and return complete webmention map ready for storage."
+ (:require [clojure.java.io :as io]
+ [weepinbell.http.downloader :refer [download-file]]
+ [weepinbell.utils.url :refer [valid-http-url?]]
+ [mf2clj.core :as mf-parser]
+ [taoensso.timbre :as timbre]
+ [weepinbell.utils.digest :refer [digest]]
+ [clojure.spec.alpha :as s]))
+
+;;;; spec definitions
+(s/def ::source valid-http-url?)
+(s/def ::target valid-http-url?)
+(s/def ::id (s/and string?
+ #(let [len (count %)]
+ (or (= len 32)
+ (= len 8)))))
+(s/def ::modified int?)
+(s/def ::webmention
+ (s/keys :req-un [::source ::target ::id ::modified]))
+
+;; NOTE current microformats parser is unstable
+;; will have to postpone microformats spec definition
+(s/def ::microformats map?)
+
+(s/def ::proxied-photo valid-http-url?)
+(s/def ::extra
+ (s/keys :opt-un [::proxied-photo]))
+
+(s/def ::webmention-wrapped
+ (s/keys :req-un [::webmention]
+ :opt-un [::microformats ::extra]))
+
+;;;; utilities
+(def h-entry? #(some #{"h-entry"} (:type %)))
+(def h-card? #(some #{"h-card"} (:type %)))
+
+(defn- find-photo-in-card [h-card]
+ (if-not (some #{"h-card"} (:type h-card)) nil
+ (let [photo (-> h-card :properties :photo first )]
+ (if (map? photo) (:value photo) photo))))
+
+(defn- find-mf-photo [mf]
+ (let [h-entries (filter h-entry? mf)
+ h-cards (filter h-card? mf)]
+ (cond
+ h-cards (-> h-cards first find-photo-in-card)
+ h-entries (->> h-cards first
+ :properties :author first
+ find-photo-in-card))))
+
+(defn- proxy-mf-photo [raw-photo base-url]
+ (if raw-photo
+ (let [photo-url (str raw-photo)
+ file-ext (or (re-find #"\.(?:jpg|jpeg|png|gif|webp)" photo-url) ".jpg")
+ filename (str (digest photo-url) file-ext)
+ local-path (str "data/avatars/" filename)
+ public-path (str "/avatars/" filename)]
+ ;; download photo if not exists
+ (when-not (.exists (io/file local-path))
+ (timbre/info "Downloading microformats photo to proxy external image.")
+ (download-file photo-url local-path))
+ (str base-url public-path))
+ nil))
+
+(defn- wrap-webmention
+ "Parse microformats and collect essential info.
+ Wrap source, target and collected data into a map and return it."
+ [id source-html source target base-url]
+ (timbre/info "parsing microformats and wrapping up webmention" id)
+ (let [mf (-> source-html mf-parser/parse :items)
+ wm {:source source
+ :target target
+ :id (digest (str source target))
+ :modified (System/currentTimeMillis)}
+ proxied-photo (-> mf
+ find-mf-photo
+ (proxy-mf-photo base-url))
+ data {:webmention wm
+ :microformats mf
+ :extra {:proxied-photo proxied-photo}}]
+ (when-not (s/valid? ::webmention-wrapped data)
+ (timbre/warn "webmention" id "data corrupted before writing to storage"))
+ data))
+
+(defn wrap
+ [data config]
+ (when (:ok? data)
+ (let [{:keys [source-html wm id]} data
+ {:keys [s t]} wm]
+ (-> data
+ (assoc :new-data
+ (wrap-webmention id source-html s t (:base-url config)))
+ (assoc :ok true)))))
+
diff --git a/src/weepinbell/processor/writer.clj b/src/weepinbell/processor/writer.clj new file mode 100644 index 0000000..4117c89 --- /dev/null +++ b/src/weepinbell/processor/writer.clj @@ -0,0 +1,17 @@ +(ns weepinbell.processor.writer
+ (:require [taoensso.timbre :as timbre]))
+
+(defn write
+ [data storage callbacks]
+ (when (:ok? data)
+ (let [{:keys [id new? new-data]} data]
+ (if new?
+ (do ;; append new webmention
+ ((:append storage) new-data)
+ ((:on-create callbacks) new-data)
+ (timbre/info "new webmention" id "received."))
+ (do ;; update old webmention
+ ((:update storage) id new-data)
+ ((:on-update callbacks) new-data)
+ (timbre/info "webmention" id "updated.")))
+ (-> data (assoc :ok? true)))))
diff --git a/src/weepinbell/storage/edn_storage.clj b/src/weepinbell/storage/edn_storage.clj index 1cee975..5f07884 100644 --- a/src/weepinbell/storage/edn_storage.clj +++ b/src/weepinbell/storage/edn_storage.clj @@ -2,9 +2,8 @@ "EDN file-based storage middle layer. Entry function is use-store, which makes a valid storage vtable." (:require [clojure.edn :as edn] - [weepinbell.utils :refer [file-exists?]] - [weepinbell.utils.url :refer [get-domain get-target get-path]] [clojure.java.io :as io] + [weepinbell.utils.url :refer [get-domain get-target get-path]] [clojure.string :refer [join]])) (defn unload-data @@ -79,11 +78,15 @@ (defn update-wm "Update an existing webmention" - [datasource source target new-data] - (delete-wm datasource source target) - (append-wm datasource new-data)) + ([datasource id new-data] + (delete-wm datasource id) + (append-wm datasource new-data)) + ([datasource source target new-data] + (delete-wm datasource source target) + (append-wm datasource new-data))) ;; make storage vtable +(def file-exists? #(.exists (io/file %))) (defn use-store [datasource] (when (not (file-exists? datasource)) (io/make-parents datasource) diff --git a/src/weepinbell/ui.clj b/src/weepinbell/ui.clj index 3afa674..8eb5915 100644 --- a/src/weepinbell/ui.clj +++ b/src/weepinbell/ui.clj @@ -4,7 +4,7 @@ "This component is deprecated. Frontend will be refactored. See issue #8" (:require [hiccup2.core :as h] - [weepinbell.utils :refer [format-list]])) + [clojure.string :as string])) (defn- page-response [page] {:body page @@ -18,6 +18,15 @@ [:title "Weepinbell - Webmention Receiver"]] [:body slot]]))) +(defn- format-list [coll] + (let [total (count coll)] + (cond + (= total 0) "" + (= total 1) (first coll) + :else (str (string/join ", " (butlast coll)) + " and " + (last coll))))) + (defn home [domains endpoint preset-target] (-> [:main.home |
