2 from datetime 
import datetime 
as dt
, timezone
 
   4 from itertools 
import starmap
 
   7 from os
.path 
import exists
 
   8 from pathlib 
import Path
 
  10 from shutil 
import copy2
, rmtree
 
  11 from subprocess 
import run
 
  13 from warnings 
import warn
 
  14 from xml
.dom 
import XHTML_NAMESPACE
 
  15 from xml
.dom
.minidom 
import getDOMImplementation
, parseString
 
  17 GIT_DIRECTORY 
= "/home/USERNAME/Status.git" 
  18 BUILD_DIRECTORY 
= "/home/USERNAME/status.site.example/.build" 
  19 PUBLIC_DIRECTORY 
= "/home/USERNAME/status.site.example/public" 
  20 PUBLIC_URL 
= "https://status.site.example" 
  25 CURRENT_DATETIME 
= f
"{dt.now(UTC).replace(tzinfo=None).isoformat(timespec='seconds')}Z" 
  26 ATOM_NAMESPACE 
= "http://www.w3.org/2005/Atom" 
  28 if stdin
.read().split()[-1] == f
"refs/heads/{LIVE_BRANCH}": 
  30         print(f
"This is an update to the '{LIVE_BRANCH}' branch; regenerating site…") 
  32         # Set up the build directory. 
  33         if exists(BUILD_DIRECTORY
): 
  34                 rmtree(BUILD_DIRECTORY
) 
  35         cloneresult 
= run(["git", "clone", "--local", "--branch", LIVE_BRANCH
, GIT_DIRECTORY
, BUILD_DIRECTORY
], capture_output
=True, encoding
="utf-8") 
  36         # if cloneresult.stderr: 
  37                 # print(cloneresult.stderr) 
  38         cloneresult
.check_returncode() 
  40         # Set up various containers. 
  44         # Create an XML representation of the provided status text. 
  45         def statusxml (text
, version
="1.0"): 
  46                 doc 
= getDOMImplementation().createDocument(None, "article", None) 
  47                 articleElt 
= doc
.documentElement
 
  48                 articleElt
.setAttribute("xmlns", XHTML_NAMESPACE
) 
  49                 articleElt
.setAttribute("lang", LANG
) 
  50                 for para 
in text
.split("\n\n"): 
  51                         paraElt 
= articleElt
.appendChild(doc
.createElement("p")) 
  52                         for component 
in re
.findall(r
'<[a-z]+:[^\s]*>(?:="[^\n"]+")?|\n|[^<\n]+|<(?![a-z]+:[^\s]*>)', para
): 
  54                                         paraElt
.appendChild(doc
.createElement("br")) 
  55                                 elif re
.fullmatch(r
'<[a-z]+:[^\s]*>(?:="[^\n"]+")?', component
): 
  56                                         href 
= component
.split(">", maxsplit
=1)[0][1:] 
  57                                         anchorElt 
= paraElt
.appendChild(doc
.createElement("a")) 
  58                                         anchorElt
.setAttribute("href", href
) 
  59                                         anchorElt
.setAttribute("rel", "noreferrer") 
  60                                         anchorElt
.appendChild(doc
.createTextNode(component 
if len(href
) == len(component
) - 2 else component
[len(href
)+4:-1])) 
  62                                         paraElt
.appendChild(doc
.createTextNode(component
)) 
  63                 return articleElt
.toxml() 
  65         # Map status paths to status objects, or None if there is an error. 
  67         # The provided path must be to a `text` object. 
  68         def statusmap (topic
, path
): 
  69                 status 
= { "@type": "MicroblogPost" }
 
  70                 version_path 
= next(path
.parent
.glob("0=*")) 
  71                 if version_path 
and version_path
.name 
!= "0=x_status_git_1.0": 
  72                         warn(f
"Unrecognized version for {path}; skipping.") 
  75                         status
["subject"] = topic
 
  76                 author_path 
= next(path
.parent
.glob("1=*")) 
  78                         status
["author"] = { "name": author_path.name[2:] }
 
  79                         with author_path
.open("r", encoding
="utf-8") as text
: 
  80                                 status
["author"]["@id"] = text
.read().strip() 
  81                 title_path 
= next(path
.parent
.glob("2=*")) 
  83                         with title_path
.open("r", encoding
="utf-8") as text
: 
  84                                 title 
