Author: Derrick

  • Wisps, a WordPress Plugin

    Wisps, a WordPress Plugin

    Last year I had a need for an editable JSON file that was retrievable via HTTP. Of course there’s a million ways that I could do this, but the easiest I thought of would be to have it inside of WordPress, since all of the people that needed access to edit the file already had edit access to a specific site. So I built a plugin.

    Doing this inside WordPress already brings a lot of benefits with little to no effort:

    1. User Management
    2. Revision History
    3. oEmbed Support
    4. Permalinks
    5. Syntax Highlighting Code Editor
    6. Self-Hosted Data

    Possibly more benefits as well, depending on the setup, such as caching.

    I’ve tweaked the plugin some, and I’m almost ready to submit it to the WordPress.org Plugin Repository. I just need to do the hard part of figuring out artwork. Ugh.

    Introducing Wisps:

    Wisps are embeddable and sharable code snippets for WordPress.

    With Wisps, you can have code snippets similar to Gist, Pastebin, or similar code sharing sites. Using the built-in WordPress code editor, you can write snippets to post and share. This has the benefit of WordPress revisions, auto-drafts, etc to keep a record of how code changes.

    Wisps can be downloaded by appending /download/ to the permalink, or viewed raw by adding /view/ or /raw/. There is full oEmbed support so you can just paste in a link to a wisp in the editor and it will be fully embedded.

    PrismJS is used for syntax highlighting for oEmbeds.

    You can add Wisp support to your theme either by modifying the custom post type page-wisp.php template, which will continue to display Wisps in the loop securely, or you can use add_theme_support( 'wisps' ) to tell the plugin to not automatically escape the output. You can then do what you like, such as potentially adding frontend support for syntax highlighting.

    Here’s what the oEmbed data looks like:

    (Yeah, I totally stole the design from Gists, because I’m not talented 😬)

    View the example Wisp

    View it raw

    Download it

    Currently available on GitHub

    Hopefully one day available on the WordPress.org Plugin Repository 🙂

    If you give it a try and have any suggestions, or issues drop me a line here or on GitHub!

  • Open source ngrok alternative

    Open source ngrok alternative

    During a client onsite last year, I was first introduced to ngrok. Ngrok provides “secure introspectable tunnels to localhost.” The free tier of ngrok provides temporary, random subdomains to use. This is fine most of the time, but kind of causes problems for things like Jetpack that require persistent domain names for connecting.

    While I could shell out the $5/month for the lowest paid tier of ngrok, I would still be limited to a certain number of domains and connections.

    While looking for an alternative to ngrok, I came across sish. Sish is “an open source serveo/ngrok alternative. HTTP(S)/WS(S)/TCP Tunnels to localhost using only SSH.” To be honest, I don’t understand most of those words, but that won’t stop me!

    I of course needed a server somewhere to run this on, so I ran over to DigitalOcean and decided to spend my $5/month on a general purpose VPS instead (Here’s my DigitalOcean referral link if you’re so inclined).

    What follows is my notes I took to install sish and get it up and running. I can’t guarantee they’re perfect, and I don’t feel like deleting my VPS and starting over again just to make sure 🙂 If you see something wrong, feel free to comment me a correction or question.

    Step 1: Make a wildcard subdomain (ex *.sish.example.com)

    Step 2: Set up a DigitalOcean droplet.

    Step 3. Log in and run:

    # Make a house a home
    echo "export PS1=\"\\[\\033[38;5;33m\\]\\u\\[\$(tput sgr0)\\]\\[\\033[38;5;11m\\]@\\[\$(tput sgr0)\\]\\[\\033[38;5;33m\\]\\H\\[\$(tput sgr0)\\]\\[\\033[38;5;15m\\]:\\[\$(tput sgr0)\\]\\[\\033[38;5;11m\\]\\w\\[\$(tput sgr0)\\]\\[\\033[38;5;15m\\]\\\\$ \\[\$(tput sgr0)\\]\"" >> ~/.bashrc
    source ~/.bashrc
    apt update
    apt upgrade
    apt install ack-grep mc byobu git curl locate
    updatedb
    
    # Security
    ufw allow http
    ufw allow https
    ufw allow ssh
    ufw allow 2222
    ufw --force enable
    apt install fail2ban
    
    # Create swapfile
    fallocate -l 1G /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile
    echo '/swapfile none swap sw 0 0' | tee -a /etc/fstab
    
    # Install Docker
    apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu/ $(lsb_release -cs) stable"
    apt update
    apt install docker-ce docker-ce-cli containerd.io
    
    # Install Certbot
    certbot-auto certonly --manual -d *.sish.example.com --agree-tos --no-bootstrap --preferred-challenges dns-01 --server https://acme-v02.api.letsencrypt.org/directory
    certbot certonly –manual -d *.sish.example.com –agree-tos –no-bootstrap –manual-public-ip-logging-ok –preferred-challenges dns-01 –server https://acme-v02.api.letsencrypt.org/directory
    
    # Install Keys
    curl https://github.com/my_github_username.keys > ~/sish/pubkeys/my_github_username
    cp -f /etc/letsencrypt/live/sish.example.com-0001/* ~/sish/ssl/
    ssh-keygen
    ln -s ~/.ssh/id_rsa ~/sish/ssh_key
    
    # Run Sish
    cat << "EOF" > /root/sish/docker-start.sh
    /usr/bin/docker run --name sish \
      -v ~/sish/ssl:/ssl \
      -v ~/sish/keys:/keys \
      -v ~/sish/pubkeys:/pubkeys \
      --restart unless-stopped \
      --net=host antoniomika/sish:latest \
      -sish.addr=sish.example.com:2222 \
      -sish.adminenabled=true \
      -sish.auth=false \
      -sish.bindrandom=false \
      -sish.domain=sish.example.com \
      -sish.forcerandomsubdomain=false \
      -sish.http=:80 \
      -sish.https=:443 \
      -sish.httpsenabled=true \
      -sish.httpspems=/ssl \
      -sish.keysdir=/pubkeys \
      -sish.pkloc=/keys/ssh_key \
      -sish.redirectrootlocation=https://example.com/ \
      -sish.serviceconsoleenabled=true
    EOF
    
    cat << EOF > /etc/systemd/system/docker-sish.service
    # Thanks to https://blog.container-solutions.com/running-docker-containers-with-systemd
    
    [Unit]
    Description=Sish container
    Requires=docker.service
    After=docker.service
    
    [Service]
    TimeoutStartSec=0
    Restart=always
    ExecStartPre=-/usr/bin/docker stop sish
    ExecStartPre=-/usr/bin/docker rm sish
    ExecStartPre=/usr/bin/docker pull antoniomika/sish:latest
    ExecStart=/bin/bash /root/sish/docker-start.sh
    ExecStop=/usr/bin/docker stop sish
    RemainAfterExit=true
    
    [Install]
    WantedBy=default.target
    EOF
    
    systemctl enable docker-sish
    systemctl start docker-sishCode language: PHP (php)

    From here, I can now set up a shortcut program to run locally to start a tunnel:

    cat << "EOF" > /usr/local/bin/sish
    #/bin/bash
    ssh -p 2222 -R $1:80:localhost:80 root@sish.example.com
    EOF
    chmod +x /usr/local/bin/sishCode language: PHP (php)

  • Debugging WordPress Hooks: Speed

    Debugging WordPress Hooks: Speed

    If you google debugging WordPress hooks you’ll find a lot of information.

    About 1,180,000 results

    Let’s add another one.

    WordPress hooks are powerful, but also complex under the hood. There’s plenty of topics I could talk about here, but right now I’m only going to talk about speed. How long does it take for a hook to fire and return?

    Some cool work in this area has already been done thanks to Debug Bar Slow Actions and Query Monitor, but outside of something like Xdebug or New Relic, you’ll have a hard time figuring out how long each individual hook callback takes without modifying either WordPress core or each hook call.

    … or maybe not …

    While doing some client debugging for my job at WordPress VIP (did I mention we’re hiring?) I came across the need to do this exact thing. I’ve just finished the code that will make this happen, and I’m releasing it into the wild for everyone to benefit.

    What’s the problem we’re trying to solve? Well, this client has a sporadic issue where posts saving in the admin time out and sometimes fail. We’ve ruled out some potential issues, and are looking at a save_post hook going haywire.

    Now I can capture every single save_post action, what the callback was, and how long it took. Here’s an example for this exact post:

    START: 10:save_post, (Callback: `delete_get_calendar_cache`)
    STOP: 10:save_post, Taken 132.084μs (Callback: `delete_get_calendar_cache`)
    START: 10:save_post, (Callback: `sharing_meta_box_save`)
    STOP: 10:save_post, Taken 15.974μs (Callback: `sharing_meta_box_save`)
    START: 10:save_post, (Callback: `Jetpack_Likes_Settings::meta_box_save`)
    STOP: 10:save_post, Taken 19.073μs (Callback: `Jetpack_Likes_Settings::meta_box_save`)
    START: 10:save_post, (Callback: `SyntaxHighlighter::mark_as_encoded`)
    STOP: 10:save_post, Taken 19.073μs (Callback: `SyntaxHighlighter::mark_as_encoded`)
    START: 10:save_post, (Callback: `AMP_Post_Meta_Box::save_amp_status`)
    STOP: 10:save_post, Taken 16.928μs (Callback: `AMP_Post_Meta_Box::save_amp_status`)
    START: 20:save_post, (Callback: `Publicize::save_meta`)
    STOP: 20:save_post, Taken 428.915μs (Callback: `Publicize::save_meta`)
    START: 9000:save_post, (Callback: `Debug_Bar_Slow_Actions::time_stop`)
    STOP: 9000:save_post, Taken 11.921μs (Callback: `Debug_Bar_Slow_Actions::time_stop`)
    START: 1:save_post, (Callback: `The_SEO_Framework\Load::_update_post_meta`)
    STOP: 1:save_post, Taken 6.016ms (Callback: `The_SEO_Framework\Load::_update_post_meta`)
    START: 1:save_post, (Callback: `The_SEO_Framework\Load::_save_inpost_primary_term`)
    STOP: 1:save_post, Taken 1.535ms (Callback: `The_SEO_Framework\Load::_save_inpost_primary_term`)
    START: 10:save_post, (Callback: `delete_get_calendar_cache`)
    STOP: 10:save_post, Taken 179.052μs (Callback: `delete_get_calendar_cache`)
    START: 10:save_post, (Callback: `sharing_meta_box_save`)
    STOP: 10:save_post, Taken 247.002μs (Callback: `sharing_meta_box_save`)
    START: 10:save_post, (Callback: `Jetpack_Likes_Settings::meta_box_save`)
    STOP: 10:save_post, Taken 25.988μs (Callback: `Jetpack_Likes_Settings::meta_box_save`)
    START: 10:save_post, (Callback: `The_SEO_Framework\Load::delete_excluded_ids_cache`)
    STOP: 10:save_post, Taken 185.966μs (Callback: `The_SEO_Framework\Load::delete_excluded_ids_cache`)
    START: 10:save_post, (Callback: `SyntaxHighlighter::mark_as_encoded`)
    STOP: 10:save_post, Taken 15.020μs (Callback: `SyntaxHighlighter::mark_as_encoded`)
    START: 10:save_post, (Callback: `AMP_Post_Meta_Box::save_amp_status`)
    STOP: 10:save_post, Taken 12.875μs (Callback: `AMP_Post_Meta_Box::save_amp_status`)
    START: 20:save_post, (Callback: `Publicize::save_meta`)
    STOP: 20:save_post, Taken 377.893μs (Callback: `Publicize::save_meta`)
    START: 9000:save_post, (Callback: `Debug_Bar_Slow_Actions::time_stop`)
    STOP: 9000:save_post, Taken 14.067μs (Callback: `Debug_Bar_Slow_Actions::time_stop`)Code language: CSS (css)

    So how does it work?

    1. We add an all action that will fire for every other action.
    2. In the all callback, we make sure we’re looking for the correct hook.
    3. We then build an array to store some data, use the $wp_filter global to fill out information such as the priority and the callback, and store the start time.
    4. Next we have to add a new action to run for our hook right before the callback we want to time. We use the fact that, even though add_action() is supposed to use an int for the priority, it will also accept a string. We add new hooks, and re-prioritze all of the existing hooks with floats that are stringified.
    5. This allows us to capture the start time and end time of each individual callback, instead of the priority group as a whole.

    Of course, this does add a tiny bit of overhead, and could cause some problems if any other plugins use stringified hook priorities, or other odd issues–so be careful 🙂

    Finally, here’s the code:

    class VIP_Hook_Timeline {
    	public $hook;
    	public $callbacks     = [];
    	public $callback_mod  = 0.0001;
    	public $callback_mods = [];
    
    	public function __construct( $hook ) {
    		$this->hook = $hook;
    		add_action( 'all', array( $this, 'start' ) );
    	}
    
    	public function start() {
    		// We only want to get a timeline for one hook.
    		if ( $this->hook !== current_filter() ) {
    			return;
    		}
    
    		global $wp_filter;
    
    		// Iterate over each priority level and set up array.
    		foreach( $wp_filter[ $this->hook ] as $priority => $callback ) {
    			// Make the mod counter if not exists.
    			if ( ! isset( $this->callback_mods[ $priority ] ) ) {
    				$this->callback_mods[ $priority ] = $priority - $this->callback_mod;
    			}
    
    			// Make the array if not exists.
    			if ( ! is_array( $this->callbacks[ $priority ] ) ) {
    				$this->callbacks[ $priority ] = [];
    			}
    
    			// Iterate over each callback and set up array.
    			foreach( array_keys( $callback ) as $callback_func ) {
    				if ( ! is_array( $this->callbacks[ $priority ][ $callback_func ] ) ) {
    					$this->callbacks[ $priority ][ $callback_func ] = [ 'start' => 0, 'stop' => 0 ];
    				}
    			}
    		}
    
    		foreach( $this->callbacks as $priority => $callback ) {
    			foreach ( array_keys( $callback ) as $callback_func ) {
    
    				// Get data befmore we move things around.
    				$human_callback = $this->get_human_callback( $wp_filter[ $this->hook ][ $priority ][$callback_func] );
    
    				// Modify the priorities.
    				$pre_callback_priority = $this->callback_mods[ $priority ];
    				$this->callback_mods[ $priority ] = $this->callback_mods[ $priority ] + $this->callback_mod;
    
    				$new_callback_priority = $this->callback_mods[ $priority ];
    				$this->callback_mods[ $priority ] = $this->callback_mods[ $priority ] + $this->callback_mod;
    
    				$post_callback_priority = $this->callback_mods[ $priority ];
    				$this->callback_mods[ $priority ] = $this->callback_mods[ $priority ] + $this->callback_mod;
    
    				// Move the callback to our "new" priority.
    				if ( $new_callback_priority != $priority ) {
    					$wp_filter[ $this->hook ]->callbacks[ strval( $new_callback_priority ) ][ $callback_func ] = $wp_filter[ $this->hook ]->callbacks[ $priority ][ $callback_func ];
    					unset( $wp_filter[ $this->hook ]->callbacks[ $priority ][ $callback_func ] );
    					if ( empty( $wp_filter[ $this->hook ]->callbacks[ $priority ] ) ) {
    						unset( $wp_filter[ $this->hook ]->callbacks[ $priority ] );
    					}
    				}
    
    				// Add a new action right before the one we want to debug to capture start time.
    				add_action( $this->hook, function( $value = null ) use ( $callback_func, $priority, $human_callback ) {
    					$this->callbacks[ $priority ][ $callback_func ]['start'] = microtime( true );
    
    					// Uncomment this if you just want to dump data to the PHP error log, otherwise add your own logic.
    					//$message = 'START: %d:%s, (Callback: `%s`)';
    					// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
    					//error_log( sprintf( $message,
    					//	$priority,
    					//	$this->hook,
    					//	$human_callback,
    					//) );
    
    					// Just in case it's a filter, return.
    					return $value;
    				}, strval( $pre_callback_priority ) );
    
    				// Add a new action right after the one we want to debug to capture end time.
    				add_action( $this->hook, function( $value = null ) use ( $callback_func, $priority, $human_callback ) {
    					$this->callbacks[ $priority ][ $callback_func ]['stop'] = microtime( true );
    
    					// Uncomment this if you just want to dump data to the PHP error log, otherwise add your own logic.
    					//$message = 'STOP: %d:%s, Taken %s (Callback: `%s`)';
    					// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
    					//error_log( sprintf( $message,
    					//	$priority,
    					//	$this->hook,
    					//	$this->get_human_diff( $priority, $callback_func ),
    					//	$human_callback,
    					//) );
    
    					// Just in case it's a filter, return.
    					return $value;
    				}, strval( $post_callback_priority ) );
    				
    			}
    		}
    	}
    
    	public function get_human_callback( $callback ) {
    		$human_callback = '[UNKNOWN HOOK]';
    		if ( is_array( $callback['function'] ) && count( $callback['function'] ) == 2 ) {
    			list( $object_or_class, $method ) = $callback['function'];
    			if ( is_object( $object_or_class ) ) {
    				$object_or_class = get_class( $object_or_class );
    			}
    			$human_callback =  sprintf( '%s::%s', $object_or_class, $method );
    		} elseif ( is_object( $callback['function'] ) ) {
    			// Probably an anonymous function.
    			$human_callback =  get_class( $callback['function'] );
    		} else {
    			$human_callback =  $callback['function'];
    		}
    		return $human_callback;
    	}
    
    	public function get_start( $priority, $callback_func ) {
    		return (float) $this->callbacks[ $priority ][ $callback_func ]['start'];
    	}
    
    	public function get_stop( $priority, $callback_func ) {
    		return (float) $this->callbacks[ $priority ][ $callback_func ]['stop'];
    	}
    
    	public function get_diff( $priority, $callback_func ) {
    		return (float) ( $this->get_stop( $priority, $callback_func ) - $this->get_start( $priority, $callback_func ) );
    	}
    
    	public function get_human_diff( $priority, $callback_func ) {
    		$seconds = $this->get_diff( $priority, $callback_func );
    
    		// Seconds.
    		if ( $seconds >= 1 || $seconds == 0 ) {
    			return number_format( $seconds, 3 ) . 's';
    		}
    
    		// Milliseconds.
    		if ( $seconds >= .001 ) {
    			return number_format( $seconds * 1000, 3 ) . 'ms';
    		}
    
    		// Microseconds.
    		if ( $seconds >= .000001 ) {
    			return number_format( $seconds * 1000000, 3 ) . 'μs';
    		}
    
    		// Nanoseconds.
    		if ( $seconds >= .000000001 ) {
    			// WOW THAT'S FAST!
    			return number_format( $seconds * 1000000000, 3 ) . 'ns';
    		}
    
    		return $seconds . 's?';
    	}
    }
    new VIP_Hook_Timeline( 'save_post' );Code language: PHP (php)

  • Disabling Laptop Screen with Ubuntu Server

    Disabling Laptop Screen with Ubuntu Server

    As a part of my home network setup, I use an old Dell Studio XPS 15″ laptop as my “server.” It’s currently running Ubuntu 19.10. One issue I had during the last update was that whatever I set up a long time ago to manage the lid switch policy was lost, so I had to re-google it and re-do it, because I don’t want the screen to be on 24/7.

    Some Googling led me to a post that got me part of the way there:

    https://mensfeld.pl/2018/08/ubuntu-18-04-disable-screen-on-lid-close/

    I had to adjust a few things for my setup, and for posterity I’m going to log them here for the future.

    # As root, or sudo'd
    apt install vbetool -y
    echo 'event=button/lid.*' | tee --append /etc/acpi/events/lm_lid
    echo 'action=/etc/acpi/lid.sh' | tee --append /etc/acpi/events/lm_lid
    touch /etc/acpi/lid.sh
    chmod +x /etc/acpi/lid.shCode language: PHP (php)

    This will set everything up for the lid.sh script to run:

    #!/bin/bash
    # Close
    grep -q close /proc/acpi/button/lid/*/state
    if [ $? = 0 ]; then
    	sleep 1 &amp;&amp; vbetool dpms off
    fi
    # Open
    grep -q open /proc/acpi/button/lid/*/state
    if [ $? = 0 ]; then
    	sleep 1 &amp;&amp; vbetool dpms on
    fiCode language: HTML, XML (xml)

    Finally, reboot and enjoy not having to worry about the screen dying.

  • Dynamic Tea House

    Dynamic Tea House

    Not too long ago, a coworker shared that they had created a WordPress Dynamic Wallpaper for macOS. I thought this was just cool, so I eventually made one of my own with my favorite dynamic set of images: the Tea House theme from Gmail.

    Now, these classic Gmail themes are all but lost to history, and that’s a huge shame.

    With the help of some code I found on GitHub, I was able to download and reconstruct the theme as a desktop background. Unfortunately, the image sizes are rather small for modern desktops, so I also used bigjpg.com and the waifu2x image scaling algorithm to upcale these to over 5K in size:

    You can download these at Dynamic Wallpaper Club, or from the links below:

    tea-house.heic.zip

    tea-house-9×16.heic.zip

    I’ve got to thank the amazing folks at Meomi for their artwork I’ve stolen without asking. I hope they can forgive me!

  • Better Caching in WordPress

    Better Caching in WordPress

    Caching data in WordPress is easy. Caching data in WordPress in a good and performant way takes a bit more work. For instance, many developers commonly use the Transients API to cache data. As the lowest common denominator in caching, this is okay. It’ll get the job done, even on a $10/year shared hosting plan. But what we should do instead is leverage the WP_Object_Cache functions to provide more functionality and better features.

    For instance, let’s say we want to cache the result of an external API request. One way we could do this would be this way:

    Note: These examples are terrible, but I hope they get the point across!

    function get_api( $value) {
    	$value = absint( $value );
    	$api_data = get_transient( 'example-api-data-' . $value );
    
    	if ( false === $api_data ) {
    		$api_data = file_get_contents( 'https://example.com/api/' . $value );
    		set_transient( 'example-api-data-' . $value, $api_data, HOUR_IN_SECONDS * 6 );
    	}
    
    	return json_decode( $api_data );
    }Code language: PHP (php)

    What’s one way we could make this better by using the WP_Object_Cache functions? Well, what happens if the API data structure changes, and you need to invalidate every cache value? It would be pretty hard to know the exact transient keys that you’d need to clear, and clearing the entire cache is a bit too nuclear for this (but it would work). Instead, wp_cache_*() could be used, which includes the ability to use a cache group that can be changed:

    function get_api( $value) {
    	$value = absint( $value );
    	$cache_group = 'example-api-data';
    
    	$api_data = wp_cache_get( $value, $cache_group );
    
    	if ( false === $api_data ) {
    		$api_data = file_get_contents( 'https://example.com/api/' . $value );
    		wp_cache_set( $value, $api_data, $cache_group, HOUR_IN_SECONDS * 6 );
    	}
    
    	return json_decode( $api_data );
    }Code language: PHP (php)

    With this, if we ever need to invalidate the cache for this API, we just need to change the $cache_group value, and all cache requests will be new.


    Another common theme I see is caching too much data. Let’s say you’re going to do a slow WP_Query, and want to cache the results for better performance:

    function get_new_posts() {
    	$posts = wp_cache_get( 'new-posts' );
    
    	if ( false === $posts ) {
    		$posts = new WP_Query( 'posts_per_page=5000' );
    		wp_cache_set( 'new-posts', $posts );
    	}
    
    	return $posts;
    }Code language: PHP (php)

    Sure, that’s fine and it’ll work but… the WP_Query object is huge!

    echo strlen( serialize( new WP_Query( 'posts_per_page=500' ) ) ); … 2,430,748

    That’s 2.5 megs of data needing to be transferred out of cache on every pageload. If your cache is accessed across the network on another server, this introduces more delay as it has to transfer. Also, some caching solutions might put a limit on the size of an individual cache object–which means that an object like this might never be cached!

    Instead, we can just grab the IDs of the posts, and do a second, much faster query:

    function get_new_posts() {
    	$post_ids = wp_cache_get( 'new-posts' );
    
    	if ( false === $posts ) {
    		$post_ids = new WP_Query( 'posts_per_page=5000&fields=ids' );
    		wp_cache_set( 'new-posts', $posts->posts );
    	}
    
    	$posts = new WP_Query( [ 'post__in' => $post_ids ] );
    
    	return $posts;
    }Code language: PHP (php)

    echo strlen( serialize( $posts->posts ) ); … only 88,838 bytes, that’s like a 96%-something difference!

    I had a few more ideas for this post, but it’s been sitting as a draft forever and I don’t remember. It’s possible this topic might be revisited some day 🙂

  • Bulk Registering Steam Keys

    Bulk Registering Steam Keys

    Due to the crazy good deals that Humble provides, I discovered that I had hundreds of Steam keys that I haven’t registered.

    After spending way too long exporting all of my keys from the Humble site, via some JS console trickery, I ended up with a spreadsheet of 1,285 unique Steam keys.

    Unfortunately, I’ve randomly activated a number of these over the years as I’ve wanted to… but I don’t know which ones, so I needed a way to bulk register. Luckily, other smarter people have already figured this out: SteamBulkActivator did the job well! The only downside is that after a number of failed activations (10 I believe) there was an hour cooldown.

    That means I’ve spent quite a while going through and testing all of my keys. Here’s the breakdown of what I ended up with:

    • Already Registered – Games I already own, or keys I’ve already registered. There’s a good chance that there are a lot of keys here that are still unregistered. The next thing I’ll need to do is try to figure out how to separate the unregistered keys from the already used ones
    • Success – I now have 383 new games!
    • Bad activation key – Humble sometimes provides keys to stores other than Steam. This is likely where these came from.
    • Duplicate activation code – The key was activated on someone else’s account. I probably gave this key away to someone else because I already had the game.

    I wish it were easier to map a key to a game so I could find out what these are, but this is still better than having unused keys laying around that I paid for.