Let's maybe start committing some stuff
This commit is contained in:
commit
c8b0d4e686
13 changed files with 4909 additions and 0 deletions
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
.cpcache
|
||||
.nrepl-port
|
||||
target
|
||||
repl
|
||||
scratch.clj
|
||||
.shadow-cljs
|
||||
target
|
||||
yarn.lock
|
||||
node_modules/
|
||||
.DS_Store
|
||||
resources/public/ui
|
||||
.store
|
||||
deps.local.edn
|
||||
.#*
|
2
bb.edn
Normal file
2
bb.edn
Normal file
|
@ -0,0 +1,2 @@
|
|||
{:deps {com.lambdaisland/launchpad #_{:mvn/version "0.12.64-alpha"}
|
||||
{:local/root "/home/arne/github/lambdaisland/launchpad"}}}
|
6
bin/launchpad
Executable file
6
bin/launchpad
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bb
|
||||
|
||||
(require '[lambdaisland.launchpad :as launchpad])
|
||||
|
||||
(launchpad/main {:steps (into [(partial launchpad/ensure-java-version 17)]
|
||||
launchpad/default-steps)})
|
6
deps.edn
Normal file
6
deps.edn
Normal file
|
@ -0,0 +1,6 @@
|
|||
{:deps {hato/hato {:mvn/version "0.9.0"}
|
||||
cheshire/cheshire {:mvn/version "5.11.0"}
|
||||
seancorfield/next.jdbc {:mvn/version "1.2.659"}
|
||||
com.impossibl.pgjdbc-ng/pgjdbc-ng {:mvn/version "0.8.9"}
|
||||
|
||||
}}
|
156
repl_sessions/first_experiments.clj
Normal file
156
repl_sessions/first_experiments.clj
Normal file
|
@ -0,0 +1,156 @@
|
|||
(ns first-experiments
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[hato.client :as hato])
|
||||
(:import
|
||||
(com.apicatalog.jsonld JsonLd)
|
||||
(com.apicatalog.jsonld.document JsonDocument)))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
#_
|
||||
(:body
|
||||
(hato/get #_"https://toot.cat/.well-known/webfinger?resource=acct:plexus@toot.cat"
|
||||
#_"https://toot.cat/users/plexus"
|
||||
#_"https://www.w3.org/ns/activitystreams"
|
||||
{:headers {"Accept" "application/json"}
|
||||
:as :stream}))
|
||||
|
||||
(defn json-fetch [url]
|
||||
(hato/get url
|
||||
{:headers {"Accept" "application/json"}
|
||||
:http-client {:redirect-policy :normal}
|
||||
:as :json-string-keys}))
|
||||
|
||||
(defn expand-context
|
||||
([new-context]
|
||||
(expand-context {} new-context))
|
||||
([current-context new-context]
|
||||
(cond
|
||||
(string? new-context)
|
||||
(do (println '->> new-context)
|
||||
(expand-context current-context
|
||||
(get (:body (json-fetch new-context)) "@context")))
|
||||
(sequential? new-context)
|
||||
(reduce expand-context current-context new-context)
|
||||
|
||||
(map? new-context)
|
||||
(into current-context
|
||||
(map
|
||||
(fn [[k v]]
|
||||
(let [id (if (map? v) (get v "@id" v) v)
|
||||
[prefix suffix] (str/split id #":")]
|
||||
(if-let [base (get (merge current-context new-context) prefix)]
|
||||
[k (assoc (if (map? v) v {})
|
||||
"@id" (str (if (map? base) (get base "@id") base)
|
||||
suffix))]
|
||||
[k (if (map? v) v {"@id" v})]))))
|
||||
new-context))))
|
||||
|
||||
(defn apply-context [v ctx]
|
||||
(into {}
|
||||
(map (fn [[k v]]
|
||||
(let [attr (get ctx k)
|
||||
k (get-in ctx [k "@id"] k)
|
||||
v (cond
|
||||
(map? v)
|
||||
(apply-context v ctx)
|
||||
|
||||
(= "@type" k)
|
||||
(get-in ctx [v "@id"] v)
|
||||
|
||||
:else
|
||||
v)]
|
||||
[k (if attr
|
||||
(assoc (dissoc attr "@id") "@value" v)
|
||||
v)])))
|
||||
v))
|
||||
|
||||
(let [user (:body (json-fetch "https://toot.cat/users/plexus"))
|
||||
ctx (expand-context {} (get user "@context"))]
|
||||
(apply-context (dissoc user "@context") ctx)
|
||||
)
|
||||
|
||||
(:body (json-fetch "https://www.w3.org/ns/activitystreams"))
|
||||
|
||||
(def clojure-prefixes
|
||||
{"http://purl.org/dc/terms/" "org.purl.dc"
|
||||
"http://www.w3.org/ns/ldp#" "org.w3.ldp"
|
||||
"http://schema.org#" "org.schema"
|
||||
"http://www.w3.org/2006/vcard/ns#" "org.w3.vcard"
|
||||
"http://joinmastodon.org/ns#" "org.joinmastodon"
|
||||
"https://w3id.org/security#" "org.w3id.security"
|
||||
"https://www.w3.org/ns/activitystreams#" "org.w3.activitystreams"
|
||||
"http://www.w3.org/2001/XMLSchema#" "org.w3.xmlns"})
|
||||
|
||||
(json-fetch "https://toot.cat/users/plexus")
|
||||
|
||||
(def context (expand-context "https://toot.cat/users/plexus"))
|
||||
(keep (comp #(get % "@id") val) context)
|
||||
"https://toot.cat/@plexus/109403085288497274"
|
||||
|
||||
(expand-context "https://www.w3.org/ns/activitystreams#Image")
|
||||
|
||||
#_(.get (com.apicatalog.jsonld.JsonLd/expand "https://toot.cat/users/plexus"))
|
||||
|
||||
;; JsonLd.expand("https://w3c.github.io/json-ld-api/tests/expand/0001-in.jsonld")
|
||||
;; .ordered()
|
||||
;; .get();
|
||||
|
||||
(def ^JsonDocument json-doc
|
||||
(JsonDocument/of
|
||||
^java.io.InputStream
|
||||
(:body
|
||||
(hato/get "https://toot.cat/users/plexus"
|
||||
{:headers {"Accept" "application/json"}
|
||||
:as :stream}))))
|
||||
|
||||
(def expanded (.get (JsonLd/expand json-doc)))
|
||||
(def flattened (.get (JsonLd/flatten json-doc)))
|
||||
(def rdf (.get (JsonLd/toRdf json-doc)))
|
||||
|
||||
(defn to-clj [v]
|
||||
(cond
|
||||
(instance? java.util.List v)
|
||||
(into [] (map to-clj) v)
|
||||
|
||||
(instance? java.util.Map v)
|
||||
(update-vals v to-clj)
|
||||
|
||||
(instance? jakarta.json.JsonString v)
|
||||
(.getString ^jakarta.json.JsonString v)
|
||||
|
||||
(instance? jakarta.json.JsonValue v)
|
||||
(let [t (.getValueType ^jakarta.json.JsonValue v)]
|
||||
(cond
|
||||
(= t jakarta.json.JsonValue$ValueType/TRUE) true
|
||||
(= t jakarta.json.JsonValue$ValueType/FALSE) false)
|
||||
)
|
||||
|
||||
:else v
|
||||
))
|
||||
|
||||
(to-clj expanded)
|
||||
|
||||
;; (clojure.reflect/reflect jakarta.json.JsonValue)
|
||||
;; (clojure.reflect/reflect rdf)
|
||||
|
||||
(clojure.walk/postwalk
|
||||
(fn [v]
|
||||
(if (map? v)
|
||||
(update-keys v (fn [k]
|
||||
(case k
|
||||
"@id" :json-ld/id
|
||||
"@type" :json-ld/type
|
||||
"@value" :json-ld/value
|
||||
(if-let [kw (and (string? k)
|
||||
(some (fn [[url ns]]
|
||||
(when (.startsWith k url)
|
||||
(keyword ns
|
||||
(subs k (.length url)))))
|
||||
clojure-prefixes))]
|
||||
kw
|
||||
k))))
|
||||
v))
|
||||
(to-clj expanded))
|
||||
|
||||
(set! *print-namespace-maps* false)
|
239
repl_sessions/first_stream.clj
Normal file
239
repl_sessions/first_stream.clj
Normal file
|
@ -0,0 +1,239 @@
|
|||
(ns repl-sessions.first-stream
|
||||
(:require [lambdaisland.souk.json-ld :refer :all]))
|
||||
|
||||
(spit "resources/lambdaisland/souk/json_ld_contexts.edn"
|
||||
(with-out-str (clojure.pprint/pprint @context-cache)))
|
||||
|
||||
(reset!
|
||||
context-cache
|
||||
{"https://www.w3.org/ns/activitystreams"
|
||||
{"Dislike" "as:Dislike",
|
||||
"Leave" "as:Leave",
|
||||
"Application" "as:Application",
|
||||
"Listen" "as:Listen",
|
||||
"followers" {"@id" "as:followers", "@type" "@id"},
|
||||
"startIndex" {"@id" "as:startIndex", "@type" "xsd:nonNegativeInteger"},
|
||||
"View" "as:View",
|
||||
"inbox" {"@id" "ldp:inbox", "@type" "@id"},
|
||||
"object" {"@id" "as:object", "@type" "@id"},
|
||||
"Like" "as:Like",
|
||||
"shares" {"@id" "as:shares", "@type" "@id"},
|
||||
"nameMap" {"@id" "as:name", "@container" "@language"},
|
||||
"width" {"@id" "as:width", "@type" "xsd:nonNegativeInteger"},
|
||||
"Relationship" "as:Relationship",
|
||||
"origin" {"@id" "as:origin", "@type" "@id"},
|
||||
"Link" "as:Link",
|
||||
"url" {"@id" "as:url", "@type" "@id"},
|
||||
"bto" {"@id" "as:bto", "@type" "@id"},
|
||||
"inReplyTo" {"@id" "as:inReplyTo", "@type" "@id"},
|
||||
"next" {"@id" "as:next", "@type" "@id"},
|
||||
"ldp" "http://www.w3.org/ns/ldp#",
|
||||
"signClientKey" {"@id" "as:signClientKey", "@type" "@id"},
|
||||
"CollectionPage" "as:CollectionPage",
|
||||
"describes" {"@id" "as:describes", "@type" "@id"},
|
||||
"anyOf" {"@id" "as:anyOf", "@type" "@id"},
|
||||
"Organization" "as:Organization",
|
||||
"OrderedCollection" "as:OrderedCollection",
|
||||
"orderedItems" {"@id" "as:items", "@type" "@id", "@container" "@list"},
|
||||
"Announce" "as:Announce",
|
||||
"OrderedCollectionPage" "as:OrderedCollectionPage",
|
||||
"height" {"@id" "as:height", "@type" "xsd:nonNegativeInteger"},
|
||||
"Note" "as:Note",
|
||||
"formerType" {"@id" "as:formerType", "@type" "@id"},
|
||||
"Offer" "as:Offer",
|
||||
"Video" "as:Video",
|
||||
"Object" "as:Object",
|
||||
"Travel" "as:Travel",
|
||||
"Mention" "as:Mention",
|
||||
"image" {"@id" "as:image", "@type" "@id"},
|
||||
"Audio" "as:Audio",
|
||||
"IntransitiveActivity" "as:IntransitiveActivity",
|
||||
"endpoints" {"@id" "as:endpoints", "@type" "@id"},
|
||||
"bcc" {"@id" "as:bcc", "@type" "@id"},
|
||||
"Flag" "as:Flag",
|
||||
"longitude" {"@id" "as:longitude", "@type" "xsd:float"},
|
||||
"Question" "as:Question",
|
||||
"radius" {"@id" "as:radius", "@type" "xsd:float"},
|
||||
"Public" {"@id" "as:Public", "@type" "@id"},
|
||||
"Activity" "as:Activity",
|
||||
"IsMember" "as:IsMember",
|
||||
"id" "@id",
|
||||
"proxyUrl" {"@id" "as:proxyUrl", "@type" "@id"},
|
||||
"IsContact" "as:IsContact",
|
||||
"Event" "as:Event",
|
||||
"hreflang" "as:hreflang",
|
||||
"Block" "as:Block",
|
||||
"Person" "as:Person",
|
||||
"altitude" {"@id" "as:altitude", "@type" "xsd:float"},
|
||||
"sharedInbox" {"@id" "as:sharedInbox", "@type" "@id"},
|
||||
"latitude" {"@id" "as:latitude", "@type" "xsd:float"},
|
||||
"liked" {"@id" "as:liked", "@type" "@id"},
|
||||
"Arrive" "as:Arrive",
|
||||
"summary" "as:summary",
|
||||
"Delete" "as:Delete",
|
||||
"attachment" {"@id" "as:attachment", "@type" "@id"},
|
||||
"relationship" {"@id" "as:relationship", "@type" "@id"},
|
||||
"href" {"@id" "as:href", "@type" "@id"},
|
||||
"name" "as:name",
|
||||
"closed" {"@id" "as:closed", "@type" "xsd:dateTime"},
|
||||
"vcard" "http://www.w3.org/2006/vcard/ns#",
|
||||
"Article" "as:Article",
|
||||
"tag" {"@id" "as:tag", "@type" "@id"},
|
||||
"published" {"@id" "as:published", "@type" "xsd:dateTime"},
|
||||
"items" {"@id" "as:items", "@type" "@id"},
|
||||
"startTime" {"@id" "as:startTime", "@type" "xsd:dateTime"},
|
||||
"location" {"@id" "as:location", "@type" "@id"},
|
||||
"Update" "as:Update",
|
||||
"Add" "as:Add",
|
||||
"Read" "as:Read",
|
||||
"context" {"@id" "as:context", "@type" "@id"},
|
||||
"partOf" {"@id" "as:partOf", "@type" "@id"},
|
||||
"Remove" "as:Remove",
|
||||
"preferredUsername" "as:preferredUsername",
|
||||
"Profile" "as:Profile",
|
||||
"totalItems" {"@id" "as:totalItems", "@type" "xsd:nonNegativeInteger"},
|
||||
"prev" {"@id" "as:prev", "@type" "@id"},
|
||||
"Follow" "as:Follow",
|
||||
"IsFollowing" "as:IsFollowing",
|
||||
"Tombstone" "as:Tombstone",
|
||||
"subject" {"@id" "as:subject", "@type" "@id"},
|
||||
"Page" "as:Page",
|
||||
"@vocab" "_:",
|
||||
"current" {"@id" "as:current", "@type" "@id"},
|
||||
"content" "as:content",
|
||||
"units" "as:units",
|
||||
"Place" "as:Place",
|
||||
"instrument" {"@id" "as:instrument", "@type" "@id"},
|
||||
"Undo" "as:Undo",
|
||||
"alsoKnownAs" {"@id" "as:alsoKnownAs", "@type" "@id"},
|
||||
"duration" {"@id" "as:duration", "@type" "xsd:duration"},
|
||||
"last" {"@id" "as:last", "@type" "@id"},
|
||||
"rel" "as:rel",
|
||||
"source" "as:source",
|
||||
"TentativeReject" "as:TentativeReject",
|
||||
"type" "@type",
|
||||
"outbox" {"@id" "as:outbox", "@type" "@id"},
|
||||
"mediaType" "as:mediaType",
|
||||
"oneOf" {"@id" "as:oneOf", "@type" "@id"},
|
||||
"deleted" {"@id" "as:deleted", "@type" "xsd:dateTime"},
|
||||
"target" {"@id" "as:target", "@type" "@id"},
|
||||
"replies" {"@id" "as:replies", "@type" "@id"},
|
||||
"provideClientKey" {"@id" "as:provideClientKey", "@type" "@id"},
|
||||
"Create" "as:Create",
|
||||
"updated" {"@id" "as:updated", "@type" "xsd:dateTime"},
|
||||
"generator" {"@id" "as:generator", "@type" "@id"},
|
||||
"endTime" {"@id" "as:endTime", "@type" "xsd:dateTime"},
|
||||
"TentativeAccept" "as:TentativeAccept",
|
||||
"oauthAuthorizationEndpoint"
|
||||
{"@id" "as:oauthAuthorizationEndpoint", "@type" "@id"},
|
||||
"audience" {"@id" "as:audience", "@type" "@id"},
|
||||
"Service" "as:Service",
|
||||
"Image" "as:Image",
|
||||
"Accept" "as:Accept",
|
||||
"Document" "as:Document",
|
||||
"preview" {"@id" "as:preview", "@type" "@id"},
|
||||
"Invite" "as:Invite",
|
||||
"contentMap" {"@id" "as:content", "@container" "@language"},
|
||||
"Group" "as:Group",
|
||||
"oauthTokenEndpoint" {"@id" "as:oauthTokenEndpoint", "@type" "@id"},
|
||||
"uploadMedia" {"@id" "as:uploadMedia", "@type" "@id"},
|
||||
"to" {"@id" "as:to", "@type" "@id"},
|
||||
"accuracy" {"@id" "as:accuracy", "@type" "xsd:float"},
|
||||
"IsFollowedBy" "as:IsFollowedBy",
|
||||
"Reject" "as:Reject",
|
||||
"summaryMap" {"@id" "as:summary", "@container" "@language"},
|
||||
"Join" "as:Join",
|
||||
"Move" "as:Move",
|
||||
"as" "https://www.w3.org/ns/activitystreams#",
|
||||
"actor" {"@id" "as:actor", "@type" "@id"},
|
||||
"likes" {"@id" "as:likes", "@type" "@id"},
|
||||
"following" {"@id" "as:following", "@type" "@id"},
|
||||
"streams" {"@id" "as:streams", "@type" "@id"},
|
||||
"cc" {"@id" "as:cc", "@type" "@id"},
|
||||
"attributedTo" {"@id" "as:attributedTo", "@type" "@id"},
|
||||
"result" {"@id" "as:result", "@type" "@id"},
|
||||
"xsd" "http://www.w3.org/2001/XMLSchema#",
|
||||
"first" {"@id" "as:first", "@type" "@id"},
|
||||
"Collection" "as:Collection",
|
||||
"icon" {"@id" "as:icon", "@type" "@id"},
|
||||
"Ignore" "as:Ignore"},
|
||||
"https://w3id.org/security/v1"
|
||||
{"EncryptedMessage" "sec:EncryptedMessage",
|
||||
"dc" "http://purl.org/dc/terms/",
|
||||
"canonicalizationAlgorithm" "sec:canonicalizationAlgorithm",
|
||||
"owner" {"@id" "sec:owner", "@type" "@id"},
|
||||
"created" {"@id" "dc:created", "@type" "xsd:dateTime"},
|
||||
"signatureValue" "sec:signatureValue",
|
||||
"CryptographicKey" "sec:Key",
|
||||
"publicKeyPem" "sec:publicKeyPem",
|
||||
"iterationCount" "sec:iterationCount",
|
||||
"id" "@id",
|
||||
"publicKey" {"@id" "sec:publicKey", "@type" "@id"},
|
||||
"Ed25519Signature2018" "sec:Ed25519Signature2018",
|
||||
"publicKeyWif" "sec:publicKeyWif",
|
||||
"GraphSignature2012" "sec:GraphSignature2012",
|
||||
"creator" {"@id" "dc:creator", "@type" "@id"},
|
||||
"publicKeyBase58" "sec:publicKeyBase58",
|
||||
"cipherAlgorithm" "sec:cipherAlgorithm",
|
||||
"digestAlgorithm" "sec:digestAlgorithm",
|
||||
"LinkedDataSignature2015" "sec:LinkedDataSignature2015",
|
||||
"cipherData" "sec:cipherData",
|
||||
"privateKey" {"@id" "sec:privateKey", "@type" "@id"},
|
||||
"EcdsaKoblitzSignature2016" "sec:EcdsaKoblitzSignature2016",
|
||||
"expires" {"@id" "sec:expiration", "@type" "xsd:dateTime"},
|
||||
"signatureAlgorithm" "sec:signingAlgorithm",
|
||||
"signature" "sec:signature",
|
||||
"domain" "sec:domain",
|
||||
"LinkedDataSignature2016" "sec:LinkedDataSignature2016",
|
||||
"revoked" {"@id" "sec:revoked", "@type" "xsd:dateTime"},
|
||||
"encryptionKey" "sec:encryptionKey",
|
||||
"cipherKey" "sec:cipherKey",
|
||||
"salt" "sec:salt",
|
||||
"digestValue" "sec:digestValue",
|
||||
"type" "@type",
|
||||
"password" "sec:password",
|
||||
"expiration" {"@id" "sec:expiration", "@type" "xsd:dateTime"},
|
||||
"publicKeyService" {"@id" "sec:publicKeyService", "@type" "@id"},
|
||||
"nonce" "sec:nonce",
|
||||
"authenticationTag" "sec:authenticationTag",
|
||||
"privateKeyPem" "sec:privateKeyPem",
|
||||
"sec" "https://w3id.org/security#",
|
||||
"normalizationAlgorithm" "sec:normalizationAlgorithm",
|
||||
"initializationVector" "sec:initializationVector",
|
||||
"xsd" "http://www.w3.org/2001/XMLSchema#"}})
|
||||
|
||||
(:body (json-get "https://toot.cat/users/plexus"))
|
||||
|
||||
"@type" "https://www.w3.org/ns/activitystreams#Person"
|
||||
|
||||
(:body (json-get "https://toot.cat/users/plexus/outbox"))
|
||||
(:body (json-get "https://toot.cat/users/plexus/outbox?page=true"))
|
||||
(:body (json-get "https://www.w3.org/ns/activitystreams"))
|
||||
|
||||
(:body (json-get "https://toot.cat/users/plexus/outbox"))
|
||||
|
||||
(expand-context
|
||||
(get (:body (json-get "https://toot.cat/users/plexus"))
|
||||
"@context"))
|
||||
|
||||
{"inbox" "https://www.w3.org/ns/activitystreams#inbox"
|
||||
"as" "https://www.w3.org/ns/activitystreams#"
|
||||
}
|
||||
|
||||
JSON-LD linked data
|
||||
RDF Resource Description Framework
|
||||
|
||||
[entity attribute value]
|
||||
[subject predicate object]
|
||||
|
||||
["http://.../clojure" "http://.../type-of-language" "https://..../functional"]
|
||||
|
||||
Uniform Resource Locator: URL
|
||||
Uniform Resource Identifier: URI
|
||||
Internationalized Resource Identifiers: IRI
|
||||
|
||||
|
||||
"@context"
|
||||
- url
|
||||
- map (object)
|
||||
- list of ...
|
410
repl_sessions/json_ld_stuff.clj
Normal file
410
repl_sessions/json_ld_stuff.clj
Normal file
|
@ -0,0 +1,410 @@
|
|||
(ns repl-sessions.json-ld-stuff
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[hato.client :as hato]
|
||||
[cheshire.core :as json])
|
||||
(:import (org.jsoup Jsoup)))
|
||||
|
||||
(set! *print-namespace-maps* false)
|
||||
|
||||
(def json-ld-spec "https://www.w3.org/TR/json-ld11/")
|
||||
|
||||
(def doc (Jsoup/parse ^String (slurp json-ld-spec)))
|
||||
|
||||
(def examples
|
||||
(for [example (.select doc ".example")
|
||||
:let [title (first (.select example ".example-title"))
|
||||
pre (first (.select example "pre"))]
|
||||
:when (and title pre)]
|
||||
(do
|
||||
(doseq [comment (.select pre ".comment")]
|
||||
(.remove comment))
|
||||
[(str/replace (.text title) #"^: " "")
|
||||
(try
|
||||
(json/parse-string (.text (first (.select example "pre")))
|
||||
)
|
||||
(catch Exception e
|
||||
e))])))
|
||||
|
||||
(defn example [title]
|
||||
(get (into {} examples) title))
|
||||
|
||||
(defn json-fetch [url]
|
||||
(hato/get url
|
||||
{:headers {"Accept" "application/json"}
|
||||
:http-client {:redirect-policy :normal}
|
||||
:as :json-string-keys}))
|
||||
|
||||
(def expand-context
|
||||
(memoize
|
||||
(fn
|
||||
([new-context]
|
||||
(expand-context {} new-context))
|
||||
([current-context new-context]
|
||||
(cond
|
||||
(string? new-context)
|
||||
(do (println '->> new-context)
|
||||
(expand-context current-context
|
||||
(get (:body (json-fetch new-context)) "@context")))
|
||||
(sequential? new-context)
|
||||
(reduce expand-context current-context new-context)
|
||||
|
||||
(map? new-context)
|
||||
(into current-context
|
||||
(map
|
||||
(fn [[k v]]
|
||||
(let [id (if (map? v) (get v "@id" v) v)
|
||||
[prefix suffix] (str/split id #":")]
|
||||
(if-let [base (get (merge current-context new-context) prefix)]
|
||||
[k (assoc (if (map? v) v {})
|
||||
"@id" (str (if (map? base) (get base "@id") base)
|
||||
suffix))]
|
||||
[k (if (map? v) v {"@id" v})]))))
|
||||
new-context))))))
|
||||
|
||||
(defn expand-id [id ctx]
|
||||
(if (string? id)
|
||||
(if-let [t (get-in ctx [id "@id"])]
|
||||
t
|
||||
(if (str/includes? id ":")
|
||||
(let [[prefix suffix] (str/split id #":")]
|
||||
(if-let [prefix-url (get-in ctx [prefix "@id"])]
|
||||
(str prefix-url suffix)
|
||||
id))
|
||||
id))
|
||||
id))
|
||||
|
||||
(defn apply-context [v ctx]
|
||||
(into {}
|
||||
(map (fn [[k v]]
|
||||
(let [attr (get ctx k)
|
||||
k (get-in ctx [k "@id"] k)
|
||||
v (cond
|
||||
(map? v)
|
||||
(apply-context v ctx)
|
||||
|
||||
(= "@type" k)
|
||||
(expand-id v ctx)
|
||||
|
||||
:else
|
||||
v)]
|
||||
(prn [k v])
|
||||
[k (if attr
|
||||
(if (= "@id" (get attr "@type"))
|
||||
(assoc attr "@id" (expand-id v ctx))
|
||||
(assoc (dissoc (cond-> attr
|
||||
(contains? attr "@type")
|
||||
(update "@type" expand-id ctx))
|
||||
"@id") "@value" v))
|
||||
v)])))
|
||||
v))
|
||||
|
||||
(defn expand [json-ld]
|
||||
(let [ctx (expand-context {} (get json-ld "@context"))]
|
||||
(apply-context (dissoc json-ld "@context") ctx)))
|
||||
|
||||
(def clojure-prefixes
|
||||
{"dcterms" "http://purl.org/dc/terms/"
|
||||
"ldp" "http://www.w3.org/ns/ldp#"
|
||||
"schema" "http://schema.org#"
|
||||
"vcard" "http://www.w3.org/2006/vcard/ns#"
|
||||
"mastodon" "http://joinmastodon.org/ns#"
|
||||
"security" "https://w3id.org/security#"
|
||||
"activitystreams" "https://www.w3.org/ns/activitystreams#"
|
||||
"xsd" "http://www.w3.org/2001/XMLSchema#"})
|
||||
|
||||
(defn to-clj [json-ld prefixes]
|
||||
(let [shorten #(if (string? %)
|
||||
(or (some (fn [[ns url]]
|
||||
(when (.startsWith % url)
|
||||
(keyword ns
|
||||
(subs % (.length url)))))
|
||||
prefixes)
|
||||
%)
|
||||
%)]
|
||||
(clojure.walk/postwalk
|
||||
(fn [v]
|
||||
(if (map? v)
|
||||
(-> v
|
||||
(update-keys (fn [k]
|
||||
(case k
|
||||
"@id" :rdf/id
|
||||
"@type" :rdf/type
|
||||
"@value" :rdf/value
|
||||
(if-let [kw (shorten k)]
|
||||
kw
|
||||
k))))
|
||||
(update-vals (fn [v]
|
||||
(cond
|
||||
(map? v)
|
||||
(cond
|
||||
(and (= "@id" (:rdf/type v))
|
||||
(contains? v :rdf/id))
|
||||
(:rdf/id v)
|
||||
(contains? v :rdf/value)
|
||||
(case (:rdf/type v)
|
||||
:xsd/dateTime
|
||||
(java.time.ZonedDateTime/parse (:rdf/value v))
|
||||
(:rdf/value v))
|
||||
:else
|
||||
v)
|
||||
(string? v)
|
||||
(shorten v)
|
||||
:else
|
||||
v))))
|
||||
v))
|
||||
(expand json-ld))))
|
||||
|
||||
(defn compact [entity context prefix-map]
|
||||
(let [ctx (into {} (map (juxt (comp #(get % "@id") val) key))
|
||||
(expand-context context))
|
||||
kw->iri (fn [k]
|
||||
(let [iri (str (get prefix-map (namespace k))
|
||||
(name k))]
|
||||
(if-let [n (get ctx iri)]
|
||||
n
|
||||
(if-let [n (some (fn [[k v]]
|
||||
(when (.startsWith iri k)
|
||||
(str v ":" (subs iri (count k)))))
|
||||
ctx)]
|
||||
n
|
||||
k))))
|
||||
expand-kv (fn expand-kv [[k v]]
|
||||
(cond
|
||||
(not (keyword? k))
|
||||
[k v]
|
||||
(= :rdf/id k)
|
||||
["@id" v]
|
||||
(= :rdf/type k)
|
||||
["@type" (kw->iri v)]
|
||||
:else
|
||||
[(kw->iri k)
|
||||
(cond
|
||||
(map? v)
|
||||
(into {} (map expand-kv) v)
|
||||
(sequential? v)
|
||||
(map #(if (map? %)
|
||||
(into {} (map expand-kv) %)
|
||||
%) v)
|
||||
:else v)]))]
|
||||
(into {"@context" context}
|
||||
(map expand-kv)
|
||||
(dissoc entity :rdf/id :rdf/type))))
|
||||
|
||||
|
||||
(comment
|
||||
(println (json/encode (assoc (expand (example "Referencing a JSON-LD context"))
|
||||
"@context" (expand-context {} (get (example "Referencing a JSON-LD context") "@context")))))
|
||||
(println (json/encode (assoc (expand (example "Referencing a JSON-LD context"))
|
||||
"@context" {"foaf" "http://xmlns.com/foaf/0.1/"})))
|
||||
|
||||
|
||||
(println (json/encode(example "Referencing a JSON-LD context")))
|
||||
|
||||
(to-clj (example "Referencing a JSON-LD context")
|
||||
(update-vals (expand-context (get (example "Referencing a JSON-LD context") "@context"))
|
||||
#(get % "@id")))
|
||||
|
||||
(def raw-profile (:body (json-fetch "https://toot.cat/users/plexus")))
|
||||
(def profile-ctx (expand-context (get raw-profile "@context")))
|
||||
(def expanded-profile (expand raw-profile))
|
||||
(def plexus-profile
|
||||
(to-clj expanded-profile clojure-prefixes))
|
||||
|
||||
(get raw-profile "@context")
|
||||
|
||||
(expand-context "https://www.w3.org/ns/activitystreams")
|
||||
|
||||
(compact plexus-profile
|
||||
["https://w3id.org/security/v1"
|
||||
"https://www.w3.org/ns/activitystreams"
|
||||
{"toot" "http://joinmastodon.org/ns#",}]
|
||||
clojure-prefixes)
|
||||
|
||||
|
||||
|
||||
|
||||
(update-vals (to-clj (expand )
|
||||
clojure-prefixes)
|
||||
#(or (:rdf/type %)
|
||||
%))
|
||||
(to-clj (expand (:body (json-fetch "https://toot.cat/users/plexus/followers?page=1")))
|
||||
clojure-prefixes)
|
||||
|
||||
(->> "https://www.w3.org/ns/activitystreams#OrderedCollectionPage"
|
||||
json-fetch
|
||||
:body
|
||||
(#(get % "@context"))
|
||||
vals
|
||||
(keep #(get % "@type"))
|
||||
(into #{}))
|
||||
|
||||
(to-clj (expand (:body (json-fetch "https://toot.cat/users/plexus/collections/featured",)))
|
||||
clojure-prefixes)
|
||||
|
||||
|
||||
(example "Loading a relative context")
|
||||
(to-clj (expand (example "In-line context definition"))
|
||||
{"org.schema" "http://schema.org/"}))
|
||||
|
||||
"Values of @id are interpreted as IRI"
|
||||
"IRIs can be relative"
|
||||
"IRI as a key"
|
||||
"Term expansion from context definition"
|
||||
"Type coercion"
|
||||
"Identifying a node"
|
||||
"Specifying the type for a node"
|
||||
"Specifying multiple types for a node"
|
||||
"Using a term to specify the type"
|
||||
"Referencing Objects on the Web"
|
||||
"Embedding Objects"
|
||||
"Using multiple contexts"
|
||||
"Describing disconnected nodes with @graph"
|
||||
"Embedded contexts within node objects"
|
||||
"Combining external and local contexts"
|
||||
"Setting @version in context"
|
||||
"Using a default vocabulary"
|
||||
"Using the null keyword to ignore data"
|
||||
"Using a default vocabulary relative to a previous default vocabulary"
|
||||
"Use a relative IRI reference as node identifier"
|
||||
"Setting the document base in a document"
|
||||
"Using \"#\" as the vocabulary mapping"
|
||||
"Using \"#\" as the vocabulary mapping (expanded)"
|
||||
"Prefix expansion"
|
||||
"Using vocabularies"
|
||||
"Expanded document used to illustrate compact IRI creation"
|
||||
"Compact IRI generation context (1.0)"
|
||||
"Compact IRI generation term selection (1.0)"
|
||||
"Compact IRI generation context (1.1)"
|
||||
"Compact IRI generation term selection (1.1)"
|
||||
"Aliasing keywords"
|
||||
"IRI expansion within a context"
|
||||
"Using a term to define the IRI of another term within a context"
|
||||
"Using a compact IRI as a term"
|
||||
"Illegal Aliasing of a compact IRI to a different IRI"
|
||||
"Associating context definitions with IRIs"
|
||||
"Illegal circular definition of terms within a context"
|
||||
"Defining an @context within a term definition"
|
||||
"Defining an @context within a term definition used on @type"
|
||||
"Expansion using embedded and scoped contexts"
|
||||
"Expansion using embedded and scoped contexts (embedding equivalent)"
|
||||
"Marking a context to not propagate"
|
||||
"A remote context to be imported in a type-scoped context"
|
||||
"Sourcing a context in a type-scoped context and setting it to propagate"
|
||||
"Result of sourcing a context in a type-scoped context and setting it to propagate"
|
||||
"Sourcing a context to modify @vocab and a term definition"
|
||||
"Result of sourcing a context to modify @vocab and a term definition"
|
||||
"A protected term definition can generally not be overridden"
|
||||
"A protected @context with an exception"
|
||||
"Overriding permitted if both definitions are identical"
|
||||
"overriding permitted in property scoped context"
|
||||
"Expanded term definition with type coercion"
|
||||
"Expanded value with type"
|
||||
"Example demonstrating the context-sensitivity for @type"
|
||||
"Example demonstrating the context-sensitivity for @type (statements)"
|
||||
"JSON Literal"
|
||||
"Expanded term definition with types"
|
||||
"Term expansion for values, not identifiers"
|
||||
"Terms not expanded when document-relative"
|
||||
"Term definitions using IRIs and compact IRIs"
|
||||
"Setting the default language of a JSON-LD document"
|
||||
"Clearing default language"
|
||||
"Expanded term definition with language"
|
||||
"Language map expressing a property in three languages"
|
||||
"Overriding default language using an expanded value"
|
||||
"Removing language information using an expanded value"
|
||||
"Setting the default base direction of a JSON-LD document"
|
||||
"Clearing default base direction"
|
||||
"Expanded term definition with language and direction"
|
||||
"Overriding default language and default base direction using an expanded value"
|
||||
"Multiple values with no inherent order"
|
||||
"Using an expanded form to set multiple values"
|
||||
"Multiple array values of different types"
|
||||
"An ordered collection of values in JSON-LD"
|
||||
"Specifying that a collection is ordered in the context"
|
||||
"Coordinates expressed in GeoJSON"
|
||||
"Coordinates expressed in JSON-LD"
|
||||
"An unordered collection of values in JSON-LD"
|
||||
"Specifying that a collection is unordered in the context"
|
||||
"Setting @container: @set on @type"
|
||||
"Nested properties"
|
||||
"Nested properties folded into containing object"
|
||||
"Defining property nesting - Expanded Input"
|
||||
"Defining property nesting - Context"
|
||||
"Defining property nesting"
|
||||
"Referencing node objects"
|
||||
"Embedding a node object as property value of another node object"
|
||||
"Referencing an unidentified node"
|
||||
"Specifying a local blank node identifier"
|
||||
"Indexing data in JSON-LD"
|
||||
"Indexing data using @none"
|
||||
"Property-based data indexing"
|
||||
"Indexing languaged-tagged strings in JSON-LD"
|
||||
"Indexing languaged-tagged strings in JSON-LD with @set representation"
|
||||
"Indexing languaged-tagged strings using @none for no language"
|
||||
"Indexing data in JSON-LD by node identifiers"
|
||||
"Indexing data in JSON-LD by node identifiers with @set representation"
|
||||
"Indexing data in JSON-LD by node identifiers using @none"
|
||||
"Indexing data in JSON-LD by type"
|
||||
"Indexing data in JSON-LD by type with @set representation"
|
||||
"Indexing data in JSON-LD by type using @none"
|
||||
"Included Blocks"
|
||||
"Flattened form for included blocks"
|
||||
"Describing disconnected nodes with @included"
|
||||
"A document with children linking to their parent"
|
||||
"A person and its children using a reverse property"
|
||||
"Using @reverse to define reverse properties"
|
||||
"Identifying and making statements about a graph"
|
||||
"Using @graph to explicitly express the default graph"
|
||||
"Context needs to be duplicated if @graph is not used"
|
||||
"Implicitly named graph"
|
||||
"Indexing graph data in JSON-LD"
|
||||
"Indexing graphs using @none for no index"
|
||||
"Referencing named graphs using an id map"
|
||||
"Referencing named graphs using an id map with @none"
|
||||
"Sample JSON-LD document to be expanded"
|
||||
"Expanded form for the previous example"
|
||||
"Sample expanded JSON-LD document"
|
||||
"Sample context"
|
||||
"Compact form of the sample document once sample context has been applied"
|
||||
"Compacting using a default vocabulary"
|
||||
"Compacting using a base IRI"
|
||||
"Coercing Values to Strings"
|
||||
"Using Arrays for Lists"
|
||||
"Reversing Node Relationships"
|
||||
"Indexing language-tagged strings"
|
||||
"Forcing Object Values"
|
||||
"Indexing language-tagged strings and @set"
|
||||
"Term Selection"
|
||||
"Sample JSON-LD document to be flattened"
|
||||
"Flattened and compacted form for the previous example"
|
||||
"Sample library frame"
|
||||
"Flattened library objects"
|
||||
"Framed library objects"
|
||||
"Referencing a JSON-LD context from a JSON document via an HTTP Link Header"
|
||||
"Specifying an alternate location via an HTTP Link Header"
|
||||
"Embedding JSON-LD in HTML"
|
||||
"Combining multiple JSON-LD script elements into a single dataset"
|
||||
"Using the document base URL to establish the default base IRI"
|
||||
"Embedding JSON-LD containing HTML in HTML"
|
||||
"Targeting a specific script element by id"
|
||||
"Illegal Unconnected Node"
|
||||
"Linked Data Dataset"
|
||||
"Sample JSON-LD document"
|
||||
"Flattened and expanded form for the previous example"
|
||||
"Turtle representation of expanded/flattened document"
|
||||
"A set of statements serialized in Turtle"
|
||||
"The same set of statements serialized in JSON-LD"
|
||||
"Embedding in Turtle"
|
||||
"Same embedding example in JSON-LD"
|
||||
"JSON-LD using native data types for numbers and boolean values"
|
||||
"Same example in Turtle using typed literals"
|
||||
"A list of values in Turtle"
|
||||
"Same example with a list of values in JSON-LD"
|
||||
"RDFa fragment that describes three people"
|
||||
"Same description in JSON-LD (context shared among node objects)"
|
||||
"HTML that describes a book using microdata"
|
||||
"Same book description in JSON-LD (avoiding contexts)"
|
||||
"HTTP Request with profile requesting an expanded document"
|
||||
"HTTP Request with profile requesting a compacted document"
|
||||
"HTTP Request with profile requesting a compacted document with a reference to a compaction context"
|
226
repl_sessions/pg_stuff.clj
Normal file
226
repl_sessions/pg_stuff.clj
Normal file
|
@ -0,0 +1,226 @@
|
|||
(ns repl-sessions.pg-stuff
|
||||
(:require [clojure.string :as str]
|
||||
[next.jdbc :as jdbc]
|
||||
[honey.sql :as sql]
|
||||
[next.jdbc.sql :as nsql]
|
||||
[next.jdbc.date-time :as jdbc-date-time]
|
||||
[next.jdbc.result-set :as rs]
|
||||
[next.jdbc.plan]
|
||||
[cheshire.core :as json]
|
||||
))
|
||||
|
||||
(defn pg-url [db-name]
|
||||
(str "jdbc:pgsql://localhost:5432/" db-name "?user=postgres"))
|
||||
|
||||
(defn recreate-db! [name]
|
||||
(let [ds (jdbc/get-datasource (pg-url "postgres"))]
|
||||
(jdbc/execute! ds [(str "DROP DATABASE IF EXISTS " name)])
|
||||
(jdbc/execute! ds [(str "CREATE DATABASE " name)])))
|
||||
|
||||
(recreate-db! "souk")
|
||||
|
||||
(defn sql-ident [v]
|
||||
(if (sequential? v)
|
||||
(str/join "." (map identifier v))
|
||||
(str "\""
|
||||
(if (keyword? v)
|
||||
(subs (str v) 1)
|
||||
v)
|
||||
"\"")))
|
||||
|
||||
(defn sql-kw [k]
|
||||
(str/upper-case
|
||||
(str/replace
|
||||
(if (keyword? k)
|
||||
(name k)
|
||||
k)
|
||||
#"-" " ")))
|
||||
|
||||
(defn sql-str [& ss]
|
||||
(str "'" (str/replace (apply str ss) #"'" "''") "'"))
|
||||
|
||||
(defn sql-list
|
||||
([items]
|
||||
(sql-list "(" ")" ", " items))
|
||||
([before after separator items]
|
||||
(str before (apply str (str/join separator items)) after)))
|
||||
|
||||
(defn strs [& items]
|
||||
(str/join " " items))
|
||||
|
||||
(defn sql [& items]
|
||||
(apply strs
|
||||
(map (fn [x]
|
||||
(cond
|
||||
(vector? x)
|
||||
(case (first x)
|
||||
:ident (sql-ident (second x))
|
||||
:kw (sql-kw (second x))
|
||||
:str (sql-str (second x))
|
||||
:raw (second x)
|
||||
:list (sql-list (map sql (next x)))
|
||||
:commas (sql-list "" "" ", " (map sql (next x)))
|
||||
:fn (str (second x)
|
||||
(sql-list (map sql (nnext x))))
|
||||
(apply sql x))
|
||||
(keyword? x)
|
||||
(sql-ident x)
|
||||
(symbol? x)
|
||||
(sql-kw x)
|
||||
(string? x)
|
||||
(sql-str x)
|
||||
(sequential? x)
|
||||
(sql-list "(" ")" ", " (map sql x))))
|
||||
items)))
|
||||
|
||||
(sql 'create-table :activitystreams/Person
|
||||
(list
|
||||
[:souk/id 'text 'primary-key]
|
||||
[:souk/properties 'jsonb 'default "{}"] ))
|
||||
|
||||
(def set-ts-trigger-def "CREATE OR REPLACE FUNCTION trigger_set_timestamp()\nRETURNS TRIGGER AS $$\nBEGIN\n NEW.\"meta/updated-at\" = NOW();\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;")
|
||||
(def default-properties
|
||||
[[:rdf/id 'text 'primary-key]
|
||||
[:rdf/props 'jsonb 'default "{}"]
|
||||
[:meta/created-at 'timestamp-with-time-zone 'default [:fn 'now] 'not-null]
|
||||
[:meta/updated-at 'timestamp-with-time-zone]])
|
||||
|
||||
(let [ds (jdbc/get-datasource (pg-url "souk"))]
|
||||
(jdbc/execute! ds [set-ts-trigger-def])
|
||||
(jdbc/execute! ds [(sql 'drop-table
|
||||
'if-exists
|
||||
:activitystreams/Person)])
|
||||
(jdbc/execute! ds [(sql 'create-table :activitystreams/Person
|
||||
(concat
|
||||
default-properties
|
||||
[[:activitystreams/name 'text 'not-null]
|
||||
[:activitystreams/preferredUsername 'text 'not-null]
|
||||
[:activitystreams/url 'text 'not-null]
|
||||
[:activitystreams/summary 'text]
|
||||
[:ldp/inbox 'text 'not-null]
|
||||
[:activitystreams/outbox 'text 'not-null]
|
||||
[:activitystreams/published 'timestamp-with-time-zone]]))])
|
||||
(jdbc/execute! ds [(sql 'create-trigger :set-timestamp
|
||||
'before-update
|
||||
'on :activitystreams/Person
|
||||
'for-each-row
|
||||
'execute-procedure [:fn 'trigger_set_timestamp])]))
|
||||
(def table-columns
|
||||
(into {}
|
||||
(let [ds (jdbc/get-datasource (pg-url "souk"))
|
||||
opts {}]
|
||||
(with-open [con (jdbc/get-connection ds opts)]
|
||||
(let [md (.getMetaData con)] ; produces java.sql.DatabaseMetaData
|
||||
(doall
|
||||
(for [{:keys [pg_class/TABLE_NAME]} (-> md
|
||||
;; return a java.sql.ResultSet describing all tables and views:
|
||||
(.getTables nil nil nil (into-array ["TABLE" "VIEW"]))
|
||||
(rs/datafiable-result-set ds opts))]
|
||||
[(keyword TABLE_NAME)
|
||||
(map (comp keyword :COLUMN_NAME) (rs/datafiable-result-set (.getColumns md nil nil TABLE_NAME nil) ds opts)
|
||||
)
|
||||
])))))))
|
||||
(to-clj (expand (:body (json-fetch "https://toot.cat/users/plexus")))
|
||||
clojure-prefixes)
|
||||
|
||||
(defn pg-coerce [val]
|
||||
(cond
|
||||
(instance? java.time.ZonedDateTime val)
|
||||
(.toOffsetDateTime val)
|
||||
#_[:raw (strs (sql-kw 'timestamp-with-time-zone)
|
||||
(sql-str
|
||||
|
||||
(.toLocalDate ^java.time.ZonedDateTime val) " "
|
||||
(let [t (.toLocalTime ^java.time.ZonedDateTime val)]
|
||||
(format "%d:%02d:%02d" (.getHour t)
|
||||
(.getMinute t) (.getSecond t)))
|
||||
(.getZone ^java.time.ZonedDateTime val)))]
|
||||
:else
|
||||
val))
|
||||
|
||||
(defn insert-sql [entity]
|
||||
(let [{:rdf/keys [type]} entity
|
||||
cols (get table-columns type)
|
||||
props (seq (select-keys entity cols))]
|
||||
(into [(sql 'insert-into type
|
||||
(cons :rdf/props
|
||||
(map key props))
|
||||
'values
|
||||
(repeat (inc (count props)) '?)
|
||||
'on-conflict [:raw "(\"rdf/id\")"]
|
||||
'do
|
||||
'update-set
|
||||
(into [:commas]
|
||||
(map (fn [[k]]
|
||||
[k '= '?]))
|
||||
props))]
|
||||
(cons
|
||||
(json/encode (apply dissoc entity :rdf/type (map key props)))
|
||||
(concat
|
||||
(map (comp pg-coerce val) props)
|
||||
(map (comp pg-coerce val) props))))))
|
||||
|
||||
(let [ds (jdbc/get-datasource (pg-url "souk"))]
|
||||
(jdbc/execute! ds (insert-sql (assoc repl-sessions.json-ld-stuff/plexus-profile
|
||||
:activitystreams/name "John Doe")))
|
||||
)
|
||||
|
||||
(defrecord MyMapResultSetBuilder [^java.sql.ResultSet rs rsmeta cols]
|
||||
rs/RowBuilder
|
||||
(->row [this] (transient {}))
|
||||
(column-count [this] (count cols))
|
||||
(with-column [this row i]
|
||||
(rs/with-column-value this row (nth cols (dec i))
|
||||
(if (= java.sql.Types/TIMESTAMP_WITH_TIMEZONE (.getColumnType rsmeta i))
|
||||
(.getObject rs ^Integer i ^Class java.time.OffsetDateTime)
|
||||
(rs/read-column-by-index (.getObject rs ^Integer i) rsmeta i))))
|
||||
(with-column-value [this row col v]
|
||||
(assoc! row col v))
|
||||
(row! [this row] (persistent! row))
|
||||
rs/ResultSetBuilder
|
||||
(->rs [this] (transient []))
|
||||
(with-row [this mrs row]
|
||||
(conj! mrs row))
|
||||
(rs! [this mrs] (persistent! mrs)))
|
||||
|
||||
(defn my-builder
|
||||
[rs opts]
|
||||
(let [rsmeta (.getMetaData rs)
|
||||
cols (rs/get-unqualified-column-names rsmeta opts)]
|
||||
(def rs rs)
|
||||
(def meta rsmeta)
|
||||
(def cols cols)
|
||||
(->MyMapResultSetBuilder rs rsmeta cols)))
|
||||
(.getColumnType meta 3)
|
||||
|
||||
(let [ds (jdbc/get-datasource (pg-url "souk"))]
|
||||
(jdbc/execute-one! ds [(sql 'select '* 'from :activitystreams/Person)]
|
||||
|
||||
{:builder-fn my-builder})
|
||||
)
|
||||
|
||||
|
||||
(pg-coerce (java.time.ZonedDateTime/parse "2017-04-11T00:00Z"))
|
||||
|
||||
{:activitystreams/followers "@id",
|
||||
:activitystreams/published "xsd:dateTime",
|
||||
:mastodon/devices "@id",
|
||||
:json-ld/type nil,
|
||||
:activitystreams/outbox "@id",
|
||||
:activitystreams/following "@id",
|
||||
:activitystreams/endpoints "@id",
|
||||
:activitystreams/name nil,
|
||||
:activitystreams/icon "@id",
|
||||
:security/publicKey "@id",
|
||||
:mastodon/featured "@id",
|
||||
:activitystreams/manuallyApprovesFollowers nil,
|
||||
:activitystreams/summary nil,
|
||||
:activitystreams/image "@id",
|
||||
:activitystreams/tag "@id",
|
||||
:mastodon/discoverable nil,
|
||||
:json-ld/id nil,
|
||||
:activitystreams/preferredUsername nil,
|
||||
:activitystreams/url "@id",
|
||||
:ldp/inbox "@id",
|
||||
:activitystreams/attachment "@id",
|
||||
:mastodon/featuredTags "@id"}
|
47
repl_sessions/rsa_keys.clj
Normal file
47
repl_sessions/rsa_keys.clj
Normal file
|
@ -0,0 +1,47 @@
|
|||
(ns repl-sessions.rsa-keys
|
||||
(:require
|
||||
[clojure.string :as str])
|
||||
(:import
|
||||
(java.security KeyPairGenerator Signature)
|
||||
(java.security.spec X509EncodedKeySpec)
|
||||
(java.util Base64)))
|
||||
|
||||
(def kpg (KeyPairGenerator/getInstance "RSA"))
|
||||
|
||||
(.initialize kpg 2048)
|
||||
(def kp (.generateKeyPair kpg))
|
||||
|
||||
(.getEncoded (.getPublic kp))
|
||||
(.getEncoded (.getPrivate kp))
|
||||
|
||||
(let [s (.encodeToString (Base64/getEncoder) (.getEncoded (.getPublic kp)))
|
||||
parts (map (partial apply str) (partition-all 64 s))]
|
||||
(str/join
|
||||
(map #(str % "\r\n")
|
||||
`["-----BEGIN PUBLIC KEY-----"
|
||||
~@parts
|
||||
"-----END PUBLIC KEY-----"])))
|
||||
|
||||
(def pem "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy3WsUuEyZLsy/2XxJ+ou\r\nnNr14R1x9laQh4EitjT4e1OPJwHHIBqEPUWk4MQzU13Jga4uua28Ecl3BxC9lSnf\r\nDp96Z0NAdkYjuCgC9xo9EjKaK8ijIbm58d4uifIl/XKZE6tYTGXXzmnx4nCfcWfF\r\n67tut/4k+/wVMjjHMLl9VhzHsBz3Wr+h7v+4SLFftq9NorMknWQuIh3IzQUNZBps\r\nCw8JRDUx8Of/I44mJMc2N12f41TLK65VCvkXF3K5qIS9jTEdhhOA8dsB92DEyaTu\r\ns+jhqXM4ivFfxDyOasQRZ0bEO+OEcJua7nnvNsFzGLkIb3/eJ1HlCQ+AKVSUGcBZ\r\nbwIDAQAB\r\n-----END PUBLIC KEY-----\r\n")
|
||||
|
||||
|
||||
|
||||
(X509EncodedKeySpec.
|
||||
(.decode (Base64/getDecoder)
|
||||
(str/replace pem #"(-+(BEGIN|END) PUBLIC KEY-+|\R)" ""))
|
||||
)
|
||||
|
||||
;; sign
|
||||
(def sign (Signature/getInstance "SHA256withRSA"))
|
||||
(.initSign sign (.getPrivate kp))
|
||||
(.update sign (.getBytes "hello"))
|
||||
(def signature (.sign sign))
|
||||
|
||||
(.encodeToString (Base64/getEncoder) signature)
|
||||
|
||||
;; verify
|
||||
(def sign (Signature/getInstance "SHA256withRSA"))
|
||||
(.initVerify sign (.getPublic kp))
|
||||
|
||||
(.update sign (.getBytes "hello"))
|
||||
(.verify sign signature)
|
3436
repl_sessions/vocabulary.clj
Normal file
3436
repl_sessions/vocabulary.clj
Normal file
File diff suppressed because it is too large
Load diff
198
resources/lambdaisland/souk/json_ld_contexts.edn
Normal file
198
resources/lambdaisland/souk/json_ld_contexts.edn
Normal file
|
@ -0,0 +1,198 @@
|
|||
{"https://www.w3.org/ns/activitystreams"
|
||||
{"Dislike" "as:Dislike",
|
||||
"Leave" "as:Leave",
|
||||
"Application" "as:Application",
|
||||
"Listen" "as:Listen",
|
||||
"followers" {"@id" "as:followers", "@type" "@id"},
|
||||
"startIndex"
|
||||
{"@id" "as:startIndex", "@type" "xsd:nonNegativeInteger"},
|
||||
"View" "as:View",
|
||||
"inbox" {"@id" "ldp:inbox", "@type" "@id"},
|
||||
"object" {"@id" "as:object", "@type" "@id"},
|
||||
"Like" "as:Like",
|
||||
"shares" {"@id" "as:shares", "@type" "@id"},
|
||||
"nameMap" {"@id" "as:name", "@container" "@language"},
|
||||
"width" {"@id" "as:width", "@type" "xsd:nonNegativeInteger"},
|
||||
"Relationship" "as:Relationship",
|
||||
"origin" {"@id" "as:origin", "@type" "@id"},
|
||||
"Link" "as:Link",
|
||||
"url" {"@id" "as:url", "@type" "@id"},
|
||||
"bto" {"@id" "as:bto", "@type" "@id"},
|
||||
"inReplyTo" {"@id" "as:inReplyTo", "@type" "@id"},
|
||||
"next" {"@id" "as:next", "@type" "@id"},
|
||||
"ldp" "http://www.w3.org/ns/ldp#",
|
||||
"signClientKey" {"@id" "as:signClientKey", "@type" "@id"},
|
||||
"CollectionPage" "as:CollectionPage",
|
||||
"describes" {"@id" "as:describes", "@type" "@id"},
|
||||
"anyOf" {"@id" "as:anyOf", "@type" "@id"},
|
||||
"Organization" "as:Organization",
|
||||
"OrderedCollection" "as:OrderedCollection",
|
||||
"orderedItems"
|
||||
{"@id" "as:items", "@type" "@id", "@container" "@list"},
|
||||
"Announce" "as:Announce",
|
||||
"OrderedCollectionPage" "as:OrderedCollectionPage",
|
||||
"height" {"@id" "as:height", "@type" "xsd:nonNegativeInteger"},
|
||||
"Note" "as:Note",
|
||||
"formerType" {"@id" "as:formerType", "@type" "@id"},
|
||||
"Offer" "as:Offer",
|
||||
"Video" "as:Video",
|
||||
"Object" "as:Object",
|
||||
"Travel" "as:Travel",
|
||||
"Mention" "as:Mention",
|
||||
"image" {"@id" "as:image", "@type" "@id"},
|
||||
"Audio" "as:Audio",
|
||||
"IntransitiveActivity" "as:IntransitiveActivity",
|
||||
"endpoints" {"@id" "as:endpoints", "@type" "@id"},
|
||||
"bcc" {"@id" "as:bcc", "@type" "@id"},
|
||||
"Flag" "as:Flag",
|
||||
"longitude" {"@id" "as:longitude", "@type" "xsd:float"},
|
||||
"Question" "as:Question",
|
||||
"radius" {"@id" "as:radius", "@type" "xsd:float"},
|
||||
"Public" {"@id" "as:Public", "@type" "@id"},
|
||||
"Activity" "as:Activity",
|
||||
"IsMember" "as:IsMember",
|
||||
"id" "@id",
|
||||
"proxyUrl" {"@id" "as:proxyUrl", "@type" "@id"},
|
||||
"IsContact" "as:IsContact",
|
||||
"Event" "as:Event",
|
||||
"hreflang" "as:hreflang",
|
||||
"Block" "as:Block",
|
||||
"Person" "as:Person",
|
||||
"altitude" {"@id" "as:altitude", "@type" "xsd:float"},
|
||||
"sharedInbox" {"@id" "as:sharedInbox", "@type" "@id"},
|
||||
"latitude" {"@id" "as:latitude", "@type" "xsd:float"},
|
||||
"liked" {"@id" "as:liked", "@type" "@id"},
|
||||
"Arrive" "as:Arrive",
|
||||
"summary" "as:summary",
|
||||
"Delete" "as:Delete",
|
||||
"attachment" {"@id" "as:attachment", "@type" "@id"},
|
||||
"relationship" {"@id" "as:relationship", "@type" "@id"},
|
||||
"href" {"@id" "as:href", "@type" "@id"},
|
||||
"name" "as:name",
|
||||
"closed" {"@id" "as:closed", "@type" "xsd:dateTime"},
|
||||
"vcard" "http://www.w3.org/2006/vcard/ns#",
|
||||
"Article" "as:Article",
|
||||
"tag" {"@id" "as:tag", "@type" "@id"},
|
||||
"published" {"@id" "as:published", "@type" "xsd:dateTime"},
|
||||
"items" {"@id" "as:items", "@type" "@id"},
|
||||
"startTime" {"@id" "as:startTime", "@type" "xsd:dateTime"},
|
||||
"location" {"@id" "as:location", "@type" "@id"},
|
||||
"Update" "as:Update",
|
||||
"Add" "as:Add",
|
||||
"Read" "as:Read",
|
||||
"context" {"@id" "as:context", "@type" "@id"},
|
||||
"partOf" {"@id" "as:partOf", "@type" "@id"},
|
||||
"Remove" "as:Remove",
|
||||
"preferredUsername" "as:preferredUsername",
|
||||
"Profile" "as:Profile",
|
||||
"totalItems"
|
||||
{"@id" "as:totalItems", "@type" "xsd:nonNegativeInteger"},
|
||||
"prev" {"@id" "as:prev", "@type" "@id"},
|
||||
"Follow" "as:Follow",
|
||||
"IsFollowing" "as:IsFollowing",
|
||||
"Tombstone" "as:Tombstone",
|
||||
"subject" {"@id" "as:subject", "@type" "@id"},
|
||||
"Page" "as:Page",
|
||||
"@vocab" "_:",
|
||||
"current" {"@id" "as:current", "@type" "@id"},
|
||||
"content" "as:content",
|
||||
"units" "as:units",
|
||||
"Place" "as:Place",
|
||||
"instrument" {"@id" "as:instrument", "@type" "@id"},
|
||||
"Undo" "as:Undo",
|
||||
"alsoKnownAs" {"@id" "as:alsoKnownAs", "@type" "@id"},
|
||||
"duration" {"@id" "as:duration", "@type" "xsd:duration"},
|
||||
"last" {"@id" "as:last", "@type" "@id"},
|
||||
"rel" "as:rel",
|
||||
"source" "as:source",
|
||||
"TentativeReject" "as:TentativeReject",
|
||||
"type" "@type",
|
||||
"outbox" {"@id" "as:outbox", "@type" "@id"},
|
||||
"mediaType" "as:mediaType",
|
||||
"oneOf" {"@id" "as:oneOf", "@type" "@id"},
|
||||
"deleted" {"@id" "as:deleted", "@type" "xsd:dateTime"},
|
||||
"target" {"@id" "as:target", "@type" "@id"},
|
||||
"replies" {"@id" "as:replies", "@type" "@id"},
|
||||
"provideClientKey" {"@id" "as:provideClientKey", "@type" "@id"},
|
||||
"Create" "as:Create",
|
||||
"updated" {"@id" "as:updated", "@type" "xsd:dateTime"},
|
||||
"generator" {"@id" "as:generator", "@type" "@id"},
|
||||
"endTime" {"@id" "as:endTime", "@type" "xsd:dateTime"},
|
||||
"TentativeAccept" "as:TentativeAccept",
|
||||
"oauthAuthorizationEndpoint"
|
||||
{"@id" "as:oauthAuthorizationEndpoint", "@type" "@id"},
|
||||
"audience" {"@id" "as:audience", "@type" "@id"},
|
||||
"Service" "as:Service",
|
||||
"Image" "as:Image",
|
||||
"Accept" "as:Accept",
|
||||
"Document" "as:Document",
|
||||
"preview" {"@id" "as:preview", "@type" "@id"},
|
||||
"Invite" "as:Invite",
|
||||
"contentMap" {"@id" "as:content", "@container" "@language"},
|
||||
"Group" "as:Group",
|
||||
"oauthTokenEndpoint" {"@id" "as:oauthTokenEndpoint", "@type" "@id"},
|
||||
"uploadMedia" {"@id" "as:uploadMedia", "@type" "@id"},
|
||||
"to" {"@id" "as:to", "@type" "@id"},
|
||||
"accuracy" {"@id" "as:accuracy", "@type" "xsd:float"},
|
||||
"IsFollowedBy" "as:IsFollowedBy",
|
||||
"Reject" "as:Reject",
|
||||
"summaryMap" {"@id" "as:summary", "@container" "@language"},
|
||||
"Join" "as:Join",
|
||||
"Move" "as:Move",
|
||||
"as" "https://www.w3.org/ns/activitystreams#",
|
||||
"actor" {"@id" "as:actor", "@type" "@id"},
|
||||
"likes" {"@id" "as:likes", "@type" "@id"},
|
||||
"following" {"@id" "as:following", "@type" "@id"},
|
||||
"streams" {"@id" "as:streams", "@type" "@id"},
|
||||
"cc" {"@id" "as:cc", "@type" "@id"},
|
||||
"attributedTo" {"@id" "as:attributedTo", "@type" "@id"},
|
||||
"result" {"@id" "as:result", "@type" "@id"},
|
||||
"xsd" "http://www.w3.org/2001/XMLSchema#",
|
||||
"first" {"@id" "as:first", "@type" "@id"},
|
||||
"Collection" "as:Collection",
|
||||
"icon" {"@id" "as:icon", "@type" "@id"},
|
||||
"Ignore" "as:Ignore"},
|
||||
"https://w3id.org/security/v1"
|
||||
{"EncryptedMessage" "sec:EncryptedMessage",
|
||||
"dc" "http://purl.org/dc/terms/",
|
||||
"canonicalizationAlgorithm" "sec:canonicalizationAlgorithm",
|
||||
"owner" {"@id" "sec:owner", "@type" "@id"},
|
||||
"created" {"@id" "dc:created", "@type" "xsd:dateTime"},
|
||||
"signatureValue" "sec:signatureValue",
|
||||
"CryptographicKey" "sec:Key",
|
||||
"publicKeyPem" "sec:publicKeyPem",
|
||||
"iterationCount" "sec:iterationCount",
|
||||
"id" "@id",
|
||||
"publicKey" {"@id" "sec:publicKey", "@type" "@id"},
|
||||
"Ed25519Signature2018" "sec:Ed25519Signature2018",
|
||||
"publicKeyWif" "sec:publicKeyWif",
|
||||
"GraphSignature2012" "sec:GraphSignature2012",
|
||||
"creator" {"@id" "dc:creator", "@type" "@id"},
|
||||
"publicKeyBase58" "sec:publicKeyBase58",
|
||||
"cipherAlgorithm" "sec:cipherAlgorithm",
|
||||
"digestAlgorithm" "sec:digestAlgorithm",
|
||||
"LinkedDataSignature2015" "sec:LinkedDataSignature2015",
|
||||
"cipherData" "sec:cipherData",
|
||||
"privateKey" {"@id" "sec:privateKey", "@type" "@id"},
|
||||
"EcdsaKoblitzSignature2016" "sec:EcdsaKoblitzSignature2016",
|
||||
"expires" {"@id" "sec:expiration", "@type" "xsd:dateTime"},
|
||||
"signatureAlgorithm" "sec:signingAlgorithm",
|
||||
"signature" "sec:signature",
|
||||
"domain" "sec:domain",
|
||||
"LinkedDataSignature2016" "sec:LinkedDataSignature2016",
|
||||
"revoked" {"@id" "sec:revoked", "@type" "xsd:dateTime"},
|
||||
"encryptionKey" "sec:encryptionKey",
|
||||
"cipherKey" "sec:cipherKey",
|
||||
"salt" "sec:salt",
|
||||
"digestValue" "sec:digestValue",
|
||||
"type" "@type",
|
||||
"password" "sec:password",
|
||||
"expiration" {"@id" "sec:expiration", "@type" "xsd:dateTime"},
|
||||
"publicKeyService" {"@id" "sec:publicKeyService", "@type" "@id"},
|
||||
"nonce" "sec:nonce",
|
||||
"authenticationTag" "sec:authenticationTag",
|
||||
"privateKeyPem" "sec:privateKeyPem",
|
||||
"sec" "https://w3id.org/security#",
|
||||
"normalizationAlgorithm" "sec:normalizationAlgorithm",
|
||||
"initializationVector" "sec:initializationVector",
|
||||
"xsd" "http://www.w3.org/2001/XMLSchema#"}}
|
19
src/lambdaisland/souk/activitypub.clj
Normal file
19
src/lambdaisland/souk/activitypub.clj
Normal file
|
@ -0,0 +1,19 @@
|
|||
(ns lambdaisland.souk.activitypub
|
||||
(:require [lambdaisland.souk.json-ld :as ld]))
|
||||
|
||||
(set! *print-namespace-maps* false)
|
||||
|
||||
(def common-prefixes
|
||||
{"dcterms" "http://purl.org/dc/terms/"
|
||||
"ldp" "http://www.w3.org/ns/ldp#"
|
||||
"schema" "http://schema.org/"
|
||||
"vcard" "http://www.w3.org/2006/vcard/ns#"
|
||||
"mastodon" "http://joinmastodon.org/ns#"
|
||||
"security" "https://w3id.org/security#"
|
||||
"activitystreams" "https://www.w3.org/ns/activitystreams#"
|
||||
"xsd" "http://www.w3.org/2001/XMLSchema#"
|
||||
"owl" "http://www.w3.org/2002/07/owl#"
|
||||
"rdfs" "http://www.w3.org/2000/01/rdf-schema#"})
|
||||
|
||||
(defn GET [url]
|
||||
(ld/internalize (ld/expand (:body (ld/json-get url))) common-prefixes))
|
150
src/lambdaisland/souk/json_ld.clj
Normal file
150
src/lambdaisland/souk/json_ld.clj
Normal file
|
@ -0,0 +1,150 @@
|
|||
(ns lambdaisland.souk.json-ld
|
||||
(:require [hato.client :as hato]
|
||||
[clojure.string :as str]
|
||||
[clojure.walk :as walk]))
|
||||
|
||||
(defn json-get [url]
|
||||
(hato/get url
|
||||
{:headers {"Accept" "application/json"}
|
||||
:http-client {:redirect-policy :normal}
|
||||
:as :json-string-keys}))
|
||||
|
||||
(def context-cache (atom {})) ;; url -> context
|
||||
|
||||
(defn fetch-context [url]
|
||||
(if-let [ctx (get @context-cache url)]
|
||||
ctx
|
||||
(get
|
||||
(swap! context-cache
|
||||
(fn [cache]
|
||||
(assoc cache url (get (:body (json-get url)) "@context"))))
|
||||
url)))
|
||||
|
||||
(defn expand-context
|
||||
([new-context]
|
||||
(expand-context {} new-context))
|
||||
([current-context new-context]
|
||||
(cond
|
||||
(string? new-context)
|
||||
(expand-context current-context (fetch-context new-context))
|
||||
|
||||
(sequential? new-context)
|
||||
(reduce expand-context current-context new-context)
|
||||
|
||||
(map? new-context)
|
||||
(into current-context
|
||||
(map
|
||||
(fn [[k v]]
|
||||
(let [id (if (map? v) (get v "@id" v) v)
|
||||
[prefix suffix] (str/split id #":")]
|
||||
(if-let [base (get (merge current-context new-context) prefix)]
|
||||
[k (assoc (if (map? v) v {})
|
||||
"@id" (str (if (map? base) (get base "@id") base)
|
||||
suffix))]
|
||||
[k (if (map? v) v {"@id" v})]))))
|
||||
new-context))))
|
||||
|
||||
(defn expand-id [id ctx]
|
||||
(if (string? id)
|
||||
(if-let [t (get-in ctx [id "@id"])]
|
||||
t
|
||||
(if (str/includes? id ":")
|
||||
(let [[prefix suffix] (str/split id #":")]
|
||||
(if-let [prefix-url (get-in ctx [prefix "@id"])]
|
||||
(str prefix-url suffix)
|
||||
id))
|
||||
id))
|
||||
id))
|
||||
|
||||
(defn apply-context [v ctx]
|
||||
(cond
|
||||
(map? v)
|
||||
(into {}
|
||||
(map (fn [[k v]]
|
||||
(let [attr (get ctx k)
|
||||
k (get-in ctx [k "@id"] k)
|
||||
v (apply-context v ctx)]
|
||||
[k (if attr
|
||||
(cond
|
||||
(and (#{"@id" "@type"} k) (string? v))
|
||||
(expand-id v ctx)
|
||||
(and (= "@id" (get attr "@type")) (string? v))
|
||||
(assoc attr "@id" (expand-id v ctx))
|
||||
:else
|
||||
(assoc (dissoc (cond-> attr
|
||||
(contains? attr "@type")
|
||||
(update "@type" expand-id ctx))
|
||||
"@id")
|
||||
"@value" v))
|
||||
v)])))
|
||||
v)
|
||||
|
||||
(sequential? v)
|
||||
(into (empty v) (map #(apply-context % ctx)) v)
|
||||
|
||||
(and (string? v) (str/includes? v ":"))
|
||||
(expand-id v ctx)
|
||||
|
||||
:else
|
||||
v))
|
||||
|
||||
(defn expand [json-ld]
|
||||
(let [ctx (expand-context {} (get json-ld "@context"))]
|
||||
(apply-context (dissoc json-ld "@context") ctx)))
|
||||
|
||||
(defn internalize [v prefixes]
|
||||
(let [shorten #(if (and (string? %) (str/includes? % "://"))
|
||||
(or (some (fn [[ns url]]
|
||||
(when (.startsWith % url)
|
||||
(keyword ns
|
||||
(subs % (.length url)))))
|
||||
prefixes)
|
||||
%)
|
||||
%)]
|
||||
(cond
|
||||
(sequential? v)
|
||||
(into (empty v) (map #(internalize % prefixes)) v)
|
||||
|
||||
(map? v)
|
||||
(-> v
|
||||
(cond-> (contains? v "@type")
|
||||
(doto prn)
|
||||
(contains? v "@type")
|
||||
(update "@type" shorten))
|
||||
(update-keys (fn [k]
|
||||
(case k
|
||||
"@id" :rdf/id
|
||||
"@type" :rdf/type
|
||||
"@value" :rdf/value
|
||||
(if-let [kw (shorten k)]
|
||||
kw
|
||||
k))))
|
||||
(update-vals (fn [v]
|
||||
(cond
|
||||
(map? v)
|
||||
(let [{id "@id" type "@type" value "@value"} v]
|
||||
(cond
|
||||
(and (= "@id" type) (contains? v "@id"))
|
||||
id
|
||||
(contains? v "@value")
|
||||
(case type
|
||||
"http://www.w3.org/2001/XMLSchema#dateTime"
|
||||
(java.time.ZonedDateTime/parse value)
|
||||
(internalize value prefixes))
|
||||
:else
|
||||
(internalize v prefixes)))
|
||||
|
||||
(string? v)
|
||||
(shorten v)
|
||||
|
||||
:else
|
||||
(internalize v prefixes)))))
|
||||
:else
|
||||
v)))
|
||||
|
||||
;; (compact
|
||||
;; (expand (:body (json-get "https://toot.cat/users/plexus")))
|
||||
;; common-prefixes)
|
||||
|
||||
;; "name"
|
||||
;; "profileName"
|
Loading…
Reference in a new issue