<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Ryan Finnie</title>
    <description>Personal blog for Ryan Finnie</description>
    <link>https://www.finnie.org/</link>
    <atom:link href="https://www.finnie.org/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Tue, 02 Jun 2026 11:00:22 -0700</pubDate>
    <lastBuildDate>Tue, 02 Jun 2026 11:00:22 -0700</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>I discovered the Debian OpenSSL bug</title>
        <description>&lt;p&gt;16 years ago today, &lt;a href=&quot;https://lists.debian.org/debian-security-announce/2008/msg00152.html&quot;&gt;Debian announced CVE-2008-0166&lt;/a&gt;, “predictable random number generator”. Reported by and discovery attributed to Luciano Bello, this was a Debian-specific OpenSSL vulnerability which had been in place for nearly two years and is &lt;a href=&quot;https://16years.secvuln.info/&quot;&gt;still being exploited in the wild to this date&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, in this post I’ll tell the story of how I discovered the bug nearly a year earlier, but didn’t report it. Why didn’t I? Read on for the story behind this totally-not-clickbait title!&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;a href=&quot;https://research.swtch.com/openssl&quot;&gt;Lessons from the Debian/OpenSSL Fiasco&lt;/a&gt;, written a week after the announcement, goes into more detail, but basically the bug resulted in key generation being tied directly to the process ID (PID) of the generating program. This would be bad enough today with most distributions defaulting to 4.2 million PIDs, but back in 2008, the default on all distros, including Debian, was 32,768. I believe Linux had optional higher PID support back then (though the earliest I can find reference to is 2011 from a quick search), but it’s been only recently that higher PID support has been default on distros, since the assumption of a maximum PID being 32,768 was baked into a lot of software for so long.&lt;/p&gt;

&lt;p&gt;On April 8, 2007, Debian 4.0 “etch” was released. This was the first stable release containing the OpenSSL bug, but it had been available in Debian unstable/testing since September 17, 2006, as well as a few non-LTS Ubuntu releases, and possibly other Debian-based distributions.&lt;/p&gt;

&lt;p&gt;At the time, I was working for a marketing company in Reno, Nevada. A large part of our business was website design, and we did in-house hosting to support it. It was large enough that we had datacenter space and multiple sysadmins to support the programmers and designers. We started out small with mostly shared hosting, with dedicated servers for the largest customers. But around 2005, I developed an internal system for virtualized hosting based on User Mode Linux. UML allowed for running a kernel as a user-mode program on the host system. This provided decent separation of the guests, and was used by early Virtual Dedicated Server providers like Linode, before they moved on to Xen and later KVM.&lt;/p&gt;

&lt;p&gt;So we were using UML on new guests, but were still pretty much treating them like dedicated servers. And back then, that meant the time-honored tradition of a quick-and-dirty initialization script to automate new setup tasks. I would boot the guest with a generic base Debian image, connect to its virtual serial console, log in as root, and run the equivalent of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl | sh&lt;/code&gt;. (At least the script being retrieved was on a local LAN.) The script would ask a few questions, then run all our base setup installations and configurations.&lt;/p&gt;

&lt;p&gt;A few months after the release of etch, I noticed that many of the deployed guests had the same OpenSSH host key. Over half of all deployed etch guests had the exact same key, but there were also smaller clusters of guests which shared other host keys between them. I had no idea how this had happened, and worse, I wasn’t able to reproduce the problem when deploying test guests. Eventually I gave up on trying to root cause the issue, re-generated the duplicated SSH host keys, and moved on.&lt;/p&gt;

&lt;p&gt;Then, on May 13, 2008, I saw the Debian announcement and it all made sense. I knew what I had discovered last year, why it had happened, and I also knew why I wasn’t able to reproduce it.&lt;/p&gt;

&lt;p&gt;One of the first things my deployment script did was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt-get install openssh-server&lt;/code&gt;. Because this happened very early in the script process, and because the base boot image was so minimal, the number of processes being run between kernel start and the point &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh-keygen&lt;/code&gt; is run as part of the OpenSSH server install was static, as long as my actions of logging in for the first time and running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl | sh&lt;/code&gt; were the same each time. When I was trying to reproduce the problem after discovering it, I was introducing variances by, say, checking the environment ahead of time, or by manually downloading the script and then running it, or by editing the script itself to add debugging.&lt;/p&gt;

&lt;p&gt;This explained why most of the deployed guests had the same host key. The other clusters of guests with a shared keys were explained by things like making a change to the install script down the road, or even typing a fidget &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ls&lt;/code&gt; or similar before starting the install. I had discovered the symptoms of the Debian OpenSSL bug months ahead of time, but I didn’t report it because I hadn’t realized the cause or implications of what I had discovered, and I’m still kicking myself for not putting the pieces together.&lt;/p&gt;

&lt;p&gt;Checking back on my IRC logs from the day after the announcement revealed this humorous reply as I was telling the story:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Way to not report this last year and save the day&lt;/p&gt;

  &lt;p&gt;Next time you discover a critical bug, save it for christmas or new years eve though&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        <pubDate>Mon, 13 May 2024 09:49:11 -0700</pubDate>
        <link>https://www.finnie.org/2024/05/13/i-discovered-the-debian-openssl-bug/</link>
        <guid isPermaLink="true">https://www.finnie.org/2024/05/13/i-discovered-the-debian-openssl-bug/</guid>
        
        
      </item>
    
      <item>
        <title>The printer formerly known as Ender</title>
        <description>&lt;p&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2024/ender-3-v2-modified.jpg&quot; alt=&quot;Ender 3 V2 3D printer, heavily modified&quot; class=&quot;img-responsive img-rounded img-lg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.finnie.org/2019/05/04/monoprice-maker-select-plus-3d-printer-mods/&quot;&gt;Five years ago&lt;/a&gt;, I wrote about the 3D printer I had recently bought, a Monoprice Maker Select Plus (Wanhao Duplicator i3 Plus), and the various mods I had done to it. I had discovered that when it comes to 3D printing, for me the journey was much more fun than the destination, and while I often use 3D printers for “practical” purposes, hacking on the printer itself is the most fun.&lt;/p&gt;

&lt;p&gt;In that post, I said:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;3D printers are like cats: people who have more than zero usually have more than one. Some even have their houses overrun by them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two years after that, I bought another printer, a Creality Ender 3 V2.  In the same spirit, it’s a very hackable printer, to the extent that I’m not even sure I should call it an Ender 3 anymore.  Most of the components have been modified or replaced in the 3 years I’ve had it, with the latest being a direct extrusion conversion. Everything orange you see in that picture is a 3D printed mod.&lt;/p&gt;

&lt;p&gt;Admittedly, a lot of it is change for change sake, but a lot is also for performance, quality and reliability reasons. For a hobbyist printer, it works amazingly, and produces nearly perfect &lt;a href=&quot;https://www.3dbenchy.com/&quot;&gt;Benchys&lt;/a&gt;, though of course the speed isn’t as great as a hyper-modern printer like a Bambu.&lt;/p&gt;

&lt;p&gt;While I am perpetually on the edge of building a full kit from scratch like a Voron, my time with the Ender 3 V2 has been very enjoyable. Below is an account of all the changes I’ve made to the printer since I bought it 3 years ago.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The newest addition (within the last few weeks) is a direct drive extruder, the &lt;a href=&quot;https://www.amazon.com/gp/product/B0C7QC7S3D&quot;&gt;Creality Sprite SE&lt;/a&gt;. (Not to be confused with the dozen other variants of the Creality Sprite. Creality has a tendency to do that with everything it makes.) This replaced the Bowden tube system; the only other accessory to note is a &lt;a href=&quot;https://www.tinkercad.com/things/b7OohBxQFKU-e3v2-direct-drive-top-cable-mount&quot;&gt;simple cable routing channel&lt;/a&gt; I designed so the cable bundle didn’t get pinched on the sides when moving around.&lt;/p&gt;