= text
.read().strip() 
  85                                 status
["title"] = title
 
  86                 date_path 
= next(path
.parent
.glob("3=*")) 
  89                         with date_path
.open("r", encoding
="utf-8") as text
: 
  90                                 datetime 
= text
.read().strip() 
  91                                 status
["created"] = datetime
 
  93                         warn(f
"Missing date for {path}; skipping.") 
  95                 identifier_path 
= next(path
.parent
.glob("4=*")) 
  98                         identifier 
= identifier_path
.name
[2:] 
  99                         status
["@id"] = f
"{PUBLIC_URL}/topics/{topic}/{identifier}" if topic 
else f
"{PUBLIC_URL}/statuses/{datetime[0:7]}/{identifier}" 
 100                         with identifier_path
.open("r", encoding
="utf-8") as text
: 
 101                                 status
["identifier"] = text
.read().strip() 
 103                         warn(f
"Missing identifier for {path}; skipping.") 
 105                 with path
.open("r", encoding
="utf-8") as text
: 
 106                         status
["content"] = statusxml(text
.read().strip()) 
 107                 return (datetime
, identifier
, status
) 
 110                 doc 
= getDOMImplementation().createDocument(None, "feed", None) 
 111                 atomElt 
= doc
.documentElement
 
 112                 atomElt
.setAttribute("xmlns", ATOM_NAMESPACE
) 
 113                 atomElt
.setAttribute("xml:lang", LANG
) 
 114                 subject 
= ld
["subject"] if "subject" in ld 
else "Statuses" 
 115                 titleElt 
= atomElt
.appendChild(doc
.createElement("title")) 
 116                 titleElt
.appendChild(doc
.createTextNode(f
"{subject} @ {PUBLIC_URL}")) 
 117                 updatedElt 
= atomElt
.appendChild(doc
.createElement("updated")) 
 118                 updatedElt
.appendChild(doc
.createTextNode(CURRENT_DATETIME
)) 
 119                 generatorElt 
= atomElt
.appendChild(doc
.createElement("generator")) 
 120                 generatorElt
.appendChild(doc
.createTextNode("x_status_git")) 
 121                 generatorElt
.setAttribute("uri", "https://git.ladys.computer/x_status_git") 
 123                 if "OrderedCollectionPage" in ld
["@type"]: 
 124                         idElt 
= atomElt
.appendChild(doc
.createElement("id")) 
 125                         idElt
.appendChild(doc
.createTextNode(f
"{PUBLIC_URL}/statuses")) 
 126                         atomLinks
["alternate"] = f
"{PUBLIC_URL}/statuses" 
 127                         atomLinks
["current"] = f
"{PUBLIC_URL}/statuses.atom" 
 128                         atomLinks
["self"] = atomLinks
["current"] if ld
["@id"] == ld
["current"] else f
"{ld['@id']}.atom" 
 130                                 atomLinks
["prev-archive"] = f
"{ld['prev']}.atom" 
 131                         if "next" in ld 
and ld
["next"] != ld
["current"]: 
 132                                 atomLinks
["next-archive"] = f
"{ld['next']}.atom" 
 134                         idElt 
= atomElt
.appendChild(doc
.createElement("id")) 
 135                         idElt
.appendChild(doc
.createTextNode(ld
["@id"])) 
 136                         atomLinks
["alternate"] = ld
["@id"] 
 137                         atomLinks
["self"] = f
"{ld['@id']}.atom" 
 138                 for (rel
, href
) in atomLinks
.items(): 
 139                         linkElt 
= atomElt
.appendChild(doc
.createElement("link")) 
 140                         linkElt
.setAttribute("rel", rel
) 
 141                         linkElt
.setAttribute("href", href
) 
 142                 for item 
in ld
["items"]: 
 143                         entryElt 
= atomElt
.appendChild(doc
.createElement("entry")) 
 144                         title 
= item
["title"] if "title" in item 
else item
["content"][0:27] + "…" 
 145                         titleElt 
= entryElt
.appendChild(doc
.createElement("title")) 
 146                         titleElt
.appendChild(doc
.createTextNode(title
)) 
 147                         idElt 
= entryElt
.appendChild(doc
.createElement("id")) 
 148                         idElt
.appendChild(doc
.createTextNode(item
["@id"])) 
 149                         updatedElt 
