going-flying.com gemini git repository
6579d7412da733536ff6cedd08604c9022b6bf53 - Matthew Ernisse - 1595803382
I have not had this much fun in a while...
diff --git a/files/deploy b/files/deploy new file mode 100755 index 0000000..454270f --- /dev/null +++ b/files/deploy @@ -0,0 +1,74 @@ +#!/bin/sh +# going-flying.com post-receive hook for gemini:// +# (c) 2016-2020 Matthew J. Ernisse <matt@going-flying.com> +# All Rights Reserved. +# +# Update the on-disk representation of my capsule when I push a new +# revision up to the git repository. + +set -e + +BRANCH="" +BUILD_DIR="/var/gemini" +GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) +REV=0 + +while read oldrev newrev refname; do + BRANCH="$refname" + REV="$newrev" +done + +if [ "$BRANCH" != "refs/heads/master" ]; then + echo "detected $BRANCH, aborting" + exit 0 +fi + +if [ -z "$GIT_DIR" ]; then + echo >&2 "fatal: post-receive GIT_DIR not set" + exit 1 +fi + +cat << EOF + »^ \ + ƒ L + ▄▄▄▌ ⌐ ╒ + ██████b ¼ / ,╓▄▄▓█▌ + ▐██████▌ )Θ ╙└ ╟███▌ + ╫███████ ] ╕ Φ█▀██▓█ + ████████ ╞ b "▓ ▐█╙¬ + ████████ ╘ ] ╚▀ + ████████⌐ └ + ██████████ ╓┐ Γ + ▓█████████████ ,¬ + ▐███████████████ + ]█████████████████ + ████████████████▀╙ + ]██████████████████ + ╟██████████████████ + ╫██████████████████▌ + ████████████████████ + ▐████████████████████⌐ + █████████████████████▌ + ,██████████████████████ + ▐██████████████████████µ + ─└└└└└└└└└└└└└└└└└─ +EOF + +umask 0022 + +if [ ! -d "$BUILD_DIR" ]; then + echo "Creating $BUILD_DIR" + + mkdir -- "$BUILD_DIR" + chgrp www-data "$BUILD_DIR" + chmod 664 "$BUILD_DIR" +fi + +echo "updating $BUILD_DIR" +GIT_WORK_TREE=$BUILD_DIR git checkout -f + +echo "setting rev to $REV" +sed -e "s/GIT_REV/${REV}/" "$BUILD_DIR/index.gmi" > "$BUILD_DIR/index.gmi.new" +mv $BUILD_DIR/index.gmi.new $BUILD_DIR/index.gmi + +echo "site deployed." diff --git a/files/thoughts-to-gemini.py b/files/thoughts-to-gemini.py new file mode 100755 index 0000000..6c25365 --- /dev/null +++ b/files/thoughts-to-gemini.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +'''thoughts-to-gemini.py (c) 2020 Matthew J Ernisse <matt@going-flying.com> +All Rights Reserved. + +Redistribution and use in source and binary forms, +with or without modification, are permitted provided +that the following conditions are met: + + * Redistributions of source code must retain the + above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce + the above copyright notice, this list of conditions + and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +import datetime +import jinja2 +import json +import os +import pytz +import requests +import sys +import time + +from bs4 import BeautifulSoup + +entry_template = ''' +===>> [ {{ entry.date }} ] +> {{ entry.message }} + +{% if 'attachment' in entry.keys() %} + Attachments: +{% for attachment in entry.attachment %} +=> {{ attachment.name|urlencode }} {{ attachment.type }} +{% endfor %} + +{% endif %} +''' + +index_template = ''' +``` + _______ __ __ __ +|_ _|| |--..-----..--.--..-----.| |--.| |_ .-----. + | | | || _ || | || _ || || _||__ --| + |___| |__|__||_____||_____||___ ||__|__||____||_____| + |_____| +``` + +# 💭 Random Thoughts + +{% for year in thoughts.years %} +## {{ year }} + + {% for month in thoughts.byYear(year) %} +### {{ month }} + {% for entry in thoughts.forMonth(year, month) %} +{{ entry }} + {% endfor %} + {% endfor %} +{% endfor %} + +# 🕰️ Generated at {{ build_time }} + +''' + +class DeHTMLizer(object): + '''Converter for the lightweight Thoughts HTML into gemini's + markup language. + ''' + def __init__(self, s): + ''' Given a HTML string, convert it into text/gemini ''' + soup = BeautifulSoup(s, 'lxml') + self.gemini = '' + self.links = [] + + for el in soup.find('body').contents: + if el.name is not None: + self.gemini += self.parseTag(el) + + elif el.string is not None: + self.gemini += el.string + + def __str__(self): + if len(self.gemini) == 0: + return '~ NO MESSAGE ~' + + if len(self.links) == 0: + return self.gemini + + trailer = '\n\n' + for n, link in enumerate(self.links): + trailer += f'=> {link} [{n + 1}]\n' + + return self.gemini + trailer + + def parseTag(self, tag): + noprint = ['style', 'script'] + + if tag.name == 'a': + self.links.append(tag['href']) + num = len(self.links) + + return f'«{tag.string}[{num}]»' + + elif tag.name == 'blockquote': + return f'“{tag.string}”' + + elif tag.name in ['em', 'strong']: + return f'*{tag.string}*' + + elif tag.name in noprint: + return '' + + elif tag.string == None: + return '' + else: + return tag.string + + +class Thoughts(object): + ''' Render Thoughts from the API and save state to disk.''' + attachurl = 'https://thoughtsassets.blob.core.windows.net/thumbnails' + def __init__(self, thoughtdir): + self.api = ThoughtApi() + self.entries = [] + self.thoughtdir = thoughtdir + self.thoughts = [] + self._years = {} + + if not os.path.exists(thoughtdir): + raise ValueError('Dir does not exist') + + t_json = os.path.join(thoughtdir, 'thoughts.json') + if os.path.exists(t_json): + with open(t_json, 'r', encoding='utf-8') as fd: + self.thoughts = json.load(fd) + + self.thoughts.sort(key=lambda k: k['id'], reverse=True) + if len(self.thoughts) != 0: + local_newest = self.thoughts[0]['id'] + if self.api.newest > local_newest: + _t = ThoughtApi(local_newest) + self.thoughts.extend(_t.thoughts) + else: + _t = ThoughtApi() + self.thoughts = list(_t.thoughts) + + with open(t_json, 'w', encoding='utf-8') as fd: + json.dump( + self.thoughts, + fd, + ensure_ascii=False + ) + + self.tmpl = jinja2.Template( + entry_template, + trim_blocks=True, + lstrip_blocks=True + ) + + for thought in self.thoughts: + self._processThought(thought) + self._downloadAttachments(thoughtdir, thought) + + now = datetime.datetime.now(pytz.timezone('US/Eastern')) + tmpl = jinja2.Template( + index_template, + trim_blocks=True, + lstrip_blocks=True + ) + + outFile = os.path.join(thoughtdir, 'index.gmi') + with open(outFile, 'w', encoding='utf-8') as fd: + fd.write(tmpl.render({ + 'build_time': now.strftime('%c %z'), + 'thoughts': self + })) + + def _downloadAttachments(self, localdir, thought): + if 'attachment' not in thought: + return + + for a in thought['attachment']: + outFile = os.path.join(localdir, a['name']) + + if os.path.exists(outFile): + continue + + resp = requests.get(self.attachurl + '/' + a['name']) + resp.raise_for_status() + + with open(outFile, 'wb') as fd: + fd.write(resp.content) + + def _processThought(self, thought): + dt = datetime.datetime.utcfromtimestamp(thought['id']) + if dt.year not in self._years: + self._years[dt.year] = {} + + month = dt.strftime('%B') + if month not in self._years[dt.year]: + self._years[dt.year][month] = [] + + thought['message'] = DeHTMLizer(thought['message']) + self._years[dt.year][month].append( + self.tmpl.render(entry=thought) + ) + + @property + def years(self): + for year in self._years.keys(): + yield year + + def byYear(self, year): + return self._years[year].keys() + + def forMonth(self, year, month): + return self._years[year][month] + + +class ThoughtApi(object): + ''' Provide an interface to my Thoughts. ''' + imgUrl = 'https://thoughtsassets.blob.core.windows.net/thumbnails' + def __init__(self, since=0): + self.since = since + + @property + def newest(self): + ''' Return the ID of the newest thought. ''' + _t = self._get(1, before=int(time.time()))[0] + return _t['id'] + + @property + def oldest(self): + _t = self._get(1, since=0)[0] + return _t['id'] + + @property + def thoughts(self): + ''' Fetch the thoughts from the API and emit them. ''' + more = True + while more: + _t = self._getRange() + if len(_t) < 25: + more = False + + for thought in _t: + self.since = thought['id'] + yield thought + + def _get(self, count=25, before=None, since=None): + headers = {'User-Agent': 'thought-to-gemini/1.0'} + params = {'count': count} + + if before is not None: + params['before'] = before + + if since is not None: + params['since'] = since + + resp = requests.get( + 'https://vociferate.azurewebsites.net/api/get', + headers=headers, + params=params + ) + + resp.raise_for_status() + thoughts = resp.json() + thoughts.sort(key=lambda k: k['id']) + return thoughts + + def _getRange(self): + ''' Return a range of 25 thoughts from self.since. ''' + return self._get(since=self.since) + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print(f'Usage: {os.path.basename(sys.argv[0])} path') + print() + print('This will write all Thoughts to index.gmi at the given') + print('path and download all attachments there as well.') + sys.exit(1) + + localdir = sys.argv[1] + if not os.path.exists(localdir): + print(f'{localdir} does not exist or is not readable.') + sys.exit(1) + + Thoughts(localdir) diff --git a/how-built.gmi b/how-built.gmi new file mode 100644 index 0000000..18e1854 --- /dev/null +++ b/how-built.gmi @@ -0,0 +1,16 @@ +# How I am building this capsule +## Author: mernisse +## Date 26/July/2020 + +I hope it may be of some interest to someone, someday how the content in here is maintained. The funny thing is that in a lot of ways it is built extremely similarly to the HTTPS version of going-flying.com, which is to say it uses a git post-receive hook. + +=> https://www.going-flying.com/blog/first-post-part-two.html + +There are actually two parts here, part one is the git repository where all of the stuff I write by hand lives. This is deployed as described, by a post-receive hook which simply checks out the repository into /var/gemini which is mounted into the Molly Brown container that is running. In this way all the content just appears. The other piece is a Python script running out of cron that renders the contents of the Thoughts microblogging (ish) system I built on Azure Functions out to a text/gemini file. + +=> https://www.going-flying.com/blog/thoughts-on-azure-functions.html +=> /files/thoughts-to-gemini.py + +In the future I plan on building something resembling a blog here as well. I think the community is drifting towards ``glog'' but I suppose we'll see when it comes down to that. When I get to that point I imagine I'll end up building it similarly to my web blog, which is to say just create a directory full of text files with a metadata header and then generating the index page and the article pages from the git deploy script. + +=> /files/deploy diff --git a/index.gmi b/index.gmi index c1b5599..918193f 100644 --- a/index.gmi +++ b/index.gmi @@ -10,11 +10,13 @@ ``` # Welcome to going-flying.com 🛩️ -I have some plans to make more content available here, but for the moment there is really only this and a version of what you might call my microblog rendered into text/gemini. For the interested this capsule is running on geminid in a Docker container. +I have some plans to make more content available here, but for the moment there is really only this and a version of what you might call my microblog rendered into text/gemini. For the interested this capsule is running on Molly Brown in a Docker container, but started life as geminid in a Docker container. => /thoughts/ My microblog? Something like that. +=> /how-built.gmi How this capsule is built, vaguely. => https://www.going-flying.com HTTPS version of going-flying.com -=> https://ssl.ub3rgeek.net/git/?p=containers.git;a=tree;f=geminid;hb=HEAD Git repository for this container +=> https://ssl.ub3rgeek.net/git/?p=containers.git;a=tree;f=geminid;hb=HEAD Git repository for this geminid in a container +=> https://ssl.ub3rgeek.net/git/?p=containers.git;a=tree;f=mollybrown;hb=HEAD Git repository for this Molly Brown in a container => gemini://gemini.uxq.ch geminid Information => gemini://gemini.circumlunar.space Gemini Information