Merge pull request #48 from DomainDrivenArchitecture/transformation
More differentiated transformation #45master
commit
f00b84c75a
@ -0,0 +1,31 @@
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, transformation ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm install -g --save-dev shadow-cljs
|
||||
- run: shadow-cljs compile test
|
||||
- run: shadow-cljs release app
|
||||
- run: rm -rf target/npm-build
|
||||
- run: mkdir target/npm-build
|
||||
- run: cp mastodon-bot.js target/npm-build/
|
||||
- run: cp package.json target/npm-build/
|
||||
- run: cp README.md target/npm-build/
|
||||
- run: tar -cz -C target/npm-build -f target/npm-build.tgz .
|
@ -0,0 +1,45 @@
|
||||
# Releasing
|
||||
|
||||
## dev release
|
||||
```
|
||||
shadow-cljs compile test
|
||||
|
||||
shadow-cljs release app
|
||||
rm -rf target/npm-build
|
||||
mkdir target/npm-build
|
||||
cp mastodon-bot.js target/npm-build/
|
||||
cp package.json target/npm-build/
|
||||
cp README.md target/npm-build/
|
||||
tar -cz -C target/npm-build -f target/npm-build.tgz .
|
||||
|
||||
npm publish ./target/npm-build.tgz --access public --tag dev0
|
||||
```
|
||||
|
||||
## prod release (should be done from master)
|
||||
```
|
||||
shadow-cljs compile test
|
||||
|
||||
#adjust version
|
||||
vi shadow-cljs.edn
|
||||
|
||||
git commit -am "releasing"
|
||||
git tag [version]
|
||||
git push && git push --tag
|
||||
|
||||
shadow-cljs release app
|
||||
|
||||
shadow-cljs release app
|
||||
rm -rf target/npm-build
|
||||
mkdir target/npm-build
|
||||
cp mastodon-bot.js target/npm-build/
|
||||
cp package.json target/npm-build/
|
||||
cp README.md target/npm-build/
|
||||
tar -cz -C target/npm-build -f target/npm-build.tgz .
|
||||
|
||||
npm publish ./target/npm-build.tgz --access public --tag [version]
|
||||
|
||||
# Bump version
|
||||
vi shadow-cljs.edn
|
||||
|
||||
git commit -am "version bump" && git push
|
||||
```
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
||||
{:items [
|
||||
{:title [Blogpost] Idiomatic Clojure: Mixing Parallel Side Effects and Iteration,
|
||||
:link https://www.reddit.com/r/Clojure/comments/gtz1ox/blogpost_idiomatic_clojure_mixing_parallel_side/,
|
||||
:pubDate 2020-05-31T13:28:48.000Z,
|
||||
:author /u/bsless,
|
||||
:content <!-- SC_OFF --><div class="md"><p><a href="https://bsless.github.io/mapping-parallel-side-effects/">https://bsless.github.io/mapping-parallel-side-effects/</a></p> <p>Followup from <a href="https://bsless.github.io/side-effects/">the previous post</a> where I went over the ways to idiomatically map side effects, this post tackles the same issue in a concurrent context. This one has more code than hand-waving, and you can follow along with all the examples.</p> <p>Would appreciate your feedback. It's a rather technical post and there may be things I missed or didn't consider.</p> <p>Cheers</p> </div><!-- SC_ON -->   submitted by   <a href="https://www.reddit.com/user/bsless"> /u/bsless </a> <br/> <span><a href="https://www.reddit.com/r/Cloj
|
||||
ure/comments/gtz1ox/blogpost_idiomatic_clojure_mixing_parallel_side/">[link]</a></span>   <span><a href="https://www.reddit.com/r/Clojure/comments/gtz1ox/blogpost_idiomatic_clojure_mixing_parallel_side/">[comments]</a></span>, :contentSnippet https://bsless.github.io/mapping-parallel-side-effects/ Followup from the previous post where I went over the ways to idiomatically map side effects, this post tackles the same issue in a concurrent context. This one has more code than hand-waving, and you can follow along with all the examples. Would appreciate your feedback. It's a rather technical post and there may be things I missed or didn't consider. Cheers submitted by /u/bsless [link] [comments],
|
||||
:id t3_gtz1ox,
|
||||
:isoDate 2020-05-31T13:28:48.000Z} ]
|
@ -1,12 +1,12 @@
|
||||
{:source-paths ["src/main"
|
||||
"src/test"]
|
||||
:dependencies [[orchestra "2018.12.06-2"]]
|
||||
:builds {:dev {:target :node-library
|
||||
:output-to "target/lib-mastodon-bot.js"
|
||||
:exports {:infra mastodon-bot.core/-main}
|
||||
:repl-pprint true
|
||||
}
|
||||
:dependencies [[orchestra "2019.02.06-1"]
|
||||
[expound "0.8.4"]]
|
||||
:builds {:test {:target :node-test
|
||||
:output-to "target/node-tests.js"
|
||||
:autorun true
|
||||
:repl-pprint true}
|
||||
:app {:target :node-script
|
||||
:output-to "target/mastodon-bot.js"
|
||||
:output-to "mastodon-bot.js"
|
||||
:main mastodon-bot.core/main
|
||||
:compiler-options {:optimizations :simple}}}}
|
||||
|
@ -1,144 +1,114 @@
|
||||
#!/usr/bin/env lumo
|
||||
|
||||
(ns mastodon-bot.core
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.spec.test.alpha :as st]
|
||||
[clojure.string :as cs]
|
||||
[orchestra.core :refer-macros [defn-spec]]
|
||||
[cljs.core :refer [*command-line-args*]]
|
||||
[clojure.string :as string]
|
||||
["rss-parser" :as rss]
|
||||
["tumblr" :as tumblr]
|
||||
[expound.alpha :as expound]
|
||||
[mastodon-bot.infra :as infra]
|
||||
[mastodon-bot.transform :as transform]
|
||||
[mastodon-bot.mastodon-api :as masto]
|
||||
[mastodon-bot.twitter-api :as twitter]))
|
||||
|
||||
(s/def ::mastodon-config masto/mastodon-config?)
|
||||
(s/def ::twitter twitter/twitter-config?)
|
||||
(s/def ::tumblr map?)
|
||||
(s/def ::rss map?)
|
||||
|
||||
(def config? (s/keys :req-un [::mastodon-config]
|
||||
:opt-un [::twitter ::tumblr ::rss]))
|
||||
|
||||
(defn-spec mastodon-config ::mastodon-config
|
||||
[mastodon-bot.twitter-api :as twitter]
|
||||
[mastodon-bot.tumblr-api :as tumblr]))
|
||||
|
||||
(set! s/*explain-out* expound/printer)
|
||||
|
||||
(s/def ::mastodon masto/mastodon-auth?)
|
||||
(s/def ::twitter twitter/twitter-auth?)
|
||||
(s/def ::tumblr tumblr/tumblr-auth?)
|
||||
(s/def ::transform transform/transformations?)
|
||||
(s/def ::auth (s/keys :opt-un [::mastodon ::twitter ::tumblr]))
|
||||
(def config?
|
||||
(s/keys :req-un [::auth ::transform]))
|
||||
|
||||
(s/def ::options (s/* #{"-h"}))
|
||||
(s/def ::config-location (s/? (s/and string?
|
||||
#(not (cs/starts-with? % "-")))))
|
||||
(s/def ::args (s/cat :options ::options
|
||||
:config-location ::config-location))
|
||||
|
||||
(defn-spec mastodon-auth ::mastodon
|
||||
[config config?]
|
||||
(:mastodon-config config))
|
||||
(get-in config [:auth :mastodon]))
|
||||
|
||||
(defn-spec twitter-config ::twitter
|
||||
(defn-spec twitter-auth ::twitter
|
||||
[config config?]
|
||||
(:twitter config))
|
||||
|
||||
(def config (infra/load-config))
|
||||
|
||||
(defn trim-text [text]
|
||||
(let [max-post-length (masto/max-post-length (mastodon-config config))]
|
||||
(cond
|
||||
|
||||
(nil? max-post-length)
|
||||
text
|
||||
|
||||
(> (count text) max-post-length)
|
||||
(reduce
|
||||
(fn [text word]
|
||||
(if (> (+ (count text) (count word)) (- max-post-length 3))
|
||||
(reduced (str text "..."))
|
||||
(str text " " word)))
|
||||
""
|
||||
(string/split text #" "))
|
||||
|
||||
:else text)))
|
||||
|
||||
(defn in [needle haystack]
|
||||
(some (partial = needle) haystack))
|
||||
|
||||
; If the text ends in a link to the media (which is uploaded anyway),
|
||||
; chop it off instead of including the link in the toot
|
||||
(defn chop-tail-media-url [text media]
|
||||
(string/replace text #" (\S+)$" #(if (in (%1 1) (map :url media)) "" (%1 0))))
|
||||
|
||||
(defn parse-tweet [{created-at :created_at
|
||||
text :full_text
|
||||
{:keys [media]} :extended_entities
|
||||
{:keys [screen_name]} :user :as tweet}]
|
||||
{:created-at (js/Date. created-at)
|
||||
:text (trim-text (str (chop-tail-media-url text media)
|
||||
(if (masto/append-screen-name? (mastodon-config config))
|
||||
(str "\n - " screen_name) "")))
|
||||
:media-links (keep #(when (= (:type %) "photo") (:media_url_https %)) media)})
|
||||
|
||||
(defmulti parse-tumblr-post :type)
|
||||
(get-in config [:auth :twitter]))
|
||||
|
||||
(defmethod parse-tumblr-post "text" [{:keys [body date short_url]}]
|
||||
{:created-at (js/Date. date)
|
||||
:text (str (trim-text body) "\n\n" short_url)})
|
||||
|
||||
(defmethod parse-tumblr-post "photo" [{:keys [caption date photos short_url] :as post}]
|
||||
{:created-at (js/Date. date)
|
||||
:text (string/join "\n" [(string/replace caption #"<[^>]*>" "") short_url])
|
||||
:media-links (mapv #(-> % :original_size :url) photos)})
|
||||
|
||||
(defmethod parse-tumblr-post :default [post])
|
||||
|
||||
(defn post-tumblrs [last-post-time]
|
||||
(fn [err response]
|
||||
(->> response
|
||||
infra/js->edn
|
||||
:posts
|
||||
(mapv parse-tumblr-post)
|
||||
(masto/post-items
|
||||
(mastodon-config config)
|
||||
last-post-time))))
|
||||
|
||||
(defn post-tweets [last-post-time]
|
||||
(fn [error tweets response]
|
||||
(if error
|
||||
(infra/exit-with-error error)
|
||||
(->> (infra/js->edn tweets)
|
||||
(map parse-tweet)
|
||||
(masto/post-items
|
||||
(mastodon-config config)
|
||||
last-post-time)))))
|
||||
|
||||
(defn parse-feed [last-post-time parser [title url]]
|
||||
(-> (.parseURL parser url)
|
||||
(.then #(masto/post-items
|
||||
(mastodon-config config)
|
||||
last-post-time
|
||||
(for [{:keys [title isoDate pubDate content link]} (-> % infra/js->edn :items)]
|
||||
{:created-at (js/Date. (or isoDate pubDate))
|
||||
:text (str (trim-text title) "\n\n" (twitter/strip-utm link))})))))
|
||||
(defn-spec tumblr-auth ::tumblr
|
||||
[config config?]
|
||||
(get-in config [:auth :tumblr]))
|
||||
|
||||
(defn tumblr-client [access-keys account]
|
||||
(try
|
||||
(tumblr/Blog. account (clj->js access-keys))
|
||||
(catch js/Error e
|
||||
(infra/exit-with-error
|
||||
(str "failed to connect to Tumblr account " account ": " (.-message e))))))
|
||||
(defn-spec transform ::transform
|
||||
[config config?]
|
||||
(:transform config))
|
||||
|
||||
(defn -main []
|
||||
(defn-spec transform! any?
|
||||
[config config?]
|
||||
(let [mastodon-auth (mastodon-auth config)]
|
||||
(masto/get-mastodon-timeline
|
||||
(mastodon-config config)
|
||||
mastodon-auth
|
||||
(fn [timeline]
|
||||
(let [last-post-time (-> timeline first :created_at (js/Date.))]
|
||||
(let [{:keys [transform]} config]
|
||||
(doseq [transformation transform]
|
||||
(let [source-type (get-in transformation [:source :source-type])
|
||||
target-type (get-in transformation [:target :target-type])]
|
||||
(cond
|
||||
;;post from Twitter
|
||||
(when-let [twitter-config (:twitter config)]
|
||||
(let [{:keys [accounts]} twitter-config]
|
||||
(doseq [account accounts]
|
||||
(twitter/user-timeline
|
||||
twitter-config
|
||||
account
|
||||
(post-tweets last-post-time)))))
|
||||
;;post from Tumblr
|
||||
(when-let [{:keys [access-keys accounts limit]} (:tumblr config)]
|
||||
(doseq [account accounts]
|
||||
(let [client (tumblr-client access-keys account)]
|
||||
(.posts client #js {:limit (or limit 5)} (post-tumblrs last-post-time)))))
|
||||
(and (= :twitter source-type)
|
||||
(= :mastodon target-type))
|
||||
(when-let [twitter-auth (twitter-auth config)]
|
||||
(transform/tweets-to-mastodon
|
||||
mastodon-auth
|
||||
twitter-auth
|
||||
transformation
|
||||
last-post-time))
|
||||
;;post from RSS
|
||||
(when-let [feeds (some-> config :rss)]
|
||||
(let [parser (rss.)]
|
||||
(doseq [feed feeds]
|
||||
(parse-feed last-post-time parser feed))))))))
|
||||
|
||||
(set! *main-cli-fn* -main)
|
||||
(st/instrument 'mastodon-config)
|
||||
(and (= :rss source-type)
|
||||
(= :mastodon target-type))
|
||||
(transform/rss-to-mastodon
|
||||
mastodon-auth
|
||||
transformation
|
||||
last-post-time)
|
||||
;;post from Tumblr
|
||||
(and (= :tumblr source-type)
|
||||
(= :mastodon target-type))
|
||||
(when-let [tumblr-auth (tumblr-auth config)]
|
||||
(transform/tumblr-to-mastodon
|
||||
mastodon-auth
|
||||
tumblr-auth
|
||||
transformation
|
||||
last-post-time))
|
||||
))))
|
||||
)))))
|
||||
|
||||
(def usage
|
||||
"usage:
|
||||
|
||||
node target/mastodon-bot.js [-h] /path/to/config.edn
|
||||
|
||||
or
|
||||
|
||||
npm start [-h] /path/to/config.edn
|
||||
")
|
||||
|
||||
(defn main [& args]
|
||||
(let [parsed-args (s/conform ::args args)]
|
||||
(if (= ::s/invalid parsed-args)
|
||||
(do (s/explain ::args args)
|
||||
(infra/exit-with-error (str "Bad commandline arguments\n" usage)))
|
||||
(let [{:keys [options config-location]} parsed-args]
|
||||
(cond
|
||||
(some #(= "-h" %) options)
|
||||
(print usage)
|
||||
:default
|
||||
(let [config (infra/load-config config-location)]
|
||||
(when (not (s/valid? config? config))
|
||||
(s/explain config? config)
|
||||
(infra/exit-with-error "Bad configuration"))
|
||||
(transform! config)))))))
|
||||
|
||||
(st/instrument 'mastodon-auth)
|
||||
(st/instrument 'twitter-auth)
|
||||
(st/instrument 'transform)
|
||||
|
@ -0,0 +1,40 @@
|
||||
(ns mastodon-bot.rss-api
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.spec.test.alpha :as st]
|
||||
[orchestra.core :refer-macros [defn-spec]]
|
||||
["rss-parser" :as rss]
|
||||
[mastodon-bot.infra :as infra]
|
||||
))
|
||||
|
||||
(s/def ::feed (s/cat :name string? :url string?))
|
||||
(s/def ::feeds (s/coll-of ::feed))
|
||||
(def rss-source? (s/keys :req-un [::feeds]))
|
||||
|
||||
(s/def ::title string?)
|
||||
(s/def ::content string?)
|
||||
(s/def ::link string?)
|
||||
(s/def ::author string?)
|
||||
(s/def ::isoDate string?)
|
||||
(s/def ::pubDate string?)
|
||||
(s/def ::feed-item (s/keys :req-un [::title ::content ::link]
|
||||
:opt-un [::author ::isoDate ::pubDate]))
|
||||
|
||||
(defn-spec rss-client any?
|
||||
[]
|
||||
(rss.))
|
||||
|
||||
(defn-spec parse-feed any?
|
||||
[item ::feed-item]
|
||||
(let [{:keys [title isoDate pubDate content link]} item]
|
||||
{:created-at (js/Date. (or isoDate pubDate))
|
||||
:text (str title
|
||||
"\n\n"
|
||||
link)}))
|
||||
|
||||
(defn-spec get-feed map?
|
||||
[url string?
|
||||
callback fn?]
|
||||
(print url)
|
||||
(-> (.parseURL (rss-client) url)
|
||||
(.then callback)))
|
@ -0,0 +1,188 @@
|
||||
(ns mastodon-bot.transform
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.spec.test.alpha :as st]
|
||||
[orchestra.core :refer-macros [defn-spec]]
|
||||
[clojure.string :as string]
|
||||
[mastodon-bot.infra :as infra]
|
||||
[mastodon-bot.mastodon-api :as masto]
|
||||
[mastodon-bot.twitter-api :as twitter]
|
||||
[mastodon-bot.rss-api :as rss]
|
||||
[mastodon-bot.tumblr-api :as tumblr]
|
||||
["deasync" :as deasync]
|
||||
["request" :as request]))
|
||||
|
||||
(s/def ::created-at any?)
|
||||
(s/def ::text string?)
|
||||
(s/def ::untrimmed-text string?)
|
||||
(s/def ::media-links string?)
|
||||
(s/def ::screen_name string?)
|
||||
(def intermediate? (s/keys :req-un [::created-at ::text ::screen_name]
|
||||
:opt-un [::media-links ::untrimmed-text]))
|
||||
|
||||
(s/def ::source-type #{:twitter :rss :tumblr})
|
||||
(s/def ::resolve-urls? boolean?)
|
||||
(s/def ::content-filter string?)
|
||||
(s/def ::content-filters (s/* ::content-filter))
|
||||
(s/def ::keyword-filter string?)
|
||||
(s/def ::keyword-filters (s/* ::keyword-filter))
|
||||
(s/def ::replacements any?)
|
||||
(defmulti source-type :source-type)
|
||||
(defmethod source-type :twitter [_]
|
||||
(s/merge (s/keys :req-un[::source-type]) twitter/twitter-source?))
|
||||
(defmethod source-type :rss [_]
|
||||
(s/merge (s/keys :req-un [::source-type]) rss/rss-source?))
|
||||
(defmethod source-type :tumblr [_]
|
||||
(s/merge (s/keys :req-un [::source-type]) tumblr/tumblr-source?))
|
||||
(s/def ::source (s/multi-spec source-type ::source-type))
|
||||
|
||||
(s/def ::target-type #{:mastodon})
|
||||
(defmulti target-type :target-type)
|
||||
(defmethod target-type :mastodon [_]
|
||||
(s/merge (s/keys :req-un [::target-type]) masto/mastodon-target?))
|
||||
(s/def ::target (s/multi-spec target-type ::target-type))
|
||||
|
||||
(s/def ::transformation (s/keys :req-un [::source ::target]
|
||||
:opt-un [::resolve-urls? ::content-filters ::keyword-filters
|
||||
::replacements]))
|
||||
(def transformations? (s/* ::transformation))
|
||||
|
||||
(defn resolve-url [[uri]]
|
||||
(try
|
||||
(or
|
||||
(some-> ((deasync request)
|
||||
#js {:method "GET"
|
||||
:uri (if (string/starts-with? uri "https://") uri (str "https://" uri))
|
||||
:followRedirect false})
|
||||
(.-headers)
|
||||
(.-location)
|
||||
(string/replace "?mbid=social_twitter" ""))
|
||||
uri)
|
||||
(catch js/Error _ uri)))
|
||||
|
||||
(def shortened-url-pattern #"(https?://)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?")
|
||||
|
||||
(defn-spec intermediate-resolve-urls intermediate?
|
||||
[resolve-urls? ::resolve-urls?
|
||||
input intermediate?]
|
||||
(if resolve-urls?
|
||||
(update input :text #(string/replace % shortened-url-pattern resolve-url))
|
||||
input))
|
||||
|
||||
(defn-spec content-filter-regexes ::content-filters
|
||||
[transformation ::transformation]
|
||||
(mapv re-pattern (:content-filters transformation)))
|
||||
|
||||
(defn-spec keyword-filter-regexes ::keyword-filters
|
||||
[transformation ::transformation]
|
||||
(mapv re-pattern (:keyword-filters transformation)))
|
||||
|
||||
(defn-spec blocked-content? boolean?
|
||||
[transformation ::transformation
|
||||
text string?]
|
||||
(boolean
|
||||
(or (some #(re-find % text) (content-filter-regexes transformation))
|
||||
(when (not-empty (keyword-filter-regexes transformation))
|
||||
(empty? (some #(re-find % text) (keyword-filter-regexes transformation)))))))
|
||||
|
||||
(defn-spec perform-replacements intermediate?
|
||||
[transformation ::transformation
|
||||
input intermediate?]
|
||||
(update input :text #(reduce-kv string/replace % (:replacements transformation))))
|
||||
|
||||
(defn-spec post-tweets-to-mastodon any?
|
||||
[mastodon-auth masto/mastodon-auth?
|
||||
transformation ::transformation
|
||||
last-post-time any?]
|
||||
(let [{:keys [source target resolve-urls?]} transformation]
|
||||
(fn [error tweets response]
|
||||
(if error
|
||||
(infra/exit-with-error error)
|
||||
(->> (infra/js->edn tweets)
|
||||
(map twitter/parse-tweet)
|
||||
(filter #(> (:created-at %) last-post-time))
|
||||
(remove #(blocked-content? transformation (:text %)))
|
||||
(map #(intermediate-resolve-urls resolve-urls? %))
|
||||
(map #(twitter/nitter-url source %))
|
||||
(map #(perform-replacements transformation %))
|
||||
(map #(masto/intermediate-to-mastodon target %))
|
||||
(masto/post-items mastodon-auth target))))))
|
||||
|
||||
(defn-spec tweets-to-mastodon any?
|
||||
[mastodon-auth masto/mastodon-auth?
|
||||
twitter-auth twitter/twitter-auth?
|
||||
transformation ::transformation
|
||||
last-post-time any?]
|
||||
(let [{:keys [source target resolve-urls?]} transformation]
|
||||
(doseq [account (:accounts source)]
|
||||
(twitter/user-timeline
|
||||
twitter-auth
|
||||
source
|
||||
account
|
||||
(post-tweets-to-mastodon
|
||||
mastodon-auth
|
||||
transformation
|
||||
last-post-time)))))
|
||||
|
||||
(defn-spec post-tumblr-to-mastodon any?
|
||||
[mastodon-auth masto/mastodon-auth?
|
||||
transformation ::transformation
|
||||
last-post-time any?]
|
||||
(let [{:keys [source target resolve-urls?]} transformation]
|
||||
(fn [error tweets response]
|
||||
(if error
|
||||
(infra/exit-with-error error)
|
||||
(->> (infra/js->edn tweets)
|
||||
:posts
|
||||
(mapv tumblr/parse-tumblr-post)
|
||||
(filter #(> (:created-at %) last-post-time))
|
||||
(remove #(blocked-content? transformation (:text %)))
|
||||
(map #(perform-replacements transformation %))
|
||||
(map #(masto/intermediate-to-mastodon target %))
|
||||
(masto/post-items mastodon-auth target))))))
|
||||
|
||||
(defn-spec tumblr-to-mastodon any?
|
||||
[mastodon-auth masto/mastodon-auth?
|
||||
tumblr-auth tumblr/tumblr-auth?
|
||||
transformation ::transformation
|
||||
last-post-time any?]
|
||||
(let [{:keys [accounts limit]} transformation]
|
||||
(doseq [account accounts]
|
||||
(let [client (tumblr/tumblr-client tumblr-auth account)]
|
||||
(.posts client
|
||||
#js {:limit (or limit 5)}
|
||||
(post-tumblr-to-mastodon
|
||||
mastodon-auth
|
||||
transformation
|
||||
last-post-time)
|
||||
)))))
|
||||
|
||||
(defn-spec post-rss-to-mastodon any?
|
||||
[mastodon-auth masto/mastodon-auth?
|
||||
transformation ::transformation
|
||||
last-post-time any?]
|
||||
(let [{:keys [source target resolve-urls?]} transformation]
|
||||
(fn [payload]
|
||||
(->> (infra/js->edn payload)
|
||||
(:items)
|
||||
(map rss/parse-feed)
|
||||
(filter #(> (:created-at %) last-post-time))
|
||||
(remove #(blocked-content? transformation (:text %)))
|
||||
(map #(intermediate-resolve-urls resolve-urls? %))
|
||||
(map #(perform-replacements transformation %))
|
||||
(map #(masto/intermediate-to-mastodon target %))
|
||||
(masto/post-items mastodon-auth target)))))
|
||||
|
||||
|
||||
(defn-spec rss-to-mastodon any?
|
||||
[mastodon-auth masto/mastodon-auth?
|
||||
transformation ::transformation
|
||||
last-post-time any?]
|
||||
(let [{:keys [source target]} transformation]
|
||||
(doseq [[name url] (:feeds source)]
|
||||
(rss/get-feed
|
||||
url
|
||||
(post-rss-to-mastodon
|
||||
mastodon-auth
|
||||
transformation
|
||||
last-post-time)))))
|
@ -0,0 +1,44 @@
|
||||
(ns mastodon-bot.tumblr-api
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.spec.test.alpha :as st]
|
||||
[orchestra.core :refer-macros [defn-spec]]
|
||||
[clojure.string :as string]
|
||||
[mastodon-bot.infra :as infra]
|
||||
["tumblr" :as tumblr]
|
||||
))
|
||||
|
||||
(s/def ::consumer_key string?)
|
||||
(s/def ::consumer_secret string?)
|
||||
(s/def ::token string?)
|
||||
(s/def ::token_secret string?)
|
||||
(def tumblr-auth? (s/keys :req-un [::consumer_key ::consumer_secret ::token
|
||||
::token_secret]))
|
||||
|
||||
(s/def ::limit pos?)
|
||||
(s/def ::account string?)
|
||||
(s/def ::accounts (s/* ::account))
|
||||
(def tumblr-source? (s/keys :req-un [::limit ::accounts]))
|
||||
|
||||
(defn-spec tumblr-client any?
|
||||
[access-keys tumblr-auth?
|
||||
account string?]
|
||||
(try
|
||||
(tumblr/Blog. account (clj->js access-keys))
|
||||
(catch js/Error e
|
||||
(infra/exit-with-error
|
||||
(str "failed to connect to Tumblr account " account ": " (.-message e))))))
|
||||
|
||||
(defmulti parse-tumblr-post :type)
|
||||
|
||||
(defmethod parse-tumblr-post "text" [{:keys [body date short_url]}]
|
||||
{:created-at (js/Date. date)
|
||||
:text body
|
||||
:untrimmed-text (str "\n\n" short_url)})
|
||||
|
||||
(defmethod parse-tumblr-post "photo" [{:keys [caption date photos short_url] :as post}]
|
||||
{:created-at (js/Date. date)
|
||||
:text (string/join "\n" [(string/replace caption #"<[^>]*>" "") short_url])
|
||||
:media-links (mapv #(-> % :original_size :url) photos)})
|
||||
|
||||
(defmethod parse-tumblr-post :default [post])
|
@ -0,0 +1,53 @@
|
||||
(ns mastodon-bot.mastodon-api-test
|
||||
(:require
|
||||
[cljs.test :refer-macros [deftest is testing run-tests]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[mastodon-bot.mastodon-api :as sut]
|
||||
))
|
||||
|
||||
(def intermediate-rss-item {:created-at #inst "2020-06-26T12:17:33.000-00:00"
|
||||
:text "Taking Theatre Online with WebGL and WebRTC\n\nhttps://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/"})
|
||||
|
||||
|
||||
(deftest should-not-append-screen-name
|
||||
(is (= {:created-at #inst "2020-06-26T12:17:33.000-00:00"
|
||||
:text "Taking Theatre Online with WebGL and WebRTC
|
||||
|
||||
https://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/
|
||||
#rssbot"
|
||||
:reblogged true, :media-links nil}
|
||||
(sut/intermediate-to-mastodon {:target-type :mastodon
|
||||
:append-screen-name? false
|
||||
:max-post-length 500
|
||||
:signature "#rssbot"}
|
||||
intermediate-rss-item)))
|
||||
(is (= {:created-at #inst "2020-06-26T12:17:33.000-00:00"
|
||||
:text "Taking Theatre Online with WebGL and WebRTC
|
||||
|
||||
https://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/
|
||||
#rssbot"
|
||||
:reblogged true, :media-links nil}
|
||||
(sut/intermediate-to-mastodon {:target-type :mastodon
|
||||
:max-post-length 500
|
||||
:signature "#rssbot"}
|
||||
intermediate-rss-item))))
|
||||
|
||||
(deftest should-not-trim
|
||||
(is (= {:created-at #inst "2020-06-26T12:17:33.000-00:00"
|
||||
:text "Taking Theatre Online with WebGL and WebRTC
|
||||
|
||||
https://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/"
|
||||
:reblogged true, :media-links nil}
|
||||
(sut/intermediate-to-mastodon {:target-type :mastodon}
|
||||
intermediate-rss-item))))
|
||||
|
||||
(deftest should-not-append-signature
|
||||
(is (= {:created-at #inst "2020-06-26T12:17:33.000-00:00"
|
||||
:text "Taking Theatre Online with WebGL and WebRTC
|
||||
|
||||
https://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/"
|
||||
:reblogged true, :media-links nil}
|
||||
(sut/intermediate-to-mastodon {:target-type :mastodon
|
||||
:append-screen-name? false
|
||||
:max-post-length 500}
|
||||
intermediate-rss-item))))
|
@ -0,0 +1,44 @@
|
||||
(ns mastodon-bot.rss-api-test
|
||||
(:require
|
||||
[cljs.test :refer-macros [deftest is testing run-tests]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[mastodon-bot.rss-api :as sut]
|
||||
))
|
||||
|
||||
(deftest test-spec
|
||||
(is (s/valid? sut/rss-source?
|
||||
{:feeds [["correctiv-blog" "https://news.correctiv.org/news/rss.php"]]}
|
||||
)))
|
||||
|
||||
(def reddit-feed-item {:title "Datahike release 0.3.1"
|
||||
:link
|
||||
"https://www.reddit.com/r/Clojure/comments/hfxotu/datahike_release_031/"
|
||||
:pubDate "2020-06-26T00:36:48.000Z"
|
||||
:author "/u/yogthos"
|
||||
:content
|
||||
"  submitted by   <a href=\"https://www.reddit.com/user/yogthos\"> /u/yogthos </a> <br/> <span><a href=\"https://github.com/replikativ/datahike/releases/tag/v0.3.1\">[link]</a></span>   <span><a href=\"https://www.reddit.com/r/Clojure/comments/hfxotu/datahike_release_031/\">[comments]</a></span>"
|
||||
:contentSnippet "submitted by /u/yogthos [link] [comments]"
|
||||
:id "t3_hfxotu"
|
||||
:isoDate "2020-06-26T00:36:48.000Z"})
|
||||
|
||||
(def hnrss-org-feed-item {:creator "seacaster"
|
||||
:isoDate "2020-06-26T12:17:33.000Z"
|
||||
:content
|
||||
"\n<p>Article URL: <a href=\"https://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/\">https://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/</a></p>\n<p>Comments URL: <a href=\"https://news.ycombinator.com/item?id=23651117\">https://news.ycombinator.com/item?id=23651117</a></p>\n<p>Points: 1</p>\n<p># Comments: 0</p>\n"
|
||||
:comments "https://news.ycombinator.com/item?id=23651117"
|
||||
:dc:creator "seacaster"
|
||||
:pubDate "Fri, 26 Jun 2020 12:17:33 +0000"
|
||||
:contentSnippet
|
||||
"Article URL: https://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/\nComments URL: https://news.ycombinator.com/item?id=23651117\nPoints: 1\n# Comments: 0"
|
||||
:title "Taking Theatre Online with WebGL and WebRTC"
|
||||
:link
|
||||
"https://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/"
|
||||
:guid "https://news.ycombinator.com/item?id=23651117"})
|
||||
|
||||
(deftest items-should-be-parsed
|
||||
(is (= {:created-at #inst "2020-06-26T12:17:33.000-00:00"
|
||||
:text "Taking Theatre Online with WebGL and WebRTC\n\nhttps://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/"}
|
||||
(sut/parse-feed hnrss-org-feed-item)))
|
||||
(is (= {:created-at #inst "2020-06-26T00:36:48.000-00:00",
|
||||
:text "Datahike release 0.3.1\n\nhttps://www.reddit.com/r/Clojure/comments/hfxotu/datahike_release_031/"}
|
||||
(sut/parse-feed reddit-feed-item))))
|
@ -0,0 +1,14 @@
|
||||
(ns mastodon-bot.transform-rss-test
|
||||
(:require
|
||||
[cljs.test :refer-macros [deftest is testing run-tests]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[mastodon-bot.transform :as sut]
|
||||
))
|
||||
|
||||
(def intermediate-rss-item {:created-at #inst "2020-06-26T12:17:33.000-00:00"
|
||||
:text "Taking Theatre Online with WebGL and WebRTC\n\nhttps://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/"})
|
||||
|
||||
(deftest should-not-resolve-urls
|
||||
(is (= {:created-at #inst "2020-06-26T12:17:33.000-00:00"
|
||||
:text "Taking Theatre Online with WebGL and WebRTC\n\nhttps://chrisuehlinger.com/blog/2020/06/16/unshattering-the-audience-building-theatre-on-the-web-in-2020/"}
|
||||
(sut/intermediate-resolve-urls false intermediate-rss-item))))
|
@ -0,0 +1,41 @@
|
||||
(ns mastodon-bot.transform-test
|
||||
(:require
|
||||
[cljs.test :refer-macros [deftest is testing run-tests]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cljs.reader :as edn]
|
||||
["fs" :as fs]
|
||||
[mastodon-bot.core :as core]
|
||||
[mastodon-bot.twitter-api :as twitter]
|
||||
[mastodon-bot.transform :as sut]
|
||||
))
|
||||
|
||||
(deftest test-spec
|
||||
(is (s/valid? sut/transformations?
|
||||
[]))
|
||||
(is (s/valid? sut/transformations?
|
||||
[{:source {:source-type :twitter
|
||||
:include-replies? false
|
||||
:include-rts? true
|
||||
:nitter-urls? true
|
||||
:accounts ["an-twitter-account"]}
|
||||
:target {:target-type :mastodon
|
||||
:append-screen-name? true
|
||||
:media-only? false
|
||||
:max-post-length 500
|
||||
:visibility "unlisted"
|
||||
:sensitive? true
|
||||
:signature "my-bot"}
|
||||
:resolve-urls? true
|
||||
:content-filters [".*bannedsite.*"]
|
||||
:keyword-filters [".*"]}])))
|
||||
|
||||
(defn readfile [filename]
|
||||
(-> filename (fs/readFileSync #js {:encoding "UTF-8"}) edn/read-string))
|
||||
|
||||
(def testconfig (readfile "test.edn"))
|
||||
|
||||
(deftest test-replacements
|
||||
(is (=
|
||||
"💠 Check out what has been going on during March in the world of @ReproBuilds! 💠 https://t.co/k6NsSO115z @opensuse@fosstodon.org @conservancy@mastodon.technology @PrototypeFund@mastodon.social @debian@fosstodon.org "
|
||||
(:text (sut/perform-replacements (first (:transform testconfig)) (twitter/parse-tweet (readfile "testdata/twitter/tweet-mentions.edn"))))
|
||||
)))
|
@ -1,2 +1,29 @@
|
||||
|
||||
{:in_reply_to_screen_name nil, :is_quote_status false, :coordinates nil, :in_reply_to_status_id_str nil, :place nil, :possibly_sensitive false, :geo nil, :in_reply_to_status_id nil, :entities {:hashtags [{:text "hackerspace", :indices [185 197]} {:text "nieuws", :indices [198 205]} {:text "arnhem", :indices [206 213]} {:text "nuarnhem", :indices [214 223]}], :symbols [], :user_mentions [{:screen_name "Hack42", :name "Hackerspace Arnhem", :id 91565087, :id_str "91565087", :indices [137 144]}], :urls [{:url "https://t.co/O1YzlWTFU3", :expanded_url "https://mailchi.mp/6591af748e3e/spamspamspam2", :display_url "mailchi.mp/6591af748e3e/s…", :indices [161 184]}]}, :source "<a href=\"https://about.twitter.com/products/tweetdeck\" rel=\"nofollow\">TweetDeck</a>", :lang "nl", :in_reply_to_user_id_str nil, :full_text "Daar is 'ie dan! SPAMSPAMSPAM editie 2! Met een samenvatting van wat er in deze eerste twee maanden van 2020 gebeurd en gedaan is binnen @hack42. Lees het via: \nhttps://t.co/O1YzlWTFU3 #hackerspace #nieuws #arnhem #nuarnhem", :id 1233321189319291000, :contributors nil, :display_text_range [0 223], :truncated false, :retweeted false, :in_reply_to_user_id nil, :id_str "1233321189319290880", :favorited false, :user {:description "Hack42: hackerspace en computermuseum Arnhem. Een technische creatieve omgeving waar alles kan. Kom langs! Wat wordt jouw project?", :profile_link_color "250F7C", :profile_sidebar_border_color "FFFFFF", :is_translation_enabled true, :profile_image_url "http://pbs.twimg.com/profile_images/1101094129419849728/vypXoIBq_normal.jpg", :profile_use_background_image true, :default_profile false, :profile_background_image_url "http://abs.twimg.com/images/themes/theme15/bg.png", :is_translator false, :profile_text_color "333333", :profile_banner_url "https://pbs.twimg.com/profile_banners/91565087/1497686456", :name "Hackerspace Arnhem", :profile_background_image_url_https "https://abs.twimg.com/images/themes/theme15/bg.png", :favourites_count 277, :screen_name "Hack42", :entities {:url {:urls [{:url "https://t.co/8YNbbxjeYQ", :expanded_url "http://hack42.nl/", :display_url "hack42.nl", :indices [0 23]}]}, :description {:urls []}}, :listed_count 78, :profile_image_url_https "https://pbs.twimg.com/profile_images/1101094129419849728/vypXoIBq_normal.jpg", :statuses_count 11721, :has_extended_profile false, :contributors_enabled false, :following true, :lang nil, :utc_offset nil, :notifications false, :default_profile_image false, :profile_background_color "02152E", :id 91565087, :follow_request_sent false, :url "https://t.co/8YNbbxjeYQ", :translator_type "regular", :time_zone nil, :profile_sidebar_fill_color "C8C8E6", :protected false, :profile_background_tile true, :id_str "91565087", :geo_enabled false, :location "ARNHEM, NL", :followers_count 1666, :friends_count 61, :verified false, :created_at "Sat Nov 21 12:49:38 +0000 2009"}, :retweet_count 2, :favorite_count 5, :created_at "Fri Feb 28 09:21:00 +0000 2020"}
|
||||
{:in_reply_to_screen_name nil,
|
||||
:is_quote_status false,
|
||||
:coordinates nil,
|
||||
:in_reply_to_status_id_str nil,
|
||||
:place nil,
|
||||
:possibly_sensitive false,
|
||||
:geo nil,
|
||||
:in_reply_to_status_id nil,
|
||||
:entities {:hashtags [{:text "hackerspace", :indices [185 197]} {:text "nieuws", :indices [198 205]} {:text "arnhem", :indices [206 213]} {:text "nuarnhem", :indices [214 223]}], :symbols [],
|
||||
:user_mentions [{:screen_name "Hack42", :name "Hackerspace Arnhem", :id 91565087, :id_str "91565087", :indices [137 144]}],
|
||||
:urls [{:url "https://t.co/O1YzlWTFU3",
|
||||
:expanded_url "https://mailchi.mp/6591af748e3e/spamspamspam2",
|
||||
:display_url "mailchi.mp/6591af748e3e/s…", :indices [161 184]}]},
|
||||
:source "<a href=\"https://about.twitter.com/products/tweetdeck\" rel=\"nofollow\">TweetDeck</a>",
|
||||
:lang "nl",
|
||||
:in_reply_to_user_id_str nil,
|
||||
:full_text "Daar is 'ie dan! SPAMSPAMSPAM editie 2! Met een samenvatting van wat er in deze eerste twee maanden van 2020 gebeurd en gedaan is binnen @hack42. Lees het via: \nhttps://t.co/O1YzlWTFU3 #hackerspace #nieuws #arnhem #nuarnhem",
|
||||
:id 1233321189319291000,
|
||||
:contributors nil,
|
||||
:display_text_range [0 223],
|
||||
:truncated false,
|
||||
:retweeted false,
|
||||
:in_reply_to_user_id nil,
|
||||
:id_str "1233321189319290880",
|
||||
:favorited false,
|
||||
:user {:description "Hack42: hackerspace en computermuseum Arnhem. Een technische creatieve omgeving waar alles kan. Kom langs! Wat wordt jouw project?", :profile_link_color "250F7C", :profile_sidebar_border_color "FFFFFF", :is_translation_enabled true, :profile_image_url "http://pbs.twimg.com/profile_images/1101094129419849728/vypXoIBq_normal.jpg", :profile_use_background_image true, :default_profile false, :profile_background_image_url "http://abs.twimg.com/images/themes/theme15/bg.png", :is_translator false, :profile_text_color "333333", :profile_banner_url "https://pbs.twimg.com/profile_banners/91565087/1497686456", :name "Hackerspace Arnhem", :profile_background_image_url_https "https://abs.twimg.com/images/themes/theme15/bg.png", :favourites_count 277, :screen_name "Hack42", :entities {:url {:urls [{:url "https://t.co/8YNbbxjeYQ", :expanded_url "http://hack42.nl/", :display_url "hack42.nl", :indices [0 23]}]}, :description {:urls []}},
|
||||
:listed_count 78, :profile_image_url_https "https://pbs.twimg.com/profile_images/1101094129419849728/vypXoIBq_normal.jpg", :statuses_count 11721, :has_extended_profile false, :contributors_enabled false, :following true, :lang nil, :utc_offset nil, :notifications false, :default_profile_image false, :profile_background_color "02152E", :id 91565087, :follow_request_sent false, :url "https://t.co/8YNbbxjeYQ", :translator_type "regular", :time_zone nil, :profile_sidebar_fill_color "C8C8E6", :protected false, :profile_background_tile true, :id_str "91565087", :geo_enabled false, :location "ARNHEM, NL", :followers_count 1666, :friends_count 61, :verified false, :created_at "Sat Nov 21 12:49:38 +0000 2009"},
|
||||
:retweet_count 2, :favorite_count 5, :created_at "Fri Feb 28 09:21:00 +0000 2020"}
|
||||
|
Reference in New Issue