Story: Over here

To whomever Finds this,

I don’t know how long I have to write, for once, my brevity rather than my verbosity is sought. For you see, I am hunted by something unholy, and I don’t know how long it will let me write here at this desk. I hear the wail of the storm outside that brought this upon me and wonder why I’m spared. I only hope that this warning finds the next owner and that they can understand what happened here.

My story started rather joyously, for, you see, I had the good fortune of being in the company of the new Lord-Lieutenant of all of Ireland, and he had promised that, in exchange for my support in his settling the Kings local affairs, I would be granted lands and a house more splendid than anything I could have looked forward to with my standing in England.

Foolishly, I know now, I also took someone very dear with me on my journey, my grand daughter Felicia. She was, quite tragically, orphaned at age eight by a terrible business in York County that I would rather not go into. She had accompanied me for two years since, and I had enjoyed her youthful naiveté and playful inquisitive nature. In our exchanges I taught her of the high ranks of England and introduced her to many great families who she may one day hope to marry into. I didn’t care for too many servants to come between us, as the dear child being my only remaining family, was special to my sentiment and a cure for my ongoing progression in years. I would always remark to myself how grateful I was to be succeeded so graciously and fortunately by this one remaining heir.

But outside it is still ghastly, thinking of her now hurts so much, and I still do not know why I’m being allowed to continue to document what feels like a last testament. Perhaps the spirit is toying with me before it gets me as it has gotten the others…

…Yes, I should explain. I’m so very sorry about them. I journeyed from London to Drogheda with my three servants and my kin. My dearest friend, the Lord-Lieutenant, met with me for tea there, and we discussed our plans, oh so very many political plans, which I now not care much. The one which finds me here had us planning to allow a fellow Lord to have his way and build his new dock in Kinsale. The local millita was proving obstinate and unreasonable, even with requests from the Crown. My new job was to bribe or otherwise convince the locals of the British plans and further the strength in trade from the Americas. I was given this house here in Dunderrow to be close enough to have control but far enough to not be in danger of attack.

I moved in here last week and was just getting settled with Felicia enjoying the trouble of it all. I remember sending messages to the village to have new furnishings sent to more properly dress my new home here in Ireland.

It only took but five days to break the peace of the fine springtime with an unholy storm, which creped over the landscape, and from the north and upon this house it struck. The wind and the rain battered the windows angrily. Going outside was like entering a winter, even though the calendar would suggest pleasant warm showers, it was freezing and the foreboding dark from clouds even in the midst of the day did nothing for my mood.

I don’t say this candidly of course, at first I thought they were suicides since the nature of their demise would suggest it. One by one they ended themselves in the same spot on the grounds. Mrs Crawdford, my cook for fifteen years, was the last to walk with apparent calmness from the tallest part of the building in the howling wind and cold thunderous rain with myself calling out to cease. Her eyes filled with some other scene, and only the surrounding weather showing the emotion of the danger she was in. I was in terror that my dearest grand daughter would soon find herself calmly taking her life in repeat of what I had just seen. I quickly got together our things and decided to make way to the village despite the weather.

Calling after Felicia in the house left no reply. I searched where I knew she should be, but there was only the grand fire place still warming the only human part of the house, abandoned without struggle. Wrenching at the thought of the worst, I became unstable and grabbed at the mantle to steady myself with tears. I caught in the very mirror above the fire a series of fine cracks as they starting running over the glass before my eyes forming the terrible words: “your kin is to the land in debt and warning”

Shocked and upset, I must have succumbed right there as I brought myself up from the floor an hour after. I still can’t believe what I saw and what it meant for my dearest. All I know is that she is gone, the monsters of this land have taken her from me. That is how I come to write this, in anger at the land that took my happiness and in misery at my misfortune to be subjected so.

It’s only a matter of time before the awful wailing outside goes abroad to darken some other shores and I can quit this place forever, or either what malicious force caused my suffering will also cease it soon as I would welcome it. Only you who find this letter will know which.

CLt. Arthur Mercal

Originally written: 2011-11-30

Responsibility in Software

Pepper & carrot creator David Revoy has created a good blog post that goes into the problem that he’s personally had with the new release of Inkscape 0.92.

The issue with text and svg is actually kind of complex. It’s at the junction of specification, feature management and dealing with old formats. But it’s also a lot about how Free Software projects deal with users to a degree too.

This is because Inkscape is entirely volunteer driven, which means when Inkscape fails for us developers, only our pride is hurt. But actually out there in the big world there are real people who will be materially hurt by a bad inkscape release.

And my frustration is that there’s no serious Free Software way to connect developers to users in that essentially material way that binds them strongly. I’ve been banging the Money and Economics drum for A VERY LONG TIME, but fellow developers are just not interested in the idea that either Free Software could be a job of service instead of indulgence and that there really is a responsibility that we quite often neglect when we don’t have the right resources to deal with them properly.

This isn’t the case for all projects. Quite a few projects have key developers that manage to turn their pet project into a real full time job. OK so they’ll sometimes get some bias from their employer and the project can turn corporate, but that’s the trade off.

