Skip to content

TIL that ip rule supports a uidrange condition that allows to route traffic for specific users. This is useful in combination with Tailscale exit nodes or other VPNs to allow setting a different default route per-service (you do run your services as separate UIDs, right?)

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

Somehow I ended up playing with SIMD and I wrote an IPv6 96-bit prefix comparison function that's 250 picoseconds faster than doing lhs.segments()[0..6] == rhs.segments()[0..6].

I have no idea what to do with these 250 picoseconds I am saving every time I need to compare 96-bit IPv6 prefixes.

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

while studying cargo index format, I realized a cursed thing: I could probably get this thing working with Nix to fetch crates without using Cargo...

i'm probably reinventing the wheel (the wheel being naersk), but:

let
  # assuming cwd = `git clone github.com/rust-lang/crates.io-index`
  fetchCrateVersions = name: builtins.readFile ./${builtins.substring 0 2 name}/${builtins.substring 2 4 name}/${name};
  fetchCrateMetadata = name: version: builtins.getAttr version (builtins.listToAttrs (builtins.map (c: { name = c.vers; value = c; }) (builtins.map builtins.fromJSON (builtins.filter (n: n != "") (nixpkgs.lib.strings.splitString "\n" (fetchCrateVersions name))))));
in
# `fetchCrateMetadata`'s output contains `cksum` attribute matching SHA256 hash of the crate, allowing for a fixed-output derivation.
# Using recursive calls of a hypothetical function, all dependencies of a certain crate could be found, and a list for calling `fetchurl` created.
# Creating such a recursive function is an exercise for the reader.

Thus, the entire crates.io registry becomes accessible, and a hypothetical Cargo.lock file could be used as a starting point to discover dependencies for a project, without the requirement for any sort of hashes.

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

This is your reminder that when switching from Chrome, you should avoid Chromium reskins.

Especially Brave with its hypocrisy around its own shitcoin, BAT, which doesn't allow non-custodial withdrawals (therefore is not even real crypto). Not to mention the widely known homophobic views of its CEO.

...also brave sucks for me as a web developer, because they disable some genuinely useful features, all in the name of "privacy". At this point it's better to use Firefox instead.

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

I've never found social networks and their algorithms "addicting", like how many fearmongers claim them to be.

Instead, I found them simply frustrating.

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

Cryptocurrencies, in relation to constructing hardware wallets, have a singular fatal flaw.

Most popular ones seem to use a very specific elliptic curve for implementing their cryptography, secp256k1. Ban secure element chips that implement that curve — and bam, you've successfully curbed any efforts to construct a hardware wallet.

Perhaps you could even attempt to ban software that implements it. That is actually much harder to do, but a government could attempt to do it.

That doesn't seem too secure to me in context of covert cryptocurrency usage.

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

Fucking Rercury is in the metrograde again or something and there's also a full moon and TikTok is now full of magic practicioners manifesting things and spamming posts with affirmations

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

Brave Browser isn't just a crypto scam, it's a double crypto scam. Because if you actually want to participate in its crypto scam, you can only do it from certain countries which have a partnered centralized exchange...and you have to go through KYC. Which defeats the purpose of cryptocurrency entirely.

You cannot self-custody your way out of it, which makes it double the scam it actually is.

Switch to Firefox, it respects your privacy and doesn't feature any crypto.

Though I do have a fair share of concerns about where Mozilla is going, Firefox is good enough for me to overlook these...for now

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

Thinking about creating a Microsub server, I remembered that my new shiny Postgres backend can easily answer if I already left a like on a post or not:

kittybox=# CREATE INDEX likes ON kittybox.mf2_json USING GIN ((mf2['properties']['like-of']));
CREATE INDEX
kittybox=# EXPLAIN SELECT EXISTS (SELECT uid FROM kittybox.mf2_json WHERE mf2['properties']['like-of'] ? 'https://aaronparecki.com/2018/12/25/17/');
                                                        QUERY PLAN                                                        
--------------------------------------------------------------------------------------------------------------------------
 Result  (cost=11.28..11.29 rows=1 width=1)
   InitPlan 1 (returns $0)
     ->  Bitmap Heap Scan on mf2_json  (cost=8.09..43.20 rows=11 width=0)
           Recheck Cond: (mf2['properties'::text]['like-of'::text] ? 'https://aaronparecki.com/2018/12/25/17/'::text)
           ->  Bitmap Index Scan on likes  (cost=0.00..8.08 rows=11 width=0)
                 Index Cond: (mf2['properties'::text]['like-of'::text] ? 'https://aaronparecki.com/2018/12/25/17/'::text)
(6 rows)

Nice. Indexes are awesome.

Webmention counters:

  • 1
  • 💬0
  • 🔄0
  • 🔖0

Here's a short Rust program using the microformats crate that checks the presence of a webmention on a certain page, properly resolving all URLs and even scanning HTML content in entry["properties"]["content"].

use std::cell::{RefCell, Ref};
use std::rc::Rc;

use clap::Parser;
use microformats::types::PropertyValue;
use microformats::html5ever;
use microformats::html5ever::tendril::TendrilSink;

