💾 Archived View for mozz.us › journal › 2020-06-18.gmi captured on 2022-06-11 at 21:29:24. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2020-09-24)

-=-=-=-=-=-=-

Adding Emoji Favicons to Gemini - Part 2

Published 2020-06-18

Summary

This is a followup to my previous article about adding favicons to gemini

[2020-06-03] Adding Emoji Favicons to Gemini

Since then, some folks have tried out the proposal and sent me feedback that I think is worth sharing publicly.

Why only emojis?

This is by far the most common criticism that I've seen so far. Unicode has over a million characters to chose from. Why restrict favicons to only emojis? Why shouldn't other unicode symbols be allowed too?

I picked emojis simply because they're a well-defined list of character sequences, maintained by the Unicode Consortium. This makes it trivial for a client to implement an allow-list of valid favicons.

https://unicode.org/emoji/charts/full-emoji-list.html

I'm not against allowing monograms or other non-emoji symbols, in fact I would like to include them. But I can't figure out a good way to express this in a specification. I definitely don't want to accept arbitrary unicode sequences of any length. And I don't want to limit to a single unicode codepoint either (that would exclude diacritical marks and other joiners). I've seen folks mention a "glyph", but what exactly *is* a glyph? The best information that I can gather is that it appears to be font-specific and not well defined. What I really want to say is something like "a printable unicode sequence that occupies 1-2 horizontal blocks of space and 1 vertical block of space. Is there a term for that?

In other words... unicode confuses me and I'm open to better ideas.

Added latency

Another point that was brought to my attention is that making the favicon request *before* the first request for content will double the round-trip time. This could add a noticeable delay if you're requesting content from a server somewhere on the other side of the world.

Of course, this in true. In the RFC, I added a couple of examples that showed making the requests serially (first request the the favicon, then request the content). I had my own web proxy's implementation in mind when I wrote this. Because portal.mozz.us generates a cooked HTML page, I wanted to wait for the favicon to download before I could render the page. But there's no reason that the two requests *need* to be made in a particular order. It's not *disallowed* by the RFC as long as the client doesn't preemptively load the favicon. With a GUI framework, a client might use an event loop to fire off both requests concurrently and then render the favicon separately from the rest of the content.

I will likely update the RFC to call this out and say something like... "In order to minimize the effects of latency, clients are encouraged to make the favicon request concurrently with the main gemini request".

I also updated my implementation on portal.mozz.us to spin off the favicon request in a separate thread. If the thread responds before the main content request, cool, I will include it in the response. Otherwise, I will return the page without the favicon, but still cache the favicon to use in any future responses. I think this is a good compromise and it wasn't as hard to write as I thought it would be.

Adoption

So far, a handful of people have been kind enough to humor me and add a favicon.txt file to their server. Thanks!

gemini://acidic.website/favicon.txt

20	text/plain
🈴

gemini://alexschroeder.ch/favicon.txt

20 text/plain; charset=UTF-8
🐝

gemini://astrobotany.mozz.us/favicon.txt

20 text/plain
🌻

gemini://carcosa.net/favicon.txt

20 text/plain
♏

gemini://cosmic.voyage/favicon.txt

20 text/plain
🚀

gemini://ftrv.se/favicon.txt

(not displayed by portal.mozz.us because it's not an emoji)

20 text/plain
⑨

gemini://gemini.conman.org/favicon.txt

20 text/plain; charset=UTF-8
👽

gemini://makeworld.gq/favicon.txt

20 text/plain
🔗

gemini://mozz.us/favicon.txt

20 text/plain
🐟

gemini://tilde.black/favicon.txt

20 text/plain
🕳

Code

For anyone interested, here's the code that I use to fetch favicons in my HTTP proxy server. It's very poorly written and even more poorly documented.

import shelve
import threading
import emoji

class Favicon:
    """
    Usage:
        favicon = Favicon(host, port).load()
    """

	# DOES_NOT_EXIST means that we tried to request the favicon but the server
	# either failed or returned an invalid response. Compared to "None" which
	# means that we don't know if the server has a favicon or not.
    DOES_NOT_EXIST = -1

    database = "/tmp/favicon_cache"
    expiration = 60 * 60  # cache expiration, seconds
    lock = threading.Lock()

    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.key = host
        if self.port:
            self.key += f":{port}"

    def load(self):
        value, is_expired = self._load_cache()
        if value is None or is_expired:
            thread = threading.Thread(target=self.refresh)
            thread.start()

        if value == self.DOES_NOT_EXIST:
            return None
        else:
            return value

    def refresh(self):
        """
        Attempt to fetch the remote favicon and cache the result.

        Use a lock to prevent two threads from refreshing at the same time and
        making duplicate requests to the server.
        """
        has_lock = self.lock.acquire(blocking=False)
        if has_lock:
            try:
                value = self._fetch_favicon()
                self._save_cache(value)
            finally:
                self.lock.release()

    def _fetch_favicon(self):
        if self.port:
            url = f"gemini://{self.host}:{self.port}/favicon.txt"
        else:
            url = f"gemini://{self.host}/favicon.txt"

        try:
            with gemini_request(self.host, self.port or 1965, url) as response:
                if response.status == '20' and response.meta.startswith("text/plain"):
                    text = response.body.read().decode(response.charset)
                    text = text.strip()
                    if text in emoji.UNICODE_EMOJI:
                        return text
                else:
                    return self.DOES_NOT_EXIST
        except Exception:
            return self.DOES_NOT_EXIST

    def _load_cache(self):
        with shelve.open(self.database) as db:
            if self.key in db:
                ttl, value = db[self.key]
                if time.time() - ttl < self.expiration:
                    return value, False
                else:
                    return value, True
        return None, False

    def _save_cache(self, value):
        with shelve.open(self.database) as db:
            db[self.key] = (time.time(), value)