This is where the Inkscape projects really hits the wall. It’s a very big and useful project, that has an incredibly poor user to developer material binding. We need about 50 cents from every inkscape user to hire ten to twenty full time developers, managers and ancillary support. Of course the money would likely be bunched up into a few hands, but the project yearns to be in the greatest number of hands and not a few big players.

And maybe that’s the big barrier, a cultural one. Inkscape is built on the idea that all developers are equal and the project can be driven forwards in many directions by lots of developers at once.

I really wish I had some solutions. But given Inkscaoe’s current issues, I’m going to focus on actually fixing the issues we have and I’ll have to come back to how we solve the resources problem more fundamentally.

Science bootstrap glyph icons

I’m starting a new repository on GitHub which will be an svg based set of scientific icons.

This is just the start of making some free to use (CC-BY-SA 4.0), science based icons that will be compiled into a web font (ttf, oeff, svg etc) and provide a css file to easy drop in placement into many websites in the science fields.

A lot of the icons are meant as inaccurate depictions. The goal is to convey the general idea behind the button or status without having to have every proton in the right place.

If you’d like to help, there are instructions on the github page for contributions.

GPG with Confirmation

I got tired of Evolution email client giving me those horrid error messages when ever I try to email someone who’s key isn’t in my current list of keys.

The design of this is appallingly bad. It discourages the use of GPG rather than encouraging the importing of keys and it makes no mention of helping you acquire keys if possible. It also allows for no additional or optional footer to explain to the recipient that their message couldn’t be encrypted because they don’t use GPG.

While I couldn’t do much about the later, without hacking on the evolution codebase directly. I did do a bit of hacking on the former with a gpg middlware. Yes, when I say hack, I mean HACK. A dangerous and potentially devastating way of wrapping the gpg binary with my own python script that could intercept the evolution call and do work to search, display and add keys to encourage the use of encryption overall.

The design was simple. When we are asked to encrypt for a person who we don’t have the keys for, we do a search. The results are shown in a GUI to the user and they can select a key to use. This then is added to the key ring and used to encrypt the email.

This setup allows for experimentation with user prompting and workflow. It’s not something I would recommend be installed on user’s computers. But for designers and developers, this sort of match-stick making is a valuable platform to build, try, test and rebuild quickly.

I use zenity for the user interface. This is a Gtk command line tool that lets you launch a window from the command line and the interface is good enough to support photos in lists and returning which item was selected. Very cool.

Bellow you will find the script I created for this hack, this is saved to /usr/bin/gpg and gpg is moved to gpg.orig:

#!/usr/bin/python
#
# Wrap the gpg command to provide evolution with a bit of extra functionality
# This is certainly a hack and you should feel very bad about using it.
#
# Public Domain, Authored by Martin Owens  2016
#
import os
import sys
import atexit

from collections import defaultdict
from subprocess import Popen, PIPE, call
from tempfile import mkdtemp, mktemp
from datetime import date
from shutil import rmtree

to_date = lambda d: date(*[int(p) for p in d.split('-')])


