summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-09-21 21:34:34 +0800
committereltrac <hi@eltr.ac>2026-09-23 10:15:19 +0800
commit8e616731cd8ef5ace2bfaa3546a5a8bff6699ce0 (patch)
tree8e851d476440385a909597f1906e281b00758231 /src
parent4d74123bfe80e14ac8f11e9950b64723a070f33a (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.clj8
-rw-r--r--src/weepinbell/http/downloader.clj (renamed from src/weepinbell/utils.clj)27
-rw-r--r--src/weepinbell/microformats/parser.clj38
-rw-r--r--src/weepinbell/microformats/wrapper.clj7
-rw-r--r--src/weepinbell/processor.clj65
-rw-r--r--src/weepinbell/processor/janitor.clj14
-rw-r--r--src/weepinbell/processor/starter.clj39
-rw-r--r--src/weepinbell/processor/storage_checker.clj16
-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.clj17
-rw-r--r--src/weepinbell/storage/edn_storage.clj13
-rw-r--r--src/weepinbell/ui.clj11
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