= entryElt
.appendChild(doc
.createElement("updated")) 
 150                         updatedElt
.appendChild(doc
.createTextNode(CURRENT_DATETIME
)) 
 151                         if "created" in item
: 
 152                                 publishedElt 
= entryElt
.appendChild(doc
.createElement("published")) 
 153                                 publishedElt
.appendChild(doc
.createTextNode(item
["created"])) 
 154                         authorElt 
= entryElt
.appendChild(doc
.createElement("author")) 
 156                                 nameElt 
= authorElt
.appendChild(doc
.createElement("name")) 
 157                                 nameElt
.appendChild(doc
.createTextNode(item
["author"]["name"])) 
 158                                 uriElt 
= authorElt
.appendChild(doc
.createElement("uri")) 
 159                                 uriElt
.appendChild(doc
.createTextNode(item
["author"]["@id"])) 
 161                                 nameElt 
= authorElt
.appendChild(doc
.createElement("name")) 
 162                                 nameElt
.appendChild(doc
.createTextNode("Anonymous")) 
 163                         contentElt 
= entryElt
.appendChild(doc
.createElement("content")) 
 164                         contentElt
.setAttribute("type", "xhtml") 
 165                         contentDiv 
= contentElt
.appendChild(doc
.createElement("div")) 
 166                         contentDiv
.setAttribute("xmlns", XHTML_NAMESPACE
) 
 167                         contentDiv
.setAttribute("lang", LANG
) 
 168                         for child 
in list(parseString(item
["content"]).documentElement
.childNodes
): 
 169                                 contentDiv
.appendChild(child
) 
 170                 return (atomLinks
["self"], atomElt
.toxml()) 
 174         for yearpath 
in Path(f
"{BUILD_DIRECTORY}/").glob("[0-9][0-9][0-9][0-9]"): 
 175                 for monthpath 
in yearpath
.glob("[0-9][0-9]"): 
 176                         for daypath 
in monthpath
.glob("[0-9][0-9]"): 
 177                                 for statuspath 
in daypath
.glob("*/text"): 
 178                                         status_paths
.append((None, statuspath
)) 
 179         for topicpath 
in Path(f
"{BUILD_DIRECTORY}/").glob("topic/*"): 
 180                 for hash0path 
in topicpath
.glob("[0-9a-f]"): 
 181                         for hash1path 
in hash0path
.glob("[0-9a-f]"): 
 182                                 for hash2path 
in hash1path
.glob("[0-9a-f]"): 
 183                                         for hash3path 
in hash2path
.glob("[0-9a-f]"): 
 184                                                 for statuspath 
in hash3path
.glob("*/text"): 
 185                                                         status_paths
.append((topicpath
.name
, statuspath
)) 
 187         # Build status objects and listings. 
 188         for (datetime
, identifier
, status
) in sorted(filter(None, starmap(statusmap
, status_paths
))): 
 189                 if "subject" in status
: 
 190                         topic 
= status
["subject"] 
 191                         if topic 
not in topics
: 
 192                                 topics
[topic
] = { "@context": { "@language": LANG, "activity": "https://www.w3.org/ns/activitystreams#", "dct": "http://purl.org/dc/terms/", "foaf": "http://xmlns.com/foaf/0.1/", "sioc": "http://rdfs.org/sioc/ns#", "sioct": "http://rdfs.org/sioc/types#", "OrderedCollection": "activity:OrderedCollection", "Thread": "sioc:Thread", "MicroblogPost": "sioct:MicroblogPost", "items": { "@id": "activity:items", "@type": "@id", "@container": "@list" }
, "created": { "@id": "dct:created", "@type": "http://www.w3.org/2001/XMLSchema#dateTime" }
, "creator": { "@id": "dct:creator", "@type": "@id" }
, "identifier": { "@id":  "dct:identifier", "@type": "http://www.w3.org/2001/XMLSchema#anyURI" }
, "subject": "dct:subject", "name": "foaf:name", "title": "dct:title", "content": { "@id": "sioc:content", "@type": "http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral" } 
}, "@id": f
"{PUBLIC_URL}/topics/{topic}", "@type": ["OrderedCollection", "Thread"], "items": [], "subject": topic 
} 
 193                         topics
[topic
]["items"].append(status
) 
 195                         yyyy_mm 
= datetime
[0:7] 
 196                         if yyyy_mm 