&lt;p&gt;The hotend is a &lt;a href=&quot;https://www.amazon.com/dp/B0789V2D7C&quot;&gt;Micro-Swiss all-metal hotend&lt;/a&gt;, and &lt;a href=&quot;https://www.printables.com/model/80823-yet-another-ender-3-v2-fan-assembly-remix-satsana-&quot;&gt;the fan assembly&lt;/a&gt; is a complete replacement, supporting a better &lt;a href=&quot;https://www.amazon.com/gp/product/B0757RPCN9&quot;&gt;hotend cooling fan&lt;/a&gt; and &lt;a href=&quot;https://www.amazon.com/gp/product/B0755BY9RH&quot;&gt;part cooling fan&lt;/a&gt;.  A &lt;a href=&quot;https://www.amazon.com/gp/product/B08MD45N9H&quot;&gt;BLTouch&lt;/a&gt; automatic bed leveler is also added. (That link now goes to Creality’s CRTouch alternative, but when I bought it, it was a genuine Antclabs BLTouch, back when they were partnering directly with Creality.)&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://www.thingiverse.com/thing:4753473&quot;&gt;LCD panel&lt;/a&gt; has been &lt;a href=&quot;https://www.tinkercad.com/things/cU3Gijv3t8R-ender-3-v2-lcd-panel-modified&quot;&gt;modified&lt;/a&gt;, changing it from portrait on the side to landscape in front, with a new &lt;a href=&quot;https://www.thingiverse.com/thing:463660&quot;&gt;control knob&lt;/a&gt;. The UI has changed from CrealityUI to MarlinUI, and because of this (and the many other mods), I’m running standard &lt;a href=&quot;https://github.com/MarlinFirmware/Marlin&quot;&gt;Marlin&lt;/a&gt;, with &lt;a href=&quot;https://forge.colobox.com/rfinnie/marlin-personal&quot;&gt;my own custom configuration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Z axis has been upgraded to &lt;a href=&quot;https://www.amazon.com/Official-Creality-Ender-Z-axis-Stepper/dp/B07VJG4ZCG&quot;&gt;dual screw with dual motors&lt;/a&gt;, though they are both controlled by the same stepper driver, so it’s more about stability than independent calibration. In addition, the Z axis &lt;a href=&quot;https://www.amazon.com/gp/product/B07RMZCLZ3&quot;&gt;shaft couplers&lt;/a&gt; have been upgraded to reduce horizontal shaft movement.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2024/ender-3-v2-modified-underside.jpg&quot; alt=&quot;Ender 3 V2 3D printer, heavily modified, underside view&quot; class=&quot;img-responsive img-rounded img-md pull-right&quot; /&gt;The entire underside has been pretty extensively reworked, as the stock Ender 3 V2 was pretty starved for airflow. The covers for the &lt;a href=&quot;https://www.thingiverse.com/thing:4653994&quot;&gt;mainboard&lt;/a&gt; and &lt;a href=&quot;https://www.thingiverse.com/thing:4713952&quot;&gt;PSU&lt;/a&gt; &lt;a href=&quot;https://www.thingiverse.com/thing:4831917&quot;&gt;area&lt;/a&gt; have been replaced with prints which allow for &lt;a href=&quot;https://www.amazon.com/gp/product/B00IOIJ4AC&quot;&gt;quiet 80mm fans&lt;/a&gt;, but as they are 12v fans and the printer is a 24v system, an &lt;a href=&quot;https://www.amazon.com/Zixtec-LM2596-Converter-Module-1-25V-30V/dp/B07VVXF7YX&quot;&gt;LM2596 buck converter&lt;/a&gt; is needed. To support all of this, the printer needs to be raised up a few inches, so &lt;a href=&quot;https://www.thingiverse.com/thing:4721832&quot;&gt;these feet&lt;/a&gt; are used (which even incorporate the original printer’s rubber feet).&lt;/p&gt;

&lt;p&gt;Last year, I was having issues with Y axis shifting.  The problem ultimately ended up being the motor itself was losing steps, so I needed to replace &lt;a href=&quot;https://www.amazon.com/gp/product/B091D37BM2&quot;&gt;the motor&lt;/a&gt; and &lt;a href=&quot;https://www.amazon.com/gp/product/B088WB8D7W&quot;&gt;pulley&lt;/a&gt; (since the factory motor includes the pulley permanently attached). However, there was also the the concern it may have been the stepper driver on the mainboard itself, and the drivers are not modular. While this ended up not being the case, I ended up buying and installing a replacement &lt;a href=&quot;https://www.amazon.com/gp/product/B09NMJMPN1&quot;&gt;V4.2.7 mainboard&lt;/a&gt;, which is a direct replacement for the stock V4.2.2 board, with some very minor improvements.&lt;/p&gt;

&lt;p&gt;Not much has been done to the bed itself, with the only addition being &lt;a href=&quot;https://www.amazon.com/gp/product/B07FY47BX7&quot;&gt;upgraded springs&lt;/a&gt;, though the BLTouch makes manual bed leveling unnecessary (you’re good if you can keep it within 0.5mm of level or so). However, I did simply remove the rear left screw from the bed, making it a 3-point system, which helps reduce flex within the bed itself.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://www.thingiverse.com/thing:3209211&quot;&gt;filament spool holder&lt;/a&gt; has been upgraded to add &lt;a href=&quot;https://www.amazon.com/Sackorange-Skateboard-Bearings-Miniature-Bearings%EF%BC%88Pack/dp/B07216D1SZ&quot;&gt;skateboard bearings&lt;/a&gt;. And finally, a &lt;a href=&quot;https://www.tinkercad.com/things/iMyXIBttRNl-ender-3-v2-filament-feed-arm&quot;&gt;simple filament feed arm&lt;/a&gt;, though this isn’t needed as much today, since I replaced the Bowden system with direct extrusion.&lt;/p&gt;
</description>
        <pubDate>Sat, 11 May 2024 10:56:00 -0700</pubDate>
        <link>https://www.finnie.org/2024/05/11/the-printer-formerly-known-as-ender/</link>
        <guid isPermaLink="true">https://www.finnie.org/2024/05/11/the-printer-formerly-known-as-ender/</guid>
        
        
      </item>
    
      <item>
        <title>Better-Assembled Access Tokens</title>
        <description>&lt;p&gt;A few years ago, &lt;a href=&quot;https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/&quot;&gt;GitHub changed the format of their access tokens&lt;/a&gt; from a hexadecimal format which was indistinguishable from a SHA1 hash, to a format with a human-identifiable prefix and built-in checksumming, which can be identified as a GitHub token by a program. This is useful for being able to determine if, for example, an access token was accidentally committed into a repository. I welcomed this, but recently wanted to build an agnostic version which could be used in other systems.&lt;/p&gt;

&lt;p&gt;Enter: Better-Assembled Access Tokens (BAAT). The token format looks like so:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;bat_pfau4bdvkqwmwwur2bjo2q2squjeld5fafgyk5sd
bat_3udmmr57bglierumrjxjxrkiv3nydd5faebohhgn
bat_bbzz6q4rnbnu6tkujrb73vhfuk6pdd5fafme5kq5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;“bat” is the prefix and can be any lowercase alphanumeric string, but should be between 2 and 5 characters.&lt;/p&gt;

&lt;p&gt;The other part – the wrapped data – contains a payload of 144 bits (18 bytes), a magic number and version identifier, and a checksum. This payload size allows for a full UUID to be generated, with 2 bytes left for additional control data if needed.&lt;/p&gt;

&lt;p&gt;The checksum includes all of the data, including the prefix (which is not a feature of GitHub’s tokens), and the fact that it has a binary magic number means a BAAT can be identified programmatically, no matter the prefix chosen by the application. A BAAT is canonically all lowercase, but can handle being case-corrupted in transit.&lt;/p&gt;

&lt;p&gt;A sample Python implementation is below and &lt;a href=&quot;https://forge.colobox.com/rfinnie/rf-tools/src/HEAD/bin/baat&quot;&gt;a general-purpose utility for generating tokens is here&lt;/a&gt;, but the general specification for BAAT is:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If the binary payload is under 18 bytes, pad it to 18 bytes&lt;/li&gt;
  &lt;li&gt;CRC32 the prefix + payload + magic number (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\x8f\xa5&lt;/code&gt;) + version (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\x01&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;Assemble the wrapped data as a base32 concatenation of payload + magic number + version + CRC&lt;/li&gt;
  &lt;li&gt;Assemble the final BAAT as the prefix + “_” + the wrapped data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To quickly detect a version 1 BAAT, check for “d5fa” at position -11 in the token string.
If that is detected, you can move on to a full verification:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Split the string into prefix and wrapped data by “_”&lt;/li&gt;
  &lt;li&gt;Base32 decode the wrapped data and verify it’s at least 7 bytes&lt;/li&gt;
  &lt;li&gt;Verify the 2 bytes at position -7 is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\x8f\xa5&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Verify the byte at position -5 is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\x01&lt;/code&gt; for version 1 (currently the only version, but doesn’t hurt to future-proof – the rest of the process assumes a version 1 BAAT)&lt;/li&gt;
  &lt;li&gt;Verify the wrapped data is 25 bytes&lt;/li&gt;
  &lt;li&gt;Extract the payload as the 18 bytes at position 0 (the beginning), and the checksum as the 4 bytes at position -4 (the end)&lt;/li&gt;
  &lt;li&gt;Verify the checksum as the CRC32 of prefix + payload + magic number + version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This specification is open; feel free to use it in your implementations!&lt;/p&gt;

&lt;p&gt;If you’re wondering why the magic number and version are in the middle of the wrapped data instead of the front like it normally is for a data format (thus requiring some additional positional math), it’s because it shows up as a static sequence of text in a list of multiple BAATs. Placing the payload at the beginning and the checksum at the end allows a human to quickly pattern match “oh, this is the ‘3ud’ token, not the ‘pfa’ token”.&lt;/p&gt;

&lt;p&gt;If you’re wondering why the payload is 18 bytes, it’s because BAAT use base32 for the encoding, which will use trailing equal signs as padding. 20 input bytes is a multiple with no padding, which would have allowed for a 16-byte payload and 4-byte checksum. But I wanted to have a 2-byte magic number, and the next multiple without padding was 25 bytes, so the final 3 bytes were used for a 1-byte version and 2 extra payload bytes.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Better-Assembled Access Tokens
# SPDX-FileCopyrightText: Copyright (C) 2023 Ryan Finnie
# SPDX-License-Identifier: MIT
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b32decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b32encode&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;random&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;randint&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;zlib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crc32&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BAATError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ValueError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;make_baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;bat&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;magic&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x8f\xa5&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;baat_ver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x01&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;255&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)])&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BAATError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Payload too large&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;prefix&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;lower&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;crc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;crc32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;utf-8&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;magic&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;baat_ver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xFFFFFFFF&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;wrapped_data_b32&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;b32encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;magic&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;baat_ver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;return &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prefix&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapped_data_b32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;utf-8&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;lower&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;parse_baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;parts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BAATError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Malformed&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;prefix&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;lower&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;wrapped_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;b32decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;upper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrapped_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BAATError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Impossible length&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;magic&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapped_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;baat_ver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapped_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;magic&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x8f\xa5&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BAATError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Invalid magic number&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;baat_ver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x01&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BAATError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Invalid BAAT version&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrapped_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;25&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BAATError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Wrong length&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapped_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;crc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;crc32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;utf-8&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;magic&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;baat_ver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xFFFFFFFF&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapped_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BAATError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Invalid CRC&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;is_baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;parse_baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ValueError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;__main__&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;255&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)])&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;baat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;make_baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;bat&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;parsed_payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;parse_baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;is_baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;baat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parsed_payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Wed, 18 Oct 2023 15:15:57 -0700</pubDate>
        <link>https://www.finnie.org/2023/10/18/better-assembled-access-tokens/</link>
        <guid isPermaLink="true">https://www.finnie.org/2023/10/18/better-assembled-access-tokens/</guid>
        
        
      </item>
    
      <item>
        <title>ChatGPT unsettled me</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=jPhJbKBuNnA&quot;&gt;Tom Scott recently put out a video&lt;/a&gt; where he had a “minor existential crisis” after giving ChatGPT a coding task. His conclusion was basically, this works better than it should, and that’s unsettling. After watching this, I had my own minor coding task which I decided to give to ChatGPT, and, spoiler alert, I am also unsettled.&lt;/p&gt;