#[derive(thiserror::Error, Debug)]
enum Error {
    #[error("http request error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("microformats error: {0}")]
    Microformats(#[from] microformats::Error),
    #[error("json error: {0}")]
    Json(#[from] serde_json::Error),
    #[error("url parse error: {0}")]
    UrlParse(#[from] url::ParseError),
}

#[derive(Debug)]
enum MentionType {
    Reply,
    Like,
    Repost,
    Bookmark,
    Mention
}

fn check_mention(document: impl AsRef<str>, base_url: &url::Url, link: &url::Url) -> Result<Option<MentionType>, Error> {
    // First, check the document for MF2 markup
    let document = microformats::from_html(document.as_ref(), base_url.clone())?;

    // Get an iterator of all items
    let items_iter = document.items.iter()
        .map(AsRef::as_ref)
        .map(RefCell::borrow);

    for item in items_iter {
        let props = item.properties.borrow();
        for (prop, interaction_type) in [
            ("in-reply-to", MentionType::Reply), ("like-of", MentionType::Like),
            ("bookmark-of", MentionType::Bookmark), ("repost-of", MentionType::Repost)
        ] {
            if let Some(propvals) = props.get(prop) {
                for val in propvals {
                    if let PropertyValue::Url(url) = val {
                        if url == link {
                            return Ok(Some(interaction_type))
                        }
                    }
                }
            }
        }
        // Process `content`
        if let Some(PropertyValue::Fragment(content)) = props.get("content")
            .map(Vec::as_slice)
            .unwrap_or_default()
            .first()
        {
            let root = html5ever::parse_document(html5ever::rcdom::RcDom::default(), Default::default())
                .from_utf8()
                .one(content.html.to_owned().as_bytes())
                .document;

            // This is a trick to unwrap recursion into a loop
            //
            // A list of unprocessed node is made. Then, in each
            // iteration, the list is "taken" and replaced with an
            // empty list, which is populated with nodes for the next
            // iteration of the loop.
            //
            // Empty list means all nodes were processed.
            let mut unprocessed_nodes: Vec<Rc<html5ever::rcdom::Node>> = root.children.borrow().iter().cloned().collect();
            while unprocessed_nodes.len() > 0 {
                // "Take" the list out of its memory slot, replace it with an empty list
                let nodes = std::mem::take(&mut unprocessed_nodes);
                for node in nodes.into_iter() {
                    // Add children nodes to the list for the next iteration
                    unprocessed_nodes.extend(node.children.borrow().iter().cloned());

                    if let html5ever::rcdom::NodeData::Element { ref name, ref attrs, .. } = node.data {
                        // If it's not `<a>`, skip it
                        if name.local != *"a" { continue; }
                        for attr in attrs.borrow().iter() {
                            // if it's not `<a href="...">`, skip it 
                            if attr.name.local != *"href" { continue; }
                            // Be forgiving in parsing URLs, and resolve them against the base URL
                            if let Ok(url) = base_url.join(attr.value.as_ref()) {
                                if &url == link {
                                    return Ok(Some(MentionType::Mention));
                                }
                            }
                        }
                    }
                }
            }
            
        }
    }

    Ok(None)
}

#[derive(Parser, Debug)]
#[clap(
    name = "kittybox-check-webmention",
    author = "Vika <vika@fireburn.ru>",
    version = env!("CARGO_PKG_VERSION"),
    about = "Verify an incoming webmention"
)]
struct Args {
    #[clap(value_parser)]
    url: url::Url,
    #[clap(value_parser)]
    link: url::Url
}

#[tokio::main]
async fn main() -> Result<(), self::Error> {
    let args = Args::parse();
    
    let http: reqwest::Client = {
        #[allow(unused_mut)]
        let mut builder = reqwest::Client::builder()
            .user_agent(concat!(
                env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")
            ));

        builder.build().unwrap()
    };

    let response = http.get(args.url.clone()).send().await?;
    let text = response.text().await?;
    
    if let Some(mention_type) = check_mention(text, &args.url, &args.link)? {
        println!("{:?}", mention_type);

        Ok(())
    } else {
        std::process::exit(1)
    }
}

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

bluesky is Twitter all over again now, but decentralized and even more chaotic.

And I still don't know half the twitter culture, so I feel lost. And there are already more cultural things like the hellthread? My head is spinning I should probably get breakfast!

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

Assuming the default bluesky feeds are open source, it would be easy to check if posts with links get downranked. This was the case at Twitter before when I was posse-ing — posts with links gained less impressions, and considering my account size, it was pretty much catastrophic.

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0

Just for fun, I decided to import a subset of my MF2-JSON data into Postgres and see if I can maybe improve Kittybox with it.

CREATE DATABASE mf2_test;
CREATE TABLE vika.mf2_json (url TEXT NOT NULL PRIMARY KEY, mf2 JSONB NOT NULL);
for post in $(ssh root@primrose "ls /persist/containers/kittybox/kittybox/data/fireburn.ru/posts/*.json"); do
    json="$(ssh root@primrose cat "$post")"
    echo "INSERT INTO vika.mf2_json (url, mf2) VALUES ('$(echo "$json" | jq -r ".properties.uid[]")', '$(echo "$json" | sed -e "s/'/''/g")') ON CONFLICT DO NOTHING;"
done | psql mf2_test --single-transaction
-- See which categories I am using for posts
SELECT DISTINCT jsonb_strip_nulls(jsonb_path_query(mf2['properties']['category'], '$[*]'))  #>> '{}' AS tag FROM vika.mf2_json ORDER BY tag ASC;

-- Index the post corpus for full-text search
CREATE INDEX fulltext ON vika.mf2_json USING GIN (to_tsvector('english', mf2['properties']['content'][]));

-- Run a full-text search -- takes 90ms w/o index, 2ms with index!
SELECT url, mf2['properties']['content'][]['value'] FROM mf2_json WHERE to_tsvector('english', mf2['properties']['content'][]) @@ to_tsquery('Kittybox') ORDER BY mf2['properties']['published'][] DESC;

This makes it possible to run a lot of data analysis on posts. Maybe I'll finally have some stuff to populate my widget on the right of the main page with.

Webmention counters:

  • 0
  • 💬0
  • 🔄0
  • 🔖0