not in months
: 
 197                                 months
[yyyy_mm
] = { "@context": { "@language": LANG, "activity": "https://www.w3.org/ns/activitystreams#", "dct": "http://purl.org/dc/terms/", "foaf": "http://xmlns.com/foaf/0.1/", "sioc": "http://rdfs.org/sioc/ns#", "sioct": "http://rdfs.org/sioc/types#", "OrderedCollectionPage": "activity:OrderedCollectionPage", "Thread": "sioc:Thread", "MicroblogPost": "sioct:MicroblogPost", "current": { "@id": "activity:current", "@type": "@id" }
, "first": { "@id": "activity:first", "@type": "@id" }
, "items": { "@id": "activity:items", "@type": "@id", "@container": "@list" }
, "partOf": { "@id": "activity:partOf", "@type": "@id" }
, "prev": { "@id": "activity:prev", "@type": "@id" }
, "next": { "@id": "activity:next", "@type": "@id" }
, "created": { "@id": "dct:created", "@type": "http://www.w3.org/2001/XMLSchema#dateTime" }
, "creator": { "@id": "dct:creator", "@type": "@id" }
, "identifier": { "@id":  "dct:identifier", "@type": "http://www.w3.org/2001/XMLSchema#anyURI" }
, "name": "foaf:name", "title": "dct:title", "content": { "@id": "sioc:content", "@type": "http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral" } 
}, "@id": f
"{PUBLIC_URL}/statuses/{yyyy_mm}", "@type": ["OrderedCollectionPage", "Thread"], "items": [], "partOf": f
"{PUBLIC_URL}/statuses" } 
 198                         months
[yyyy_mm
]["items"].append(status
) 
 200         # Set up the public directory. 
 201         if exists(PUBLIC_DIRECTORY
): 
 202                 rmtree(PUBLIC_DIRECTORY
) 
 203         mkdir(PUBLIC_DIRECTORY
) 
 205         # Copy H·T·M·L files to their expected locations. 
 206         copy2(f
"{BUILD_DIRECTORY}/index.html", f
"{PUBLIC_DIRECTORY}/index.html") 
 207         copy2(f
"{BUILD_DIRECTORY}/about.html", f
"{PUBLIC_DIRECTORY}/.about.html") 
 208         copy2(f
"{BUILD_DIRECTORY}/status.html", f
"{PUBLIC_DIRECTORY}/.status.html") 
 209         copy2(f
"{BUILD_DIRECTORY}/statuses.html", f
"{PUBLIC_DIRECTORY}/.statuses.html") 
 210         copy2(f
"{BUILD_DIRECTORY}/topic.html", f
"{PUBLIC_DIRECTORY}/.topic.html") 
 211         copy2(f
"{BUILD_DIRECTORY}/topics.html", f
"{PUBLIC_DIRECTORY}/.topics.html") 
 213         # Output “about” metadata 
 214         if not exists(f
"{PUBLIC_DIRECTORY}/about"): 
 215                 mkdir(f
"{PUBLIC_DIRECTORY}/about") 
 216         with open(f
"{PUBLIC_DIRECTORY}/about/index.jsonld", "w", encoding
="utf-8") as f
: 
 217                 json