class GPG(object):
    keyserver = 'hkp://pgp.mit.edu'
    remote_commands = ['--search-keys', '--recv-keys']

    def __init__(self, cmd='/usr/bin/gpg', local=False):
        self.command = cmd
        self.photos = []
        self.local = local
        self.homedir = mkdtemp() if local else None
        atexit.register(self.at_exit)

    def at_exit(self):
        """Remove any temporary files and cleanup"""
        # Clean up any used local home directory (only if it's local)
        if self.local and self.homedir and os.path.isdir(self.homedir):
            rmtree(self.homedir)

        # Clean up any downloaded photo-ids
        for photo in self.photos:
            if os.path.isfile(photo):
                os.unlink(photo)
            try:
                os.rmdir(os.path.dirname(photo))
            except OSError:
                pass

    def __call__(self, *args):
        """Call gpg command for result"""
        # Add key server if required
        if any([cmd in args for cmd in self.remote_commands]):
            args = ('--keyserver', self.keyserver) + args
        if self.homedir:
            args = ('--homedir', self.homedir) + args

        command = Popen([self.command, '--batch'] + list(args), stdout=PIPE)
        (out, err) = command.communicate()
        self.status = command.returncode
        return out

    def list_keys(self, *keys, **options):
        """Returns a list of keys (with photos if needed)"""
        with_photos = options.get('photos', False)
        args = ()
        if with_photos:
            args += ('--list-options', 'show-photos',
                     '--photo-viewer', 'echo PHOTO:%I')
        out = self(*(args + ('--list-keys',) + keys))

        # Processing the output with this parser
        units = []
        current = defaultdict(list)
        for line in out.split('\n'):
            if not line.strip():
                # We should always output entries if they have a uid and key
                if current and 'uid' in current and 'key' in current:
                    # But ignore revoked keys if revoked option is True
                    if not (current.get('revoked', False) and options.get('revoked', False)):
                        units.append(dict(current))

                current = defaultdict(list)

            elif line.startswith('PHOTO:'):
                current['photo'] = line.split(':', 1)[-1]
                self.photos.append(current['photo'])
            elif ' of size ' in line:
                continue
            elif '   ' in line:
                (kind, line) = line.split('   ', 1)
                if kind == 'pub':
                    current['expires'] = False
                    current['revoked'] = False

                    if '[' in line:
                        (line, mod) = line.strip().split('[', 1)
                        (mod, _) = mod.split(']', 1)
                        if ': ' in mod:
                            (mod, edited) = mod.split(': ', 1)
                            current[mod] = to_date(edited)

                    (key, created) = line.split(' ', 1)
                    current['created'] = to_date(created)
                    (current['bits'], current['key']) = key.split('/', 1)
                elif kind in ('uid', 'sub'):
                    current[kind].append(line.strip())
                else:
                    current[kind] = line.strip()

        return units

    @property
    def default_photo(self):
        if not hasattr(self, '_photo'):
            self._photo = mktemp('.svg')
            with open(self._photo, 'w') as fhl:
                fhl.write("""
  
""")
            self.photos.append(self._photo)
        return self._photo

    def recieve_keys(self, *keys, **options):
        """Present the opotunity to add the key to the user:
         
        Returns
          - True if the key was already or is now imported.
          - False if keys were available but the user canceled.
          - None if no keys were found within the search.

        """
        keys = self.search_keys(*keys)
        if not keys:
            return None # User doesn't have GPG

        # Always use a temporary gpg home to review keys
        gpg = GPG(cmd=self.command, local=True) if not self.local else self

        # B. Import each of the keys
        gpg('--recv-keys', *zip(*keys)[0])

        # C. List keys (with photo options)
        choices = []
        for key in gpg.list_keys(photos=True):
            choices.append(key.get('photo', self.default_photo))
            choices.append('\n'.join(key['uid']))
            choices.append(key['key'])
            choices.append(str(key['expires']))

        if len(choices) / 4 == 1:
            title = "Can I use this GPG key to encrypt for this user?"
        else:
            title = "Please select the GPG key to use for encryption"

        # Show using gtk zenity (easier than gtk3 directly)
        p = Popen(['zenity',
            '--width', '900', '--height', '700', '--title', title,
            '--list', '--imagelist', '--print-column', '3',
              '--column', 'Photo ID',
              '--column', 'ID',
              '--column', 'Key',
              '--column', 'Expires',
            ] + choices, stdout=PIPE, stderr=PIPE)

        # Returncode is generated after communicate!
        key = p.communicate()[0].strip()

        # Select the default first key if one choice.
        # (person pressed ok without looking)
        if not key and len(choices) == 4:
            key = choices[2]

        if p.returncode != 0:
            # Cancel was pressed
            return False

        # E. Import the selected key
        self('--recv-keys', key)
        return self.status == 0

    def is_key_available(self, search):
        """Return False if the email is not found in the local key list"""
        self('--list-keys', search)
        if self.status == 2: # Keys not found
            return False
        # We return true, even if gpg returned some other kind of error
        # Because this prevents us running more commands to a broken gpg
        return True

    def search_keys(self, *keys):
        """Returns a list of (key_id, info) tuples from a search"""
        out = self('--search-keys', *keys)
        found = []
        prev = []
        for line in out.split("\n"):
            if line.startswith('gpg:'):
                continue
            if 'created:' in line:
                key_id = line.split('key ')[-1].split(',')[0]
                if '(revoked)' not in line:
                    found.append((key_id, prev))
                prev = []
            else:
                prev.append(line)
        return found


if __name__ == '__main__':
    cmd = sys.argv[0] + '.orig'
    if not os.path.isfile(cmd):
        sys.stderr.write("Can't find pass-through command '%s'\n" % args[0])
        sys.exit(-13)

    args = [cmd] + sys.argv[1:]
    # Check to see if call is from an application
    if 'GIO_LAUNCHED_DESKTOP_FILE' in os.environ:
        # We use our moved gpg command file
        gpg = GPG(cmd=cmd)
        # Check if we've got a missing key during an encryption, we get the
        # very next argument after a -r or -R argument (which should be
        # the email address)
        for recipient in [args[i+1] for (i, v) in enumerate(args) if v in ('-r', '-R')]:
            # Only check email addresses
            if '@' in recipient:
                if not gpg.is_key_available(recipient):
                    if gpg.recieve_keys(recipient) is None:
                        pass
                        # We can add a footer to the message here explaining GPG
                        # We can't do this, evolution will wrap it all up in a
                        # message structure.
                        #msg = sys.stdin.read()
                        #if msg:
                        #    msg += GPG_TRIED_FOOTER
                        #sys.stdout.write(msg)
                        #sys.exit(0)

    # We call and do not PIPE anything (pass-through)
    try:
        sys.exit(call(args))
    except KeyboardInterrupt:
        sys.exit(-14)