&lt;p&gt;The problem I needed to solve was I have an old Twitter bot which had automatically followed a bunch of people over the years, and I wanted to clear out those follows. As of this writing, Twitter’s API service seems to inexplicably still exist, but the single-purpose OAauth “app” associated with that account was for API v1.1, not v2, so I needed to use API v1.1 calls.&lt;/p&gt;

&lt;p&gt;I’d done a lot of Twitter API work over the years, and a lot of that was through Python, so I was ready to kitbash something together using existing code snippets.  But let’s see what ChatGPT would do if given the opportunity:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Write a script in Python to use the Twitter API v1.1 to get a list of all friends and then unsubscribe from them&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And yeah, it created a correctly-formatted, roughly 25 line Python script to do this exactly. It even gave a warning that API access requires authentication, and, amusingly, that unsubscribing from all friends would affect the account’s “social reach”.&lt;/p&gt;

&lt;p&gt;(I’m summarizing its responses here; a full chat log, including code at every step, is available at the end of this post.)&lt;/p&gt;

&lt;p&gt;One drawback to the specific situation was it wrote the script to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tweepy&lt;/code&gt; library, which I had never heard of and wasn’t sure if it was using API v1.1 (though I suspected it was, from the library function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destroy_friendship()&lt;/code&gt;; “friendships” are verbs in v1.1 but not v2). Nonetheless, I was more familiar with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests_oauthlib&lt;/code&gt; and the direct API endpoints, so I just asked ChatGPT to rewrite it to use that.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Can we use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests_oauthlib&lt;/code&gt; library instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tweepy&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sure enough, it produced exactly what I wanted, and I ended up using it for my task.&lt;/p&gt;

&lt;p&gt;Everything beyond this was “what-ifs” to poke at ChatGPT.  The first thing I noticed was it was using a less efficient API endpoint. Thinking back to Tom’s video where he realized he could simply ask ChatGPT why it did something a certain way, I realized I could simply say:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;That works, but the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/ids.json&lt;/code&gt; endpoint allows for 5000 results per request, versus 200 on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/list.json&lt;/code&gt; as you pointed out. Let’s use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/ids.json&lt;/code&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ChatGPT’s response was basically “yep, I agree that’s more efficient; here’s an updated script!”, utilizing the new endpoint and specifying the new 5000 user limit.&lt;/p&gt;

&lt;p&gt;This was a subtle test for it since the endpoint I suggested is very similar to the old one, but not a drop-in replacement.  You need to make a few minor changes elsewhere in the script to utilize it.  ChatGPT passed this test and updated both the endpoint name and the required changes.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I’m using Python 3.5 and can’t use f-strings. Can you rewrite the code to use string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format()&lt;/code&gt; instead?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’m not actually using Python 3.5, but I wanted to ask this as f-strings and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format()&lt;/code&gt; are very different looking ways to format strings. It rewrote the script correctly.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Can you package this up to run in a GitHub Actions workflow?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It gave me the Python script it had been working on up until this point, and a workflow YAML file, along with instructions on where to put them. The workflow format was correct, and, impressively, remembered my off-hand requirement of Python 3.5 from earlier (even though the context of that was about a specific feature, not Python 3.5 specifically), and made sure to use 3.5 when setting up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actions/setup-python@v2&lt;/code&gt;.  It also knew that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests_oauthlib&lt;/code&gt; were not stdlib, so it had the workflow install them through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The workflow did use the GitHub Actions secret store to correctly use the secrets and pass them as environment variable, but up until this moment, the script itself had hardcoded placeholders.  I asked ChatGPT to actually use the environment variables.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The workflow file is correctly passing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONSUMER_KEY&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONSUMER_SECRET&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCESS_TOKEN&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCESS_TOKEN_SECRET&lt;/code&gt; secrets as environment variables to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt;, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt; still has the placeholder variables. Can you modify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt; to use the environment variables?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Again, it did this without a problem.&lt;/p&gt;

&lt;p&gt;In conclusion, I am impressed by ChatGPT’s performance, to the extent of being unsettled by the experience. There are many logical, legal and ethical problems with AI generation and I don’t want to minimize those concerns, but I just wanted to document my limited experience with using it for a coding exercise.&lt;/p&gt;

&lt;p&gt;(It’s also not lost on me the irony of using current-generation AI to fiddle with the account of X11R5, an old MegaHAL Markov bot which is decidedly more ancient AI.)&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;complete-chat-log&quot;&gt;Complete chat log&lt;/h2&gt;

&lt;p&gt;Here is the entire session log between ChatGPT and myself:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Write a script in Python to use the Twitter API v1.1 to get a list of all friends and then unsubscribe from them&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before proceeding with the code, please note that the Twitter API v1.1 requires authentication, which means you need to have valid credentials to access the API. Also, please be aware that unsubscribing from all friends can have significant consequences on your account’s social reach, and it should not be done lightly.&lt;/p&gt;

&lt;p&gt;To use the Twitter API v1.1 in Python, we need to first install the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tweepy&lt;/code&gt; library. We can use the following code to install it:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pip &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;tweepy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tweepy&lt;/code&gt; is installed, we can use the following script to get a list of all friends and then unsubscribe from them:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tweepy&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# set up Twitter API credentials
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your_consumer_key&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your_consumer_secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your_access_token&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your_access_token_secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# authenticate with the Twitter API
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tweepy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OAuthHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;set_access_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# create an API object
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tweepy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;API&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# get a list of all friends
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friends&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;api&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;friends_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# loop through the list of friends and unsubscribe from them
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friends&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;api&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;destroy_friendship&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this script, we first set up our Twitter API credentials, then authenticate with the Twitter API using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tweepy&lt;/code&gt;. We then create an API object and use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends_ids()&lt;/code&gt; method to get a list of all friends.&lt;/p&gt;