.dump({ "@context": { "@language": LANG, "activity": "https://www.w3.org/ns/activitystreams#", "sioc": "http://rdfs.org/sioc/ns#", "sioct": "http://rdfs.org/sioc/types#", "Forum": "sioc:Forum", "Thread": "sioc:Thread", "Microblog": "sioct:Microblog", "streams": { "@id": "activity:streams", "@type": "@id" } 
}, "@id": f
"{PUBLIC_URL}", "@type": "Microblog", "streams": [{ "@id": f"{PUBLIC_URL}
/statuses
", "@type": "Thread
" }, { "@id": f"{PUBLIC_URL}/topics", "@type": "Forum" }] }, f
, ensure_ascii
=False, allow_nan
=False) 
 219         # Output month‐based listings and the non‐topic index 
 220         if not exists(f
"{PUBLIC_DIRECTORY}/statuses"): 
 221                 mkdir(f
"{PUBLIC_DIRECTORY}/statuses") 
 222         statuspairs 
= list(enumerate(months
.items())) 
 223         for (index
, (yyyy_mm
, ld
)) in statuspairs
: 
 224                 if not exists(f
"{PUBLIC_DIRECTORY}/statuses/{yyyy_mm}"): 
 225                         mkdir(f
"{PUBLIC_DIRECTORY}/statuses/{yyyy_mm}") 
 226                 ld
["first"] = f
"{PUBLIC_URL}/statuses/{statuspairs[0][1][0]}" 
 227                 ld
["current"] = f
"{PUBLIC_URL}/statuses/{statuspairs[-1][1][0]}" 
 229                         ld
["prev"] = f
"{PUBLIC_URL}/statuses/{statuspairs[index - 1][1][0]}" 
 230                 if index 
< len(statuspairs
) - 1: 
 231                         ld
["next"] = f
"{PUBLIC_URL}/statuses/{statuspairs[index + 1][1][0]}" 
 232                 with open(f
"{PUBLIC_DIRECTORY}/statuses/{yyyy_mm}/index.jsonld", "w", encoding
="utf-8") as f
: 
 233                         json
.dump(ld
, f
, ensure_ascii
=False, allow_nan
=False) 
 234                 atomlink
, atomxml 
= atomForLD(ld
) 
 235                 with open(f
"{PUBLIC_DIRECTORY}/{atomlink[len(PUBLIC_URL):-5]}/index.atom", "w", encoding
="utf-8") as f
: 
 237         with open(f
"{PUBLIC_DIRECTORY}/statuses/index.jsonld", "w", encoding
="utf-8") as f
: 
 238                 json
.dump({ "@context": { "@language": LANG, "activity": "https://www.w3.org/ns/activitystreams#", "sioc": "http://rdfs.org/sioc/ns#", "OrderedCollection": "activity:OrderedCollection", "Thread": "sioc:Thread", "current": { "@id": "activity:current", "@type": "@id" }
, "first": { "@id": "activity:first", "@type": "@id" }
, "has_parent": { "@id": "sioc:has_parent", "@type": "id" } 
}, "@id": f
"{PUBLIC_URL}/statuses", "@type": ["OrderedCollection", "Thread"], "first": f
"{PUBLIC_URL}/statuses/{statuspairs[0][1][0]}", "current": f
"{PUBLIC_URL}/statuses/{statuspairs[-1][1][0]}", "has_parent": f
"{PUBLIC_URL}" }, f
, ensure_ascii
=False, allow_nan
=False) 
 240         # Output topic‐based listings and the topic index 
 241         if not exists(f
"{PUBLIC_DIRECTORY}/topics"): 
 242                 mkdir(f
"{PUBLIC_DIRECTORY}/topics") 
 243         for (topic
, ld
) in topics
.items(): 
 244                 if not exists(f
"{PUBLIC_DIRECTORY}/topics/{topic}"): 
 245                         mkdir(f
"{PUBLIC_DIRECTORY}/topics/{topic}") 
 246                 with open(f
"{PUBLIC_DIRECTORY}/topics/{topic}/index.jsonld", "w", encoding
="utf-8") as f
: 
 247                         json
.dump(ld
, f
, ensure_ascii
=False, allow_nan
=False) 
 248                 atomlink
, atomxml 
= atomForLD(ld
) 
 249                 with open(f
"{PUBLIC_DIRECTORY}/{atomlink[len(PUBLIC_URL):-5]}/index.atom", "w", encoding
="utf-8") as f
: 
 251         with open(f
"{PUBLIC_DIRECTORY}/topics/index.jsonld", "w", encoding
="utf-8") as f
: 
 252                 json
.dump({ "@context": { "@language": LANG, "activity": "https://www.w3.org/ns/activitystreams#", "dct": "http://purl.org/dc/terms/", "sioc": "http://rdfs.org/sioc/ns#", "Collection": "activity:Collection", "Forum": "sioc:Forum", "items": { "@id": "activity:items", "@type": "@id" }
, "has_parent": { "@id": "sioc:has_parent", "@type": "id" }
, "subject": "dct:subject" }, "@id": f
"{PUBLIC_URL}/topics", "@type": ["Collection", "Forum"], "items": list(map(lambda a
: { "@id": a["@id"], "subject": a["subject"] }
, topics
.values())), "has_parent": f
"{PUBLIC_URL}" }, f
, ensure_ascii
=False, allow_nan
=False) 
 254         # Remove the build directory. 
 255         rmtree(BUILD_DIRECTORY
)