&lt;p&gt;Finally, we loop through the list of friends and use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destroy_friendship()&lt;/code&gt; method to unsubscribe from each friend. Note that this is a permanent action, so use it with caution.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Can we use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests_oauthlib&lt;/code&gt; library instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tweepy&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes, we can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests_oauthlib&lt;/code&gt; library instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tweepy&lt;/code&gt; to interact with the Twitter API v1.1 in Python. Assuming you have valid credentials and have installed the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests_oauthlib&lt;/code&gt; library, here’s an example script to get a list of all friends and then unsubscribe from them using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests_oauthlib&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests_oauthlib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OAuth1Session&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# replace with your own credentials
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your consumer key&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your consumer secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your access token&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your access token secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# set up OAuth1Session
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OAuth1Session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;resource_owner_key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;resource_owner_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# get a list of all friends
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friends/list.json&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;friend_list&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# unsubscribe from each friend
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friendships/destroy.json?user_id=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Unsubscribed from &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;screen_name&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Failed to unsubscribe from &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;screen_name&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this script, we first set up an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OAuth1Session&lt;/code&gt; object with our Twitter API credentials. We then use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get&lt;/code&gt; method to retrieve a list of all friends (limited to 200 per request, as per the API’s limitations). We extract the list of friends from the JSON response using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;We then loop through each friend in the list and use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post&lt;/code&gt; method to unsubscribe from each friend by making a request to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friendships/destroy.json&lt;/code&gt; endpoint with the friend’s ID as a parameter. We print a message indicating whether the unsubscribe request was successful or not.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;That works, but the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/ids.json&lt;/code&gt; endpoint allows for 5000 results per request, versus 200 on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/list.json&lt;/code&gt; as you pointed out. Let’s use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/ids.json&lt;/code&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You’re right, using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/ids.json&lt;/code&gt; endpoint would allow us to retrieve up to 5000 friend IDs per request, which would be more efficient than using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/list.json&lt;/code&gt; endpoint. Here’s an updated script that uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/ids.json&lt;/code&gt; endpoint:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests_oauthlib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OAuth1Session&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# replace with your own credentials
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your consumer key&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your consumer secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your access token&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your access token secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# set up OAuth1Session
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OAuth1Session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;resource_owner_key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;resource_owner_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# get a list of all friend IDs
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friends/ids.json&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;friend_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ids&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# unsubscribe from each friend
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friendships/destroy.json?user_id=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Unsubscribed from friend ID &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Failed to unsubscribe from friend ID &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this updated script, we use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friends/ids.json&lt;/code&gt; endpoint to retrieve a list of all friend IDs, with a maximum count of 5000 per request. We extract the list of friend IDs from the JSON response using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;We then loop through each friend ID in the list and use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post&lt;/code&gt; method to unsubscribe from each friend by making a request to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;friendships/destroy.json&lt;/code&gt; endpoint with the friend’s ID as a parameter. We print a message indicating whether the unsubscribe request was successful or not.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I’m using Python 3.5 and can’t use f-strings. Can you rewrite the code to use string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format()&lt;/code&gt; instead?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sure! Here’s the same code using string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format()&lt;/code&gt; instead of f-strings:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests_oauthlib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OAuth1Session&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# replace with your own credentials
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your consumer key&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your consumer secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your access token&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your access token secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# set up OAuth1Session
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OAuth1Session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;resource_owner_key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;resource_owner_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# get a list of all friend IDs
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friends/ids.json&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;friend_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ids&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# unsubscribe from each friend
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friendships/destroy.json?user_id={}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Unsubscribed from friend ID {}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Failed to unsubscribe from friend ID {}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this updated code, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format()&lt;/code&gt; to insert the friend ID into the string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;url&lt;/code&gt; and the message strings.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Can you package this up to run in a GitHub Actions workflow?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sure, here’s how you could package the code in a GitHub Actions workflow:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Create a new Python file in your repository called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt; with the following content:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests_oauthlib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OAuth1Session&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# replace with your own credentials
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your consumer key&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your consumer secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your access token&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;your access token secret&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# set up OAuth1Session
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OAuth1Session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;resource_owner_key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;resource_owner_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# get a list of all friend IDs
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friends/ids.json&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;friend_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ids&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# unsubscribe from each friend
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friendships/destroy.json?user_id={}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Unsubscribed from friend ID {}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Failed to unsubscribe from friend ID {}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ol&gt;
  &lt;li&gt;Create a new file in your repository called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/workflows/twitter_unfollow.yml&lt;/code&gt; with the following content:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Twitter Unfollow&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;main&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;cron&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&apos;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# run every day at midnight&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;unfollow&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/checkout@v2&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Set up Python &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;3.5&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/setup-python@v2&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;python-version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3.5&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Install dependencies&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;python -m pip install --upgrade pip&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;pip install requests requests_oauthlib&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Unfollow Twitter friends&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;python twitter_unfollow.py&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;CONSUMER_KEY&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;$&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;CONSUMER_SECRET&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;$&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;ACCESS_TOKEN&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;$&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;ACCESS_TOKEN_SECRET&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This workflow runs the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt; script every day at midnight, using Python 3.5. It installs the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests_oauthlib&lt;/code&gt; packages using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt;, and sets the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONSUMER_KEY&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONSUMER_SECRET&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCESS_TOKEN&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCESS_TOKEN_SECRET&lt;/code&gt; environment variables using GitHub Secrets. Be sure to replace the placeholder strings in the workflow file with the actual values of your credentials.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The workflow file is correctly passing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONSUMER_KEY&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONSUMER_SECRET&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCESS_TOKEN&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCESS_TOKEN_SECRET&lt;/code&gt; secrets as environment variables to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt;, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt; still has the placeholder variables. Can you modify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt; to use the environment variables?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sure, you can modify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;twitter_unfollow.py&lt;/code&gt; to use the environment variables like this:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests_oauthlib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OAuth1Session&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# get credentials from environment variables
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;CONSUMER_KEY&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;CONSUMER_SECRET&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ACCESS_TOKEN&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ACCESS_TOKEN_SECRET&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# set up OAuth1Session
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OAuth1Session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consumer_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;resource_owner_key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;resource_owner_secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;access_token_secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# get a list of all friend IDs
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friends/ids.json&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;friend_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ids&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# unsubscribe from each friend
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;friend_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://api.example.com/1.1/friendships/destroy.json?user_id={}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;twitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Unsubscribed from friend ID {}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Failed to unsubscribe from friend ID {}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;friend_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this updated code, we use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;os.environ&lt;/code&gt; dictionary to get the values of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONSUMER_KEY&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONSUMER_SECRET&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCESS_TOKEN&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACCESS_TOKEN_SECRET&lt;/code&gt; environment variables.&lt;/p&gt;
</description>
        <pubDate>Tue, 14 Feb 2023 21:23:33 -0800</pubDate>
        <link>https://www.finnie.org/2023/02/14/chatgpt-unsettled-me/</link>
        <guid isPermaLink="true">https://www.finnie.org/2023/02/14/chatgpt-unsettled-me/</guid>
        
        
      </item>
    
      <item>
        <title>IPv6 Prefix Delegation on Spectrum with dhclient</title>
        <description>&lt;p&gt;A few years ago, Spectrum (a US cable company formed from the combination of Charter and Time Warner Cable) started offering IPv6 Prefix Delegation (PD) /64s.  The device connected to the cable modem will normally get an individual global dynamic IPv6 address via Router Advertisement, but Prefix Delegation is essentially the ability to request an entire network to be routed to you.&lt;/p&gt;

&lt;p&gt;I used to live in Reno in a formerly Charter network, but recently moved to Southern California in a formerly Time Warner network, so I’m confident this information applies to all Spectrum regions.  The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhclient&lt;/code&gt; invocation should work for any provider which supports Prefix Delegation, but the lease behavior I describe is probably not universal.&lt;/p&gt;

&lt;p&gt;Here’s the systemd &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhclient6-pd.service&lt;/code&gt; file on my router, a Raspberry Pi 4 connected directly to the cable modem.  Replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eext0&lt;/code&gt; with your external interface name.&lt;/p&gt;
&lt;div class=&quot;language-systemd highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;[Unit]&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;Description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;IPv6 PD lease reservation
&lt;span class=&quot;nt&quot;&gt;Wants&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;network-online.target
&lt;span class=&quot;nt&quot;&gt;After&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;network-online.target
&lt;span class=&quot;nt&quot;&gt;StartLimitIntervalSec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;0

&lt;span class=&quot;k&quot;&gt;[Service]&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;Restart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;always
&lt;span class=&quot;nt&quot;&gt;RestartSec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;30
&lt;span class=&quot;nt&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;/sbin/dhclient -d -6 -P -v -lf /var/lib/dhcp/dhclient6-pd.leases eext0

&lt;span class=&quot;k&quot;&gt;[Install]&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;WantedBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once running, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhclient6-pd.leases&lt;/code&gt; should give you something like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;default-duid &quot;\000\001\000\001#\225\311g\000\006%\243\332{&quot;;
lease6 {
  interface &quot;eext0&quot;;
  ia-pd 25:a3:da:7b {
    starts 1628288112;
    renew 1800;
    rebind 2880;
    iaprefix 2600:6c51:4d00:ff::/64 {
      starts 1628288112;
      preferred-life 3600;
      max-life 3600;
    }
  }
  option dhcp6.client-id 0:1:0:1:23:95:c9:67:0:6:25:a3:da:7b;
  option dhcp6.server-id 0:1:0:1:4b:73:43:3a:0:14:4f:c3:f6:90;
  option dhcp6.name-servers 2607:f428:ffff:ffff::1,2607:f428:ffff:ffff::2;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So now I can see that 2600:6c51:4d00:ff::/64 is routable to me, and can set up network addresses and services.  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhclient&lt;/code&gt; could be set up to run scripts on trigger events, but in this current state it just keeps the PD reservation.&lt;/p&gt;

&lt;p&gt;But… &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-life 3600&lt;/code&gt;?  Does that mean I’ll lose the PD if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhclient&lt;/code&gt; doesn’t check in within an hour? What if I have a power outage?  Yes, you will lose the PD after an hour if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhclient&lt;/code&gt; isn’t running… for now.  After a few renewals, the far end will trust that your initial PD request wasn’t a drive-by, and will up the period from 1 hour to 7 days, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhclient6-pd.leases&lt;/code&gt; will look like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;default-duid &quot;\000\001\000\001#\225\311g\000\006%\243\332{&quot;;
lease6 {
  interface &quot;eext0&quot;;
  ia-pd 25:a3:da:7b {
    starts 1628288112;
    renew 1800;
    rebind 2880;
    iaprefix 2600:6c51:4d00:ff::/64 {
      starts 1628288112;
      preferred-life 3600;
      max-life 3600;
    }
  }
  option dhcp6.client-id 0:1:0:1:23:95:c9:67:0:6:25:a3:da:7b;
  option dhcp6.server-id 0:1:0:1:4b:73:43:3a:0:14:4f:c3:f6:90;
  option dhcp6.name-servers 2607:f428:ffff:ffff::1,2607:f428:ffff:ffff::2;
}
lease6 {
  interface &quot;eext0&quot;;
  ia-pd 25:a3:da:7b {
    starts 1628291743;
    renew 300568;
    rebind 482008;
    iaprefix 2600:6c51:4d00:ff::/64 {
      starts 1628291743;
      preferred-life 602968;
      max-life 602968;
    }
  }
  option dhcp6.client-id 0:1:0:1:23:95:c9:67:0:6:25:a3:da:7b;
  option dhcp6.server-id 0:1:0:1:4b:73:43:3a:0:14:4f:c3:f6:90;
  option dhcp6.name-servers 2607:f428:ffff:ffff::1,2607:f428:ffff:ffff::2;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(The last &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lease6&lt;/code&gt; is the most recent lease received.)&lt;/p&gt;

&lt;p&gt;As far as I can tell, this 7 day PD can be renewed indefinitely; I was using the same network for nearly 2 years.  But be warned: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-life&lt;/code&gt; is final.  If you have a misconfiguration and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhclient&lt;/code&gt; doesn’t check in for a week, Spectrum will release your PD immediately after 7 days and your client will receive a completely new /64.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Since this is fresh in my mind from setting up my new home, here are a few things to set up on your core router, but this is not meant to be an exhaustive IPv6 Linux router guide.&lt;/p&gt;

&lt;p&gt;The external interface automatically gets a global dynamic v6 address; as for the internal interface, while you technically don’t need a static address thanks to link-local routing, in practice you should give it one.  Here’s my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/systemd/network/10-eint0.network&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Match]
Name=eint0

[Network]
Address=10.9.8.1/21
Address=2600:6c51:4d00:ff::1/64
Address=fe80::1/128
IPv6AcceptRA=false
IPForward=true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You’ll also want an RA daemon for the internal network.  My &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/radvd.conf&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;interface eint0 {
  IgnoreIfMissing on;
  MaxRtrAdvInterval 2;
  MinRtrAdvInterval 1.5;
  AdvDefaultLifetime 9000;
  AdvSendAdvert on;
  AdvManagedFlag on;
  AdvOtherConfigFlag on;
  AdvHomeAgentFlag off;
  AdvDefaultPreference high;
  prefix 2600:6c51:4d00:ff::/64 {
    AdvOnLink on;
    AdvAutonomous on;
    AdvRouterAddr on;
    AdvValidLifetime 2592000;
    AdvPreferredLifetime 604800;
  };
  RDNSS 2600:6c51:4d00:ff::1 {
  };
};
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And a DHCPv6 server.  My &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/dhcp/dhcpd6.conf&lt;/code&gt;, providing information about DNS and DHCP-assigned addressing (in addition to the RA autoconfiguration):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;default-lease-time 2592000;
preferred-lifetime 604800;
option dhcp-renewal-time 3600;
option dhcp-rebinding-time 7200;
allow leasequery;
option dhcp6.preference 255;
option dhcp6.rapid-commit;
option dhcp6.info-refresh-time 21600;
option dhcp6.name-servers 2600:6c51:4d00:ff::1;
option dhcp6.domain-search &quot;snowman.lan&quot;;

subnet6 2600:6c51:4d00:ff::/64 {
  range6 2600:6c51:4d00:ff::c0c0:0 2600:6c51:4d00:ff::c0c0:ffff;
}

host workstation {
  host-identifier option dhcp6.client-id 00:01:00:01:21:37:85:10:01:23:45:ab:cd:ef;
  fixed-address6 2600:6c51:4d00:ff::2;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Sat, 07 Aug 2021 15:10:09 -0700</pubDate>
        <link>https://www.finnie.org/2021/08/07/ipv6-prefix-delegation-on-spectrum-with-dhclient/</link>
        <guid isPermaLink="true">https://www.finnie.org/2021/08/07/ipv6-prefix-delegation-on-spectrum-with-dhclient/</guid>
        
        
      </item>
    
      <item>
        <title>The Repository Run-Parts CI Directory (RRPCID) specification</title>
        <description>&lt;p&gt;Years ago, I wrote &lt;a href=&quot;https://forge.colobox.com/rfinnie/dsari&quot;&gt;dsari&lt;/a&gt; (“Do Something And Record It”), a lightweight CI system. This was prompted by administering Jenkins installations for multiple development groups at the time, each environment having increasingly specialized (and often incompatible) plugins layered onto the core functionality.&lt;/p&gt;

&lt;p&gt;This led me to take the opposite approach.  I made a CI system based on one executable (usually a script) per job, and the assumption that you, the CI job developer, know exactly what functionality you want.  Want custom notifications?  &lt;a href=&quot;https://forge.colobox.com/rfinnie/dsari/src/HEAD/doc/notifications.md&quot;&gt;Write it into the script.&lt;/a&gt;  Sub-job triggers based on the result of the run?  &lt;a href=&quot;https://forge.colobox.com/rfinnie/dsari/src/HEAD/doc/triggers.md&quot;&gt;You can totally do that.&lt;/a&gt;  Remote agents?  Bah, just tell the script to ssh to a remote system &lt;a href=&quot;https://forge.colobox.com/rfinnie/dsari/src/HEAD/doc/concurrency.md&quot;&gt;based on the concurrency group the run is currently in&lt;/a&gt;.  dsari’s acronym was a light-hearted take on this simplicity.&lt;/p&gt;

&lt;p&gt;Fast forward to now, and GitHub’s CI has quickly become ubiquitous.  But before that, Travis essentially pushed the idea of in-repository CI definitions, as opposed to a CI job being built around the repository as in Jenkins.  As an example, &lt;a href=&quot;https://forge.colobox.com/finnix/finnix-live-build&quot;&gt;finnix-live-build&lt;/a&gt; has a GitHub workflow which makes a test build, but I also have multiple dsari instances at home, for different architectures, doing the same thing on schedule.&lt;/p&gt;

&lt;p&gt;However, the dsari job script merely replicates the build process of the GitHub workflow.  If I add new functionality to the GitHub workflow, I need to also update build scripts on 5 different machines.  This would be great to move in-repository, but I quickly found there is no established general-purpose in-repository CI layout.&lt;/p&gt;

&lt;p&gt;So let’s make one!&lt;/p&gt;

&lt;p&gt;If the closest simplification to the Jenkins CI model is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cron&lt;/code&gt;, the closest simplification to the GitHub CI model is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run-parts&lt;/code&gt;.  However, since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run-parts&lt;/code&gt; has different functionality on different systems (with Debian’s implementation currently being the most versatile), the “Run-Parts” part of the RRPCID acronym is in spirit only (though you could use Debian’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run-parts --exit-on-error&lt;/code&gt; for the job processing part of the RRPCID logic).&lt;/p&gt;

&lt;p&gt;Here’s the specification I came up with:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A workflow is a collection of jobs, and is a readable directory under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.rrpcid/workflows/&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;A job is a collection of actions, and is a readable directory under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;${workflow_dir}/jobs/&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;An action is an executable file under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;${job_dir}/&lt;/code&gt;.  In theory this can be anything, but is likely to be a shell script.&lt;/li&gt;
  &lt;li&gt;Actions are executed with the repository as the current working directory.&lt;/li&gt;
  &lt;li&gt;Actions are executed with the environment variable CI=true. Other environment variables may be passed in from the underlying CI manager.&lt;/li&gt;
  &lt;li&gt;Actions are executed in lexical sort order within the job directory.&lt;/li&gt;
  &lt;li&gt;Workflow, job and action names must only contain letters a through z and A through Z, numbers 0 through 9, and characters “-“ (dash) and “_” (underscore).  Note specifically the lack of “.” (period).&lt;/li&gt;
  &lt;li&gt;A repository may have multiple workflows, a workflow may have multiple jobs, and a job may have multiple actions.&lt;/li&gt;
  &lt;li&gt;If an action exits with a status other than 0, further actions in a job are skipped.&lt;/li&gt;
  &lt;li&gt;All jobs in a workflow are run, regardless of whether other jobs’ actions have failed.&lt;/li&gt;
  &lt;li&gt;Actions within the job must not assume another job has previously run.&lt;/li&gt;
  &lt;li&gt;An implicit, unnamed workflow lives directly in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.rrpcid/&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Whether all, some or none of the repository’s workflows are run is up to the CI manager; that logic is outside the scope of this specification.&lt;/li&gt;
  &lt;li&gt;All other files and directories are ignored. For example, a directory named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.rrpcid/testdata/&lt;/code&gt; is outside the scope of this specification, and would not be handled.&lt;/li&gt;
  &lt;li&gt;A recommended directory for generated artifacts is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;artifacts/&lt;/code&gt; within the workflow directory, but a CI manager is not required to do anything with this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A script layout utilizing multiple workflows (including the implicit unnamed workflow) and multiple jobs might look like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.rrpcid/jobs/ci/action_1
.rrpcid/jobs/ci/action_2
.rrpcid/jobs/lint/run-lint
.rrpcid/workflows/deploy/jobs/env1/deploy
.rrpcid/workflows/deploy/jobs/env2/deploy
.rrpcid/workflows/deploy/jobs/archive/01tar
.rrpcid/workflows/deploy/jobs/archive/02upload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The following shell code would satisfy the above requirements, assuming it’s being run from dash/bash (both sort “*” matches which are needed for the job directory; other Bourne shells may not).  It satisfies the requirements, but is by no means the only way to implement an RRPCID processor.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;CI&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true
&lt;/span&gt;run_workflow&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;job_dir &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/jobs&quot;&lt;/span&gt;/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;job_dir&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;job_dir&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;job_dir&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/[a-zA-Z0-9_-]//g&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue
        for &lt;/span&gt;action &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;job_dir&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/[a-zA-Z0-9_-]//g&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
            &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;break
        &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;done
    done&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

run_workflow .rrpcid
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;workflow_dir &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; .rrpcid/workflows/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;workflow_dir&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;workflow_dir&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;workflow_dir&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/[a-zA-Z0-9_-]//g&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue
    &lt;/span&gt;run_workflow &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;workflow_dir&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://forge.colobox.com/finnix/finnix-live-build/tree/HEAD/.rrpcid&quot;&gt;finnix-live-build now has a simple RRPCID job&lt;/a&gt;, though as of this writing I have not yet switched over the home dsari jobs to utilize it.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Side note / rant: I went back and forth on whether to allow “.” as part of the names, specifically the action script names.  Historically, ignoring “.” has been traditional for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run-parts&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cron.d&lt;/code&gt;, etc, because it ignores automatically-created files such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo.bak&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo.swp&lt;/code&gt;, etc.  However, I acknowledge using extensions for executable scripts within a project (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sh&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.py&lt;/code&gt;, etc) is currently popular.&lt;/p&gt;

&lt;p&gt;My answer to this? Stop Doing That. I can’t tell you how many times I’ve seen someone (including me) put &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;do_cool_thing.sh&lt;/code&gt; into a repository, and over time it gets expanded to the point it’s too complicated to be effectively managed as a shell script, and is rewritten in, say, Python.  The problem is references to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;do_cool_thing.sh&lt;/code&gt; are now too entrenched, and you now have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;do_cool_thing.sh&lt;/code&gt; which is actually a Python script (!!!), or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;do_cool_thing.sh&lt;/code&gt; which is just a wrapper call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;do_cool_thing&lt;/code&gt; (if I learned my lesson) or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;do_cool_thing.py&lt;/code&gt; (if I didn’t).&lt;/p&gt;

&lt;p&gt;Just drop the extension when creating a script.  Shebangs exist for a reason. 😉&lt;/p&gt;

&lt;p&gt;By the way, if you do find yourself in this migration situation, here’s a general-purpose redirect script for the old location:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/sh&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; .sh&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Mon, 17 May 2021 16:21:17 -0700</pubDate>
        <link>https://www.finnie.org/2021/05/17/the-repository-runparts-ci-directory-rrpcid-specification/</link>
        <guid isPermaLink="true">https://www.finnie.org/2021/05/17/the-repository-runparts-ci-directory-rrpcid-specification/</guid>
        
        
      </item>
    
      <item>
        <title>Want to hire me? Let&apos;s talk careers.</title>
        <description>&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Hello! I am a highly experienced Linux systems engineer, looking to work with the right team. If you are in need of a senior SRE with a focus on operational development, or a developer with a focus on design for infrastructure, &lt;a href=&quot;https://www.finnie.org/Ryan-Finnie-resume.pdf&quot;&gt;here’s my resume&lt;/a&gt;; I’d love to talk with you!&lt;/p&gt;

&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;

&lt;p&gt;I left my previous employer last year, having planned to take a several month sabbatical. In a stroke of… interesting timing, my last day was the first week of March 2020.  With COVID and lockdowns and the world in turmoil, I decided to extend my sabbatical and work on a bunch of personal projects.&lt;/p&gt;

&lt;p&gt;A year has passed and I’m ready to join the career world again.&lt;/p&gt;

&lt;h2 id=&quot;about-me&quot;&gt;About me&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://www.finnie.org/Ryan-Finnie-resume.pdf&quot;&gt;My resume&lt;/a&gt; contains the essential details, but the nice thing about a blog post is it allows me to be more fluid. So let’s be fluid!&lt;/p&gt;

&lt;p&gt;My immediate previous work experience was 8 years as a Site Reliability Engineer at Canonical, the company behind Ubuntu.  Canonical was an 80% remote work company (and since became 100% since COVID), and I worked within a group of about 20 SREs, supporting the company’s operations, as well as interacting directly with the open source community.  While I have experience with many Linux distributions, suffice it to say, I know Ubuntu inside and out.&lt;/p&gt;

&lt;p&gt;I’ve been doing Linux systems engineering for over 20 years, and have worked with many people across the open source world.  My claim to fame is &lt;a href=&quot;https://www.finnix.org/&quot;&gt;Finnix&lt;/a&gt;, a bootable utility Linux distribution (LiveCD), geared toward system administration, rescue and recovery, etc.  I am a &lt;a href=&quot;https://qa.debian.org/developer.php?email=ryan%40finnie.org&quot;&gt;Debian maintainer&lt;/a&gt;, an &lt;a href=&quot;https://launchpad.net/~fo0bar&quot;&gt;Ubuntu technical member&lt;/a&gt;, and additionally have packaging experience with Fedora and Homebrew.&lt;/p&gt;

&lt;p&gt;While I can pick up nearly any programming language, I describe myself as a prolific Python programmer.  For a portfolio example of my current Python ability, see &lt;a href=&quot;https://forge.colobox.com/rfinnie/rf-pymods&quot;&gt;rf-pymods&lt;/a&gt;, a collection of standalone helper modules.  Docstrings on each function, 100% code coverage on unit tests, tox, GitHub workflows.  For a more realistic example, see &lt;a href=&quot;https://forge.colobox.com/rfinnie/2ping&quot;&gt;2ping&lt;/a&gt;, a network investigation utility which was developed in 2010 and has been updated and maintained since.  Tox, CI, test framework (but not (yet) 100% coverage), reasonable code and functional documentation.&lt;/p&gt;

&lt;p&gt;Public cloud (I wrote the caching proxy software running the per-region Ubuntu mirrors on AWS, Azure and GCE). Private cloud (nearly a decade of OpenStack experience). Containers. Continuous integration (I even have my own lightweight CI system called &lt;a href=&quot;https://forge.colobox.com/rfinnie/dsari&quot;&gt;dsari&lt;/a&gt;). The list goes on. And yes, &lt;a href=&quot;https://www.finnie.org/2020/07/14/git-branchbased-contribution-workflow-management/&quot;&gt;I know Git&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;about-you&quot;&gt;About you&lt;/h2&gt;

&lt;p&gt;The top consideration I have with a potential employer is a healthy remote work lifestyle.  I’ve been working from home for 10 years now, and recognize the strengths (and weaknesses) of a remote work setup. COVID caused many companies to shoehorn in work-from-home into their existing business strategy on short notice, while I’ve been most pleased with companies who have remote collaboration as part of their DNA.&lt;/p&gt;

&lt;p&gt;That being said, I’m looking for a senior SRE position which has a focus on operational development.  This can also take the form of a development role which has a focus on design for infrastructure.  In many ways, these are one in the same.  Open source development and contribution is strongly preferred; I do most of my work in the open, and value companies which do the same.&lt;/p&gt;

&lt;p&gt;I’ve had experience with startups and are not opposed to them, but would prefer a mid-sized established company or a late-stage startup.  Industry is not as important as the people and the teams.  I am based on the US west coast and have extensive experience working with geographically distributed teams.&lt;/p&gt;

&lt;h2 id=&quot;lets-talk&quot;&gt;Let’s talk&lt;/h2&gt;

&lt;p&gt;If you’re excited, &lt;a href=&quot;https://www.finnie.org/Ryan-Finnie-resume.pdf&quot;&gt;here’s my resume&lt;/a&gt;, &lt;a href=&quot;https://github.com/rfinnie&quot;&gt;here’s my GitHub profile&lt;/a&gt;, &lt;a href=&quot;https://www.finnix.org/&quot;&gt;go give Finnix a try&lt;/a&gt;, etc, then &lt;a href=&quot;mailto:ryan@finnie.org&quot;&gt;send me an email&lt;/a&gt;.  I’d love to talk with you.&lt;/p&gt;
</description>
        <pubDate>Mon, 26 Apr 2021 10:00:00 -0700</pubDate>
        <link>https://www.finnie.org/2021/04/26/want-to-hire-me-lets-talk-careers/</link>
        <guid isPermaLink="true">https://www.finnie.org/2021/04/26/want-to-hire-me-lets-talk-careers/</guid>
        
        
      </item>
    
      <item>
        <title>(Spinning) Rust begone!</title>
        <description>&lt;p&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2021/derust-pyramid.jpg&quot; alt=&quot;33 hard drives arranged in a pyramid pattern&quot; class=&quot;img-responsive img-rounded img-lg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You could say I have a few computers… 63 as of this writing (I made a
spreadsheet), though about half of those are SBCs (single board
computers; Raspberry Pis and similar). However, many of the other
computers are old, and include mechanical hard drives in various states
of failure. About a year ago, I began a quest to eliminate as many
mechanical hard drives from my collection as possible.&lt;/p&gt;

&lt;h2 id=&quot;25-sata&quot;&gt;2.5” SATA&lt;/h2&gt;

&lt;p&gt;This one’s simple, just replace the 2.5” SATA HDD with a 2.5” SATA SSD.&lt;/p&gt;

&lt;h2 id=&quot;35-sata&quot;&gt;3.5” SATA&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2021/derust-3.5-sata-underside.jpg&quot; alt=&quot;3D printed 2.5 inch to 3.5 inch drive adapter with case-specific standoffs and an SSD installed&quot; class=&quot;img-responsive img-rounded img-md pull-right&quot; /&gt;Almost as simple as 2.5” SATA, just replace with a 2.5” SATA SSD and a 2.5” to 3.5” adapter bracket. However, it took me awhile to find the ideal bracket, &lt;a href=&quot;https://www.thingiverse.com/thing:29628&quot;&gt;a 3D printed universal-ish adapter&lt;/a&gt; (specifically the “minimalist sunk screwholes” variant). It can mount the underside of an SSD in several positions, and has 3.5” holes for side or underside mounting.&lt;/p&gt;

&lt;p&gt;Note that while the SATA/power ports are close to a normal 3.5” drive’s location, it’s not exact. For a situation where you need to plug into a backplane (like a server or NAS), you’ll need a &lt;a href=&quot;https://www.amazon.com/General-Drive-HDD-Adapter-CADDY/dp/B00F3QFKNS/&quot;&gt;caddy adapter which re-routes the ports&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&quot;clearfix&quot;&gt;&lt;/div&gt;

&lt;h2 id=&quot;35-ide&quot;&gt;3.5” IDE&lt;/h2&gt;

&lt;div class=&quot;pull-right&quot;&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2021/derust-sd-slot.jpg&quot; alt=&quot;SD to IDE adapter in a 3D printed PCI/ISA bracket&quot; class=&quot;img-responsive img-rounded img-md&quot; /&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2021/derust-sd-3.5.jpg&quot; alt=&quot;SD to IDE adapter in a 3D printed 3.5 inch mount&quot; class=&quot;img-responsive img-rounded img-md&quot; /&gt;&lt;/div&gt;
&lt;p&gt;These became the majority of my conversions. Since we’re talking about older IDE devices, the speed of a modern SSD isn’t needed, so &lt;a href=&quot;https://www.amazon.com/KOOBOOK-1Pcs-40Pin-Adapter-Drive/dp/B07YFPX7JB/&quot;&gt;SD to IDE adapters&lt;/a&gt; work extremely well. (The linked product is only an example; it’s a common design sold by many sellers under similar names.) I’ve had success across a range of devices, from a 486 desktop to a Sun Blade 100 workstation to a Power Mac G4 desktop.&lt;/p&gt;

&lt;p&gt;The preferred mounting option is in a PCI / ISA slot, so you can remove the SD card and mount it on a more modern computer if needed. I designed &lt;a href=&quot;https://www.thingiverse.com/thing:4686963&quot;&gt;a reinforced bracket for this purpose&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Alternatively, you can mount it in a 3.5” bay, either as an external 3.5” device (i.e. a floppy bay) or internal, if all else fails. Again, &lt;a href=&quot;https://www.thingiverse.com/thing:4572090&quot;&gt;I have designed a 3D-printed mount for these situations&lt;/a&gt;. The same mount can either be used internally or externally, with side or underside 3.5” mounting. Note that the adapter’s IDE and power ports are nowhere near a normal hard drive’s positioning, and the top-mounted Molex port is rarely in a convenient location, so you’ll also want to get a short 40-pin extension ribbon cable, and a short floppy power to Molex adapter.&lt;/p&gt;

&lt;h2 id=&quot;35-ide-for-stubborn-computers&quot;&gt;3.5” IDE (for stubborn computers)&lt;/h2&gt;

&lt;p&gt;The only computer I found which doesn’t like the SD to IDE adapter is the Power Mac G3 Blue &amp;amp; White. Mac OS 8.6 sees the drive, but would somehow encounter I/O timeouts any time it tried to use it. For this machine, I went with a CompactFlash card and &lt;a href=&quot;https://www.amazon.com/Ximimark-Compact-Bootable-Adapter-Converter/dp/B07LBLXDZM/&quot;&gt;a CF to IDE adapter&lt;/a&gt;. CF is a subset of IDE, so you want the adapter to be completely passive. This worked, but as CF cards are getting harder (and more expensive) to find, I wanted to use this as a last resort.&lt;/p&gt;
&lt;div class=&quot;clearfix&quot;&gt;&lt;/div&gt;

&lt;h2 id=&quot;25-ide&quot;&gt;2.5” IDE&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2021/derust-m2-2.5.jpg&quot; alt=&quot;M.2 to 2.5 inch IDE adapter with M.2 drive installed&quot; class=&quot;img-responsive img-rounded img-md pull-right&quot; /&gt;This one’s a bit weird. For older laptops and machines like the G4 Mac Mini, it needs to be the same form factor as a 2.5” IDE drive, so we’ll want to get… M.2 drives. Okay, SATA, not NVMe, but it still feels odd buying a brand new gumstick-sized card for a 15+ year old device. The secret is &lt;a href=&quot;https://www.amazon.com/gp/product/B07Z67GX6W/&quot;&gt;this M.2 SATA to 2.5” IDE converter&lt;/a&gt; (again, sold by multiple sellers); beyond that, you can pick any cheap M.2 SATA 2242 drive, though I recommend 120GB drives as the target device is probably old enough to be affected by the 48-bit LBA limit of 137GB.&lt;/p&gt;

&lt;h2 id=&quot;scsi&quot;&gt;SCSI&lt;/h2&gt;

&lt;p&gt;I have several machines which have both SCSI and IDE interfaces, and for those I just used the methods above for IDE. But I do have one SCSI-only machine: the SGI Challenge S, a server variant of the SGI Indy. For this, there is the &lt;a href=&quot;http://www.codesrc.com/mediawiki/index.php/SCSI2SD&quot;&gt;SCSI2SD v6&lt;/a&gt;. Until now, the converters/adapters have all been in the $10 to $20 range, but the SCSI2SD is significantly more expensive at about $100. But it’s full-featured, and has options for just about any classic computer situation. It can divide up an SD card to emulate multiple SCSI devices, even CD/tape drives, and has software-configurable termination options. I love it, but at its price, I’m glad I only needed it once.&lt;/p&gt;

&lt;p&gt;Again, you’ll probably need a bracket to physically mount it within the computer, but I found &lt;a href=&quot;https://www.thingiverse.com/thing:3067337&quot;&gt;someone else’s SCSI2SD universal mount&lt;/a&gt; to work fine with the Challenge S without modification.&lt;/p&gt;
&lt;div class=&quot;clearfix&quot;&gt;&lt;/div&gt;

&lt;h2 id=&quot;related-floppy-drives&quot;&gt;Related: floppy drives&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2021/derust-gotek.jpg&quot; alt=&quot;Gotek floppy emulator with OLED mod and 3D printed case, mounted in a Packard Bell desktop&quot; class=&quot;img-responsive img-rounded img-md pull-right&quot; /&gt;Floppy drives and disks also fail, and for this I recommend the &lt;a href=&quot;https://www.amazon.com/gp/product/B0762NCHC6/&quot;&gt;Gotek SFR1M44-U100&lt;/a&gt;, which lets you use a USB thumb drive to emulate floppy disks. There are many mods you can do to this drive, but at the very least, I recommend replacing the stock firmware, which requires a proprietary utility to write images to the USB drive. Replace the firmware with &lt;a href=&quot;https://github.com/keirf/FlashFloppy&quot;&gt;FlashFloppy&lt;/a&gt;, which has many more emulation options than the stock firmware, and also lets you directly drop image files onto the USB drive.&lt;/p&gt;

&lt;h2 id=&quot;whats-left&quot;&gt;What’s left?&lt;/h2&gt;

&lt;p&gt;Surely I didn’t completely eliminate spinning hard drives from my home, did I? Something I found was the closer I got to zero, the more the remaining ones stood out, and the stronger the desire to address them.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.finnie.org/2015/02/05/review-some-cheap-laptop-from-best-buy/&quot;&gt;Some Cheap Laptop from Best Buy&lt;/a&gt; was a laptop I used for a few months back in 2015. I consulted the repair manual for the laptop, and getting to the 500GB HDD would have been a monumental task. The laptop itself was not useful to me, nor did I have any sentimental attachment (beyond the joke review I made 6 years ago), so I solved the problem by reinstalling Windows 8.1 on it and giving it away.&lt;/li&gt;
  &lt;li&gt;My primary home server has 28TB of raw HDDs, in the form of 7x 4TB drives. It would be prohibitively expensive to replace it all with SSDs. One drive is external backup and one is a Purple drive for security camera recordings, and the other 5 are in a RAID 6 setup, backed by a 500GB SSD bcache, so I’m not worried about reliability or performance.&lt;/li&gt;
  &lt;li&gt;My Windows gaming machine has 3 tiers of storage: 1TB of boot NVMe, 2TB of SATA SSD, and a 6TB HDD. Again, I’m not concerned.&lt;/li&gt;
  &lt;li&gt;My remote colocation server has two primary boot 250GB SATA SSDs, and two WD RE4 2 TB “enterprise” HDDs, each in RAID 1. I’ll likely do pure solid-state with my next colocation server, but considering the current server is only 3 years old and the one before that lasted nearly a decade, it’s probably not going to be for quite awhile.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 03 Mar 2021 17:52:55 -0800</pubDate>
        <link>https://www.finnie.org/2021/03/03/spinning-rust-begone/</link>
        <guid isPermaLink="true">https://www.finnie.org/2021/03/03/spinning-rust-begone/</guid>
        
        
      </item>
    
      <item>
        <title>The &quot;perfect&quot; IP hashing algorithm</title>
        <description>&lt;p&gt;If you want to anonymize IP addresses, for example in HTTP logs, here’s the method I recommend:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/env python3
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashlib&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ipaddress&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;


&lt;span class=&quot;n&quot;&gt;KEY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hash_ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;last_len&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;base_bytes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;packed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;last_bytes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;packed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;base_bhash&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;shake_256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base_bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base_bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;last_bhash&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;shake_256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;packed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ipaddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ip_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base_bhash&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;last_bhash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;line_ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line_rest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ip_hashed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hash_ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ipaddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ip_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line_ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{} {}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ip_hashed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line_rest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;IPv4 and IPv6 addresses are both supported.&lt;/li&gt;
  &lt;li&gt;Hashed IPv4 addresses return IPv4, and hashed IPv6 addresses return IPv6.  However, that doesn’t mean the hashed addresses are globally valid (they could be within multicast, for example).&lt;/li&gt;
  &lt;li&gt;The last IPv4 /24 or IPv6 /64 can still be correlated, allowing you to pick out relatively similar hashed addresses.  For example, if you see both 230.10.134.107 and 230.10.134.226 hashed addresses, you know they’re part of the same /24, even though you know nothing else about the addresses.&lt;/li&gt;
  &lt;li&gt;However, the last IPv4 /24 or IPv6 /64 is not globally hashed and does not leak information about itself.  If you happen to know hashed 230.10.134.107 corresponds to real 10.9.8.4, hashed 178.59.91.107 does not mean the real IP ends in .4.&lt;/li&gt;
  &lt;li&gt;A hash key is optional but recommended.  Reuse the key if you want to be able to correlated hashed IPs over time, or be able to hash a known real IP to search for it in the past.  Use a random key and throw it away afterwards if you want it to be truly anonymous.&lt;/li&gt;
  &lt;li&gt;SHAKE-256 is used because it’s modern (SHA-3) and support arbitrary-length output. If you can’t use SHA-3, use something like SHA-512, HMAC it with a key, and use a portion of the output. (SHA-3 digests can securely use key prepending; other digests require HMAC.)&lt;/li&gt;
  &lt;li&gt;Hashed IPv6 addresses get quite unwieldy. Get used to addresses like 20f1:b413:7f5d:7720:1fe2:1bf3:38fb:620. (Exactly one hex byte out of 32 was compressible in that random example I used. Most examples have none.)&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 29 Sep 2020 14:03:43 -0700</pubDate>
        <link>https://www.finnie.org/2020/09/29/the-perfect-ip-hashing-algorithm/</link>
        <guid isPermaLink="true">https://www.finnie.org/2020/09/29/the-perfect-ip-hashing-algorithm/</guid>
        
        
      </item>
    
      <item>
        <title>That Time I Bought Classified Military Intelligence Off eBay (NOT CLICKBAIT)</title>
        <description>&lt;p&gt;Yesterday, “Alex” posted a &lt;a href=&quot;https://mango.pdf.zone/finding-former-australian-prime-minister-tony-abbotts-passport-number-on-instagram&quot;&gt;hilarious account&lt;/a&gt; of getting former Australian prime minister Tony Abbott’s passport information. The lesson of the story boils down to: how do you report a vulnerability at a national scale? Do you just call 1-800-NATIONAL-SECURITY and let them know what happened? (Actually, Australia does have a literal hotline like that, but it turned out to be useless. Seriously, before you read further here, go read “Alex”’s story.)&lt;/p&gt;

&lt;p&gt;This story reminds me of a similar problem I had 13 years ago. But be warned, this story is completely from memory and some things may be wrong. It will also not be nearly as entertaining as “Alex”’s story.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://www.finnie.org/blog-media/2020/cobalt-raq2-classified.jpg&quot; alt=&quot;Cobalt RaQ 2, redacted&quot; class=&quot;img-responsive img-rounded img-lg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In 2007, I bought a &lt;a href=&quot;https://en.wikipedia.org/wiki/Cobalt_RaQ&quot;&gt;Cobalt RaQ 2&lt;/a&gt; off eBay. Introduced in 1999, the RaQ 2 was the rackmount server variant of the more well known &lt;a href=&quot;https://en.wikipedia.org/wiki/Cobalt_Qube&quot;&gt;Cobalt Qube 2&lt;/a&gt;. It featured a MIPS R5000 CPU and was technically a general purpose server platform, but was most often used as a file server or proxy server. I bought it used (and long after its useful life) because I like to collect non-x86 hardware which can run Linux.&lt;/p&gt;

&lt;p&gt;When it arrived, I powered it on, intending to look around Cobalt’s bespoke Linux distribution before wiping the drive and installing Debian. I was expecting a server restored to factory settings, but was secretly hoping for some random company’s fileserver data. Likely an out-of-business company since these sorts of sales usually come from liquidators.&lt;/p&gt;

&lt;p&gt;Through the front panel, I was able to reset the administrator password and retrieve the configured IP information, which was… a public IP address. Huh, weird.  Was it being used as a web server?  Doing a whois on it revealed it was in a block owned by the &lt;strong&gt;DoD Network Information Center&lt;/strong&gt;. Yes, that DoD.&lt;/p&gt;

&lt;p&gt;By attaching a crossover Ethernet cable, I was able to log into the RaQ’s administrative web interface.  I could see it was configured as a caching LAN web proxy, and its hostname ended in &lt;strong&gt;smil.mil&lt;/strong&gt;. Yes, that military.&lt;/p&gt;

&lt;p&gt;Without looking at the contents, I could tell that the drive was full of cached content, with the usual extensions (.html, .jpg, etc), though if I remember correctly, the base filenames were randomly assigned so there was no indication of the subject of each file’s content. The file dates were mostly through 2003.&lt;/p&gt;

&lt;p&gt;That’s as far as I dug.  I turned off the RaQ and pulled the hard drive.  As I would come to learn, smil.mil is a domain on &lt;a href=&quot;https://en.wikipedia.org/wiki/SIPRNet&quot;&gt;SIPRNet&lt;/a&gt;, the US government’s classified equivalent to the Internet. SIPRNet is supposed to be air-gapped; there should be no physical way to reach a machine on SIPRNet from the Internet or vice versa, and it was definitely not accessible to civilians.  The fact that I had a caching proxy server used on SIPRNet was inconceivable.&lt;/p&gt;

&lt;p&gt;(This also explains why it had a “public” IP address. If you look at &lt;a href=&quot;https://vad.solutions/ipmap/&quot;&gt;visual representations of IPv4 space&lt;/a&gt; (buy this poster, hint hint), you’ll see DoD spaces but they don’t appear to be in use. They &lt;em&gt;are&lt;/em&gt; in use, just not on the Internet. RFC 1918? Never heard of it. Similarly, smil.mil and sgov.gov look like Internet domains, but only resolve on SIPRnet. I’ll give the government a pass here; the IANA has no organization-specific private namespace, like .internal or .lan, and it annoys me that domains like that are not explicitly reserved. What were we talking about? Oh right, national security.)&lt;/p&gt;

&lt;p&gt;So what should I do with this information?  I could go to the press, and it would be quite a embarrassment to the administration, but I didn’t want to draw political attention to myself.  Ideally I just wanted to go to someone official and say “someone messed up, here’s the server’s hard drive and who I bought it from”. But again, there isn’t really a 1-800-CLASSIFIED-LEAK.&lt;/p&gt;

&lt;p&gt;A co-worker suggested I contact my congressman’s office to see if they could help me navigate to someone proper in the federal government. I called, they took down my information, and never called me back.  I sent an email, no response. I’m sure I sounded like a crackpot, but it’s not my fault it was all true.&lt;/p&gt;

&lt;p&gt;In the end, I gave up. The hard drive went into my document safe, just in case I got a call later, perhaps the FBI got a list of buyers from the eBay seller.  But that never happened, and about 5 years later I destroyed the hard drive without loading any of its cached contents.  Somehow I lost my email archives from earlier than 2010, and eBay sale records only go back about 2 years, so I can’t even find out who sold me the server back in 2007.&lt;/p&gt;

&lt;p&gt;I still have the Cobalt RaQ 2 hardware today (minus the hard drive), and it currently runs Debian off an SD to IDE adapter. There was nothing otherwise special about the hardware, nor any asset stickers or similar, so now it’s just a weird server from an old generation.&lt;/p&gt;
</description>
        <pubDate>Wed, 16 Sep 2020 10:45:57 -0700</pubDate>
        <link>https://www.finnie.org/2020/09/16/that-time-i-bought-classified-military-intelligence-off-ebay-not-clickbait/</link>
        <guid isPermaLink="true">https://www.finnie.org/2020/09/16/that-time-i-bought-classified-military-intelligence-off-ebay-not-clickbait/</guid>
        
        
      </item>
    
  </channel>
</rss>
