<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Zack Hobson</title>
  <subtitle>Building software and writing about it.</subtitle>
  <link href="https://zackhobson.com/atom.xml" rel="self"/>
  <link href="https://zackhobson.com/"/>
  <updated>2023-09-05T00:00:00Z</updated>
  <id>https://zackhobson.com/</id>
  <author>
    <name>Zack Hobson</name>
    <email>zack@zackhobson.com</email>
  </author>
  
  <entry>
    <title>Node.js and HTML5 Web Sockets</title>
    <link href="https://zackhobson.com/2010/03/28/node-js-and-web-sockets/"/>
    <updated>2010-03-28T00:00:00Z</updated>
    <id>https://zackhobson.com/2010/03/28/node-js-and-web-sockets/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://dev.w3.org/html5/websockets/&quot;&gt;Web Sockets&lt;/a&gt; elevate socket networking in client-side web apps to a
first-class citizen, giving clients and servers a simple way to communicate
over a persistent stream without the need for third-party plugins or hacks.
Now that the &lt;a href=&quot;http://blog.chromium.org/2009/12/web-sockets-now-available-in-google.html&quot;&gt;developer release of Chrome supports Web Sockets&lt;/a&gt;, I decided to
give it a try using &lt;a href=&quot;http://nodejs.org/&quot;&gt;node&lt;/a&gt;. Having never touched this technology before, it took
me most of a day to get it all working. I am writing up some notes on the
process for others who might want to try out something similar.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;One major caveat:&lt;/em&gt; Node&#39;s networking code is in flux right now and which
version you use can matter a great deal. For this exercise I&#39;m using
&lt;a href=&quot;http://github.com/ry/node/zipball/v0.1.33&quot;&gt;0.1.33&lt;/a&gt;. It&#39;s worth noting that I had to downgrade from the edge version
of node to get this working.&lt;/p&gt;
&lt;h3&gt;The Server&lt;/h3&gt;
&lt;p&gt;Besides node itself, there is a module called &lt;a href=&quot;http://github.com/ncr/node.ws.js&quot;&gt;node.ws.js&lt;/a&gt; that wraps node&#39;s
&lt;a href=&quot;http://nodejs.org/api.html#_tcp&quot;&gt;tcp socket API&lt;/a&gt; with a nearly identical interface: createServer() accepts
a callback, and this callback receives an event listener for processing web
socket events. Once a connection is made you can write to it like any socket.
Messages from the client are processed asynchronously in the &amp;quot;data&amp;quot; listener.&lt;/p&gt;
&lt;pre class=&quot;prettyprint lang-js&quot;&gt;
var sys = require(&#39;sys&#39;),
     ws = require(&#39;ws&#39;)

var server = ws.createServer(function (socket) {

  socket.addListener(&quot;connect&quot;, function (resource) {
    sys.puts(&quot;client connected from &quot; + resource)
    socket.write(&quot;welcome&#92;r&#92;n&quot;)
  })

  socket.addListener(&quot;data&quot;, function (data) {
    socket.write(data)
  })

  socket.addListener(&quot;close&quot;, function () {
    sys.puts(&quot;client left&quot;)
  })
})

server.listen(8080)
&lt;/pre&gt;
&lt;p&gt;Communication is initiated from the web browser with a special handshake
sequence. This sequence looks like an HTTP header and is designed to allow
an existing HTTP connection to upgrade to a web socket.
We won&#39;t be taking advantage of this functionality, but there is a project
called &lt;a href=&quot;http://github.com/RosePad/Socket.IO-node&quot;&gt;Socket.IO&lt;/a&gt; that provides some compelling features in this regard
and may be worth a look.&lt;/p&gt;
&lt;h3&gt;The Client&lt;/h3&gt;
&lt;p&gt;Remember that in order for this to work your web browser &lt;em&gt;must&lt;/em&gt; support web
sockets, which is fairly rare at the moment. You will probably need to run a
&lt;a href=&quot;http://dev.chromium.org/getting-involved/dev-channel&quot;&gt;development version of Google Chrome&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Conceptually the client and server are nearly identical, as they&#39;re both
reading and writing asynchronously to a network socket. In the client
it&#39;s trivial to hook these callbacks up to the DOM using jQuery to
do stuff like display server messages on the page.&lt;/p&gt;
&lt;p&gt;I did quite a bit of digging before I stumbled on this great jQuery plugin
called &lt;a href=&quot;http://plugins.jquery.com/project/ws&quot;&gt;ws&lt;/a&gt;. The actual WebSocket API is quite easy to use, but I was going
to be using &lt;a href=&quot;http://jquery.com/&quot;&gt;jQuery&lt;/a&gt; in my client anyway, so why not. Certainly, the API
for this plugin is pleasantly concise:&lt;/p&gt;
&lt;pre class=&quot;prettyprint lang-js&quot;&gt;
$(document).ready(function () {
  var ws = $.ws.conn({
    url : &#39;ws://localhost:8080&#39;,

    onopen : function () {
      console.log(&#39;connected&#39;);
    },

    onmessage : function (data) {
      console.log(&quot;received: &quot; + data)
    },

    onclose : function (event) {
      console.log(&#39;disconnected&#39;);
    }
  });
});
&lt;/pre&gt;
&lt;p&gt;This is pretty much the simplest possible thing you can do, but I hope this
minimal example illustrates the potential of this technique. This works in
the latest &lt;a href=&quot;http://dev.chromium.org/getting-involved/dev-channel&quot;&gt;developer builds of Chromium or Google Chrome&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are already some great examples of this kind of stuff in action. I
recommend checking out &lt;a href=&quot;http://bloga.jp/ws/jq/&quot;&gt;this cool web socket demo&lt;/a&gt; from the developer
of the ws jQuery plugin mentioned above. And of course there&#39;s &lt;a href=&quot;http://jfd.github.com/wpilot/&quot;&gt;WPilot&lt;/a&gt;,
which ties web sockets together with the awesome HTML 5 canvas element.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>HCl 0.3.0 supports free Harvest accounts!</title>
    <link href="https://zackhobson.com/2010/04/02/hcl-supports-free-harvest-accounts/"/>
    <updated>2010-04-02T00:00:00Z</updated>
    <id>https://zackhobson.com/2010/04/02/hcl-supports-free-harvest-accounts/</id>
    <content type="html">&lt;p&gt;This morning I received two pull requests for the same issue in &lt;a href=&quot;http://zackhobson.com/hcl&quot;&gt;HCl&lt;/a&gt;, my
command-line client for the &lt;a href=&quot;http://www.getharvest.com/&quot;&gt;Harvest&lt;/a&gt; time-tracking service:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://zackhobson.com/images/hcl-pull-requests.jpg&quot; alt=&quot;Pull requests!&quot; /&gt;&lt;/p&gt;
&lt;p&gt;They were both fixes for a &lt;a href=&quot;http://github.com/zenhob/hcl/issues/issue/28&quot;&gt;long-running problem&lt;/a&gt; with free
Harvest accounts that I just never got around to addressing. It surprised me
to see this activity after so long, but when I checked my fork queue I found
a number of commits for which I&#39;d never received pull requests. What a nice
surprise!&lt;/p&gt;
&lt;p&gt;Anyway, thanks in part to the efforts of &lt;a href=&quot;http://github.com/mbleigh&quot;&gt;Michael Bleigh&lt;/a&gt; the latest
version of &lt;a href=&quot;http://zackhobson.com/hcl&quot;&gt;HCl&lt;/a&gt; supports non-SSL (read: free) Harvest accounts. Yay!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Easier Sessions with MacVim</title>
    <link href="https://zackhobson.com/2011/02/21/macvim-sessions/"/>
    <updated>2011-02-21T00:00:00Z</updated>
    <id>https://zackhobson.com/2011/02/21/macvim-sessions/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;&lt;strong&gt;Given:&lt;/strong&gt;&lt;/em&gt; You never want to close your Vim session because you lose your lovingly crafted
(haha!) buffer list, and in my case, open tabs. I am not here to convince you that you
want to use sessions with Vim, that&#39;s up to you. Suffice to say that if you
don&#39;t use editor sessions this post will not be of much use to you.&lt;/p&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;I was thinking it&#39;d be nice if it was a little easier to open Vim sessions on
my Mac.  Usually I use &lt;a href=&quot;http://www.alfredapp.com/&quot;&gt;Alfred&lt;/a&gt; to start
&lt;a href=&quot;http://code.google.com/p/macvim/&quot;&gt;MacVim&lt;/a&gt; then &amp;quot;ESC-:session
~/Projects/blablah/Session.vim&amp;quot;, tab-completing as appropriate. I can&#39;t just
bind a key to this command because I have to specify the session file. If I am
in a Terminal the process is pretty direct (I can use mvim -S filename) but
usually I need an editor session before I need a Terminal.&lt;/p&gt;
&lt;p&gt;Alternately, I can type &amp;quot;open session.vim&amp;quot; in Alfred to open the session file in a MacVim
buffer and then &amp;quot;ESC-:so %&amp;quot;, which maybe is technically easier, but I figured at that
point I&#39;m not far from being able to just open MacVim with that session automatically,
right?&lt;/p&gt;
&lt;p&gt;So I googled around a bit and found this &lt;a href=&quot;http://vim.wikia.com/wiki/Vim-sessions_under_Mac&quot;&gt;useful article on the subject&lt;/a&gt;, which was almost exactly what I was looking for, with some caveats:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It requires that you save your session files with an extension besides &amp;quot;.vim&amp;quot; (I used
the suggested &amp;quot;.vis&amp;quot;).&lt;/li&gt;
&lt;li&gt;The app kept popping up the &amp;quot;You haven&#39;t run this application before&amp;quot; dialog on every
launch.&lt;/li&gt;
&lt;li&gt;My bash environment is never loaded by the Automator app, so my path and such are not
set appropriately. This messes with my Vim plugins that use external programs.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Solution&lt;/h3&gt;
&lt;p&gt;Originally this post was about how to solve the problems listed above, which is possible
to do in all three cases. However, I began to realize that there was probably a simpler way
to accomplish this. This realization hinged on the fact that I didn&#39;t really ever want to
edit my Vim session files, that this was what differentiated them from other Vim scripts.&lt;/p&gt;
&lt;p&gt;So I simplified the process to the following steps (note that Automator is no longer
involved):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set up the Mac OS file associations such that .vis files are opened automatically with
MacVim.&lt;/li&gt;
&lt;li&gt;Add the following to your $HOME/.gvimrc:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;
&quot; save sessions with .vis extension
map &amp;lt;leader&amp;gt;s :mksession!  session.vis&amp;lt;CR&amp;gt;

&quot; automatically source vim sessions so I can open them with the finder
au BufRead *.vis so %
&lt;/pre&gt;
&lt;p&gt;I decided to put this in my gvimrc since I don&#39;t really use sessions in console Vim
(that&#39;s more for one-off stuff than project work), and it might be useful just in case I
do need to edit a session file for some reason. Also I elected to stick to explicit
session saving rather than autosave. Personally I don&#39;t mind saving sessions manually as I
don&#39;t want to do it every time and I&#39;m used to saving my files anyway.&lt;/p&gt;
&lt;p&gt;So now, Alfred&#39;s open file functionality lets me instantly dial up any of my
MacVim sessions:&lt;/p&gt;
&lt;img src=&quot;http://zackhobson.com/images/alfred-macvim-sessions.jpg&quot; alt=&quot;Illustration: Alfred search box&quot; /&gt;
&lt;p&gt;When I select one of these it opens a MacVim window with the session loaded. A
drastic reduction in keystrokes, and no need to involve Automator. Using a
different file extension means that I can more easily select from all of my
available sessions.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>You don&#39;t need a plug-in to use ack with Vim</title>
    <link href="https://zackhobson.com/2011/09/02/vim-ack-grep/"/>
    <updated>2011-09-02T00:00:00Z</updated>
    <id>https://zackhobson.com/2011/09/02/vim-ack-grep/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://betterthangrep.com/&quot;&gt;ack&lt;/a&gt; is well known as one of the best command-line search tools, and there are
plug-ins for many popular editors, including for some reason Vim. I&#39;ve not been
able to determine why these exist for Vim, as support is already built-in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:set grepprg=ack&#92; -aH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See, Vim already has built-in support for external grep, so by telling ack to
output the same format we can use it as a drop-in replacement:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:grep foo dir1 dir2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This populates the quickfix window, so you can use &lt;code&gt;copen&lt;/code&gt; or &lt;code&gt;cnext&lt;/code&gt; to
list them or skip to the first one, respectively. In fact all the
grep-related commands  will work as usual, except that ack is doing all the
work underneath.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Detached for Mac</title>
    <link href="https://zackhobson.com/2013/12/21/detached-for-mac/"/>
    <updated>2013-12-21T00:00:00Z</updated>
    <id>https://zackhobson.com/2013/12/21/detached-for-mac/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;A version of this post originally appeared on the &lt;a href=&quot;http://blog.cloudcitydevelopment.com/2013/12/19/detached-for-mac/&quot;&gt;Cloud City blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Many developers are accustomed to working in a terminal window.
Even on the Mac, with a high resolution display and accelerated graphics, much
of the tools we use every day are operated through a terminal emulator. I
personally worked this way before I used a Mac and I still do today. In fact,
every Mac comes with a great tool for managing multiple terminal sessions: &lt;a href=&quot;https://www.gnu.org/software/screen/&quot;&gt;GNU
Screen&lt;/a&gt;. Unfortunately, it can be hard to use, and it provides no visibility
of your running sessions from the GUI.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://zackhobson.com/detached/&quot;&gt;Detached&lt;/a&gt; is a tool for your Mac that sits in your menu bar and lets you create
terminal sessions that you can close and reopen later, without losing the associated
process or your place in it (until you reboot).
This is similar to how sessions work in most document-based applications:
If you close the application with work in progress, you expect it to restore that
state when you reopen it. Thanks to GNU Screen, this functionality is already
built into your Mac: Detached just exposes it by making your terminal sessions
accessible from your menu bar.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zackhobson.com/images/detached-screenshot-1.png&quot; alt=&quot;screenshot&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;http://github.com/zenhob/detached/&quot;&gt;first version of Detached&lt;/a&gt; was written about 3 years ago, using &lt;a href=&quot;http://macruby.org/&quot;&gt;MacRuby&lt;/a&gt;.
At the time, it was difficult to distribute MacRuby binaries, because they had to
include the entire MacRuby runtime in order to be portable. The first distributable
binary for the original version of Detached was over 70 megabytes!&lt;/p&gt;
&lt;p&gt;It only took a few days to create the initial version of Detached, and
once I&#39;d gotten it chugging along it didn&#39;t need much work. It had been a fun
experience, but I wasn&#39;t ready to build MacRuby apps for a living or anything.
However, I still had Detached, and I ended up using it nearly every day for the
next three years! By that time, I&#39;d gone independent, there had been umpteen
releases of Xcode, and the original Detached project was getting pretty hard to
wrangle. I decided to rewrite it in Objective-C.&lt;/p&gt;
&lt;p&gt;Rewriting was easy! Cocoa and Xcode have come a long way in the last three years
and I had no trouble getting up to speed on ARC memory management and the like.
The resulting binary was less than one megabyte, and two orders of magnitude
smaller than the original. That&#39;s much easier to distribute!&lt;/p&gt;
&lt;p&gt;The only thing I was missing was an icon for the application. I wasn&#39;t really
keen on trying to do this myself, so I brought it to &lt;a href=&quot;http://twitter.com/geerlinger/&quot;&gt;Stephanie Geerlings&lt;/a&gt;
and &lt;a href=&quot;http://digiguin.com/&quot;&gt;Brendan Miller&lt;/a&gt; of &lt;a href=&quot;http://cloudcity.io/&quot;&gt;Cloud City Development&lt;/a&gt;, who
worked with me through several drafts before we settled on this fellow:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://zackhobson.com/images/detached512.png&quot; alt=&quot;logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I love this logo because it&#39;s simple, memorable and charming.
Earlier revisions tried to maintain some metaphor or theme relating to the
software itself, but in the end it was more important to me that the image be
appealing and fun. I think Brendan did a fantastic job.&lt;/p&gt;
&lt;p&gt;My original plan was to distribute on the App Store,
but I&#39;d need to sandbox the app, which would mean not using AppleScript to power
Terminal and iTerm, which is the core functionality! Instead, I am
&lt;a href=&quot;http://zackhobson.com/detached/&quot;&gt;distributing Detached myself&lt;/a&gt;.
The &lt;a href=&quot;http://github.com/zenhob/DetachedObjc/&quot;&gt;source code for Detached&lt;/a&gt;
is also available under the terms of the &lt;a href=&quot;https://gnu.org/licenses/gpl.html&quot;&gt;GPL&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>New features and fixes in HCl 0.4.x</title>
    <link href="https://zackhobson.com/2013/12/23/hcl-harvest-command-line/"/>
    <updated>2013-12-23T00:00:00Z</updated>
    <id>https://zackhobson.com/2013/12/23/hcl-harvest-command-line/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;http://zenhob.github.io/hcl&quot;&gt;HCl&lt;/a&gt; has always just been about scratching an itch. I&#39;d built it years ago when
I needed to track my hours, and stopped maintaining it when I got a gig that
didn&#39;t. In the late summer I started using &lt;a href=&quot;http://www.getharvest.com/&quot;&gt;Harvest&lt;/a&gt; again on a
client project, and the experience made me realize that HCl still
needed some work. The result was several weeks of bug-fixing and enhancements.&lt;/p&gt;
&lt;h3&gt;Improvements to task aliases&lt;/h3&gt;
&lt;p&gt;When I went back to using HCl, the biggest pain point was specifying and using
task aliases. I had to look up the &lt;code&gt;set&lt;/code&gt; command every time, and starting a timer
felt cumbersome:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hcl set task.mytask 123 456
hcl start mytask some stuff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To address this, I made some changes to the start command, to make the command
name optional and allow the task name to be specified anywhere with &lt;code&gt;@&lt;/code&gt;. I also
added a simpler &lt;code&gt;alias&lt;/code&gt; command for aliasing tasks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hcl alias mytask 123 456
hcl @mytask some stuff
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Multiple Harvest accounts&lt;/h3&gt;
&lt;p&gt;You can use an alternate configuration by specifying &lt;code&gt;HCL_DIR&lt;/code&gt; in the environment.
This allows you to switch between multiple accounts quickly. For example, adding
something like this to your shell configuration will create a &lt;code&gt;myhcl&lt;/code&gt; command with
separate account credentials:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alias myhcl=&amp;quot;env HCL_DIR=~/.myhcl hcl&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then use &lt;code&gt;myhcl&lt;/code&gt; and &lt;code&gt;hcl&lt;/code&gt; with different account credentials to easily
switch accounts.&lt;/p&gt;
&lt;h3&gt;Cancel a running timer&lt;/h3&gt;
&lt;p&gt;You can cancel a running timer (or the most recently stopped timer) with
&lt;code&gt;hcl cancel&lt;/code&gt;. You can also use &lt;code&gt;hcl nvm&lt;/code&gt; and &lt;code&gt;hcl oops&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Auto-completion of task aliases&lt;/h3&gt;
&lt;p&gt;You can enable completion of task aliases in Bash by adding this to your bashrc:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eval `hcl completion`
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Refresh or review your Harvest credentials&lt;/h3&gt;
&lt;p&gt;You can now refresh your auth credentials with the &lt;code&gt;--reauth&lt;/code&gt; option, and
review them with &lt;code&gt;hcl config&lt;/code&gt;. HCl also refreshes your credentials automatically
on auth failure.&lt;/p&gt;
&lt;h3&gt;Log entries without a timer&lt;/h3&gt;
&lt;p&gt;At user request, there is a new command &lt;code&gt;hcl log&lt;/code&gt; that you can use to log
an entry without leaving a timer running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hcl log @mytask +1.25 Finally did that thing.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MacOS X only: Passwords are now stored in your keychain&lt;/h3&gt;
&lt;p&gt;On MacOS X your password is now stored in your keychain!
This behavior is automatic, you don&#39;t need to enable it.
Unfortunately it&#39;s not available on any other platform yet.&lt;/p&gt;
&lt;h3&gt;More improvements and fixes since 0.3.x&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Added support (and continuous integration) for several modern Ruby platforms.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;note&lt;/code&gt; command with no arguments displays all notes for a running timer.&lt;/li&gt;
&lt;li&gt;Allow filtering of &lt;code&gt;tasks&lt;/code&gt; by project code.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;stop&lt;/code&gt; command now checks for running timers from the previous day.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;resume&lt;/code&gt; command now accepts an optional task.&lt;/li&gt;
&lt;li&gt;Added support for auto-retry on API throttle.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  <entry>
    <title>SSL and Ruby</title>
    <link href="https://zackhobson.com/2014/01/23/ssl-and-ruby/"/>
    <updated>2014-01-23T00:00:00Z</updated>
    <id>https://zackhobson.com/2014/01/23/ssl-and-ruby/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;  Jeff Hodges &lt;a href=&quot;https://twitter.com/jmhodges/status/428620272091287552&quot;&gt;has corrected a statement&lt;/a&gt; in
this post about the most recent OpenSSL providing secure defaults in Ruby.
The error was my fault, I misread part of the email thread in question.
A follow-up post is in progress, but I&#39;ve corrected this post in the
meantime.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;en&quot;&gt;&lt;p&gt;The ruby-core position is that they do not want Ruby to be responsible for the security of users. I and others believe that Ruby already is.&lt;/p&gt;&amp;mdash; Jeff Hodges (@jmhodges) &lt;a href=&quot;https://twitter.com/jmhodges/statuses/424234624437325824&quot;&gt;January 17, 2014&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;Recently Jeff Hodges publicly disclosed the contents of a
&lt;a href=&quot;https://gist.github.com/jmhodges/d6480f5f81f25b0dfa15&quot;&gt;thread on the ruby-security mailing list&lt;/a&gt;.
In this thread, Mr. Hodges pointed
out that the lack of secure defaults means that users with older versions of
OpenSSL will be susceptible to certain kinds of attacks. This generated
&lt;a href=&quot;http://www.reddit.com/r/ruby/comments/1vh68r/members_of_ruby_core_are_pushing_back_against/&quot;&gt;some&lt;/a&gt; &lt;a href=&quot;https://news.ycombinator.com/item?id=7097645&quot;&gt;discussion&lt;/a&gt; in Ruby circles about the security
responsibilities of Ruby&#39;s volunteer maintainers.&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;en&quot;&gt;&lt;p&gt;Yes, intentionally weakened rand # generators, suspect curves, &amp;amp; fiber taps are bad, but we&amp;#39;re our own worse enemy: &lt;a href=&quot;https://t.co/4y0tqeGyHz&quot;&gt;https://t.co/4y0tqeGyHz&lt;/a&gt;&lt;/p&gt;&amp;mdash; Kenn White (@kennwhite) &lt;a href=&quot;https://twitter.com/kennwhite/statuses/425643427842514944&quot;&gt;January 21, 2014&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;At the same time, it&#39;s been pointed out by Kenn White that open-source Ruby
software already has a &lt;a href=&quot;https://github.com/search?l=ruby&amp;amp;q=OpenSSL%3A%3ASSL%3A%3AVERIFY_NONE&amp;amp;ref=cmdform&amp;amp;type=Code&quot;&gt;pretty wide-spread issue&lt;/a&gt; with
not validating the server SSL certificate against a root CA, allowing easy
man-in-the-middle attacks. In this context, better defaults are not much help,
as verification is explicitly by-passed.&lt;/p&gt;
&lt;p&gt;Full disclosure: &lt;a href=&quot;http://zenhob.github.io/hcl/&quot;&gt;My Harvest command line&lt;/a&gt; had this bug before I
switched to Faraday, which handles this behavior
&lt;a href=&quot;https://github.com/lostisland/faraday/search?q=VERIFY_PEER&amp;amp;type=Code&quot;&gt;correctly by default&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&#39;s probably important to you that your software uses SSL in the most
effective way possible, so let&#39;s see how to avoid the above issues in your
own Ruby programs and environment.&lt;/p&gt;
&lt;h3&gt;Using the latest OpenSSL version&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Unfortunately, at the time of this writing, updating the version of OpenSSL
used by Ruby will not necessarily provide the most secure defaults. Hopefully
this will be fixed in Ruby itself.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To make sure your Ruby installation is using the latest OpenSSL, you need to
know which version of
OpenSSL your Ruby is using, and how to update it.&lt;/p&gt;
&lt;p&gt;First, we&#39;ll invoke &lt;code&gt;ruby&lt;/code&gt; on the command line to see which version of OpenSSL
it was built with, and whether it matches the latest version listed on the
&lt;a href=&quot;http://www.openssl.org/&quot;&gt;OpenSSL web site&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ruby -ropenssl -e &#39;puts OpenSSL::OPENSSL_VERSION&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Helpfully, this command reports both the version and release date of the
OpenSSL library used by your Ruby installation. If it&#39;s out of date, you&#39;ll
need to update OpenSSL and then rebuild Ruby.&lt;/p&gt;
&lt;p&gt;The method for updating your OpenSSL installation and Ruby varies with your
system. It&#39;s pretty straightforward on a Mac using &lt;a href=&quot;http://brew.sh/&quot;&gt;Homebrew&lt;/a&gt; and &lt;a href=&quot;https://github.com/sstephenson/rbenv&quot;&gt;rbenv&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew update
brew upgrade openssl
rbenv install -f `rbenv version`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On my machine, this process ensured that my Ruby installation utilized
the latest OpenSSL, which is 1.0.1f at the time of this writing.&lt;/p&gt;
&lt;h3&gt;Verifying your SSL certificate&lt;/h3&gt;
&lt;p&gt;In SSL, trust is every bit as important as secrecy. With no way to verify
identity, you have no way to verify that someone else isn&#39;t just reading your
data and passing it on. This is the reason we have root certs to begin with:
They&#39;re the root of the trust network that verifies the owner of the domain is
who they say they are.&lt;/p&gt;
&lt;p&gt;In order to verify the SSL peer with Ruby&#39;s built-in OpenSSL and Net::HTTP,
you need to specify the correct mode and cert storage location. Here is a
simple example of a wrapper method that creates a Net::HTTP connection
using peer verification:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require &#39;net/http&#39;
require &#39;openssl&#39;
def verified_peer_connect hostname
  Net::HTTP.new(hostname, 443).tap do |conn|
    conn.use_ssl = true
    conn.verify_mode = OpenSSL::SSL::VERIFY_PEER
    conn.cert_store = OpenSSL::X509::Store.new
    conn.cert_store.set_default_paths
  end
end

# Example: fetch the Google home page
conn = verified_peer_connect(&#39;www.google.com&#39;)
conn.get(&#39;/&#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course, SSL will work without these settings, as those scores of open
source examples can attest. The point is to more fully utilize the
capabilities of SSL, including peer verification.&lt;/p&gt;
&lt;p&gt;Ruby and Rails have been in the spotlight for &lt;a href=&quot;https://www.ruby-lang.org/en/security/&quot;&gt;security&lt;/a&gt;
&lt;a href=&quot;http://weblog.rubyonrails.org/2013/1/8/Rails-3-2-11-3-1-10-3-0-19-and-2-3-15-have-been-released/&quot;&gt;issues&lt;/a&gt; in the recent
past, so it&#39;s a good idea these days to be especially vigilant about security updates
and best practices. Regardless of any disagreement surrounding disclosure or
default ciphers, these issues can be avoided entirely with just a little bit of
up-front effort.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>SSL and Ruby, part 2</title>
    <link href="https://zackhobson.com/2014/02/10/ssl_and_ruby_part_2/"/>
    <updated>2014-02-10T00:00:00Z</updated>
    <id>https://zackhobson.com/2014/02/10/ssl_and_ruby_part_2/</id>
    <content type="html">&lt;p&gt;Recently I wrote about &lt;a href=&quot;https://zackhobson.com/2014/01/23/ssl-and-ruby&quot;&gt;an issue with Ruby and SSL&lt;/a&gt; that was
publicized by &lt;a href=&quot;http://twitter.com/jmhodges&quot;&gt;Jeff Hodges&lt;/a&gt;. Mr. Hodges has uncovered what
seems like a fairly serious issue: Known insecure cipher suites and other
options are being used by the OpenSSL bindings that ship with Ruby.
In my original article I asserted that updating Ruby to the most recent version
of OpenSSL will fix this issue, but &lt;em&gt;this is not actually the case!&lt;/em&gt;
There is still no officially published fix at the time of this writing, but
there are ways you can fix this in your own Ruby installation, if you&#39;re so
inclined.&lt;/p&gt;
&lt;p&gt;Here is &lt;a href=&quot;https://gist.github.com/jmhodges/d6480f5f81f25b0dfa15&quot;&gt;the leaked ruby-security thread&lt;/a&gt; in question, I still recommend
reading it in its entirety. I&#39;ll attempt to summarize below. First a little
background:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-4&quot;&gt;Cipher suites&lt;/a&gt; are named sets of security algorithms used to
negotiate a secure connection. The SSL client maintains a list of these
suites, to be attempted (in order) until the client and server can agree on a
set of ciphers and establish a connection. Of course, weaknesses and
vulnerabilities are found in encryption algorithms all the time.
Better techniques are discovered. Best practices change.
When this happens, it can render some cipher suites less than optimal for security.
Ideally, the cipher suites using these algorithms would be deprecated, but servers
are under pressure to support as many encryption techniques as they can, for the
widest compatibility possible. This means that if the client prefers a known-insecure
suite, the server is unlikely to refuse it.&lt;/p&gt;
&lt;p&gt;Now then, the problem: Ruby specifies its own set of default cipher suites for
OpenSSL, including some that have known problems. It also allows some other
behaviors (like TLS compression) that are known to be insecure. In addition to this,
the OpenSSL defaults themselves are not always secure. It follows from this
that Ruby itself is the last line of defense for users of Ruby&#39;s OpenSSL bindings.&lt;/p&gt;
&lt;p&gt;Some of Ruby&#39;s maintainers pushed back on this, arguing that Ruby is not a security
project, and that bypassing OpenSSL defaults carries further risks. But as far
as I can tell, this argument doesn&#39;t hold water, as Ruby already specifies its own
default options and ciphers for OpenSSL. Someone proposed a
&lt;a href=&quot;https://gist.github.com/jmhodges/d6480f5f81f25b0dfa15#file-gistfile1-txt-L853&quot;&gt;patch that demonstrated which options and ciphers would be preferable&lt;/a&gt;,
which gives us some insight into how we might solve this problem on our own.&lt;/p&gt;
&lt;p&gt;If you&#39;d like to see this problem for yourself, you can demonstrate the weakness on
any Ruby installation using a &lt;a href=&quot;https://gist.github.com/cscotta/8302049&quot;&gt;simple Ruby program&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require &amp;quot;net/https&amp;quot;
require &amp;quot;uri&amp;quot;
require &amp;quot;json&amp;quot;

uri = URI.parse(&amp;quot;https://www.howsmyssl.com/a/check&amp;quot;)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
resp = JSON.parse(http.request(Net::HTTP::Get.new(uri.request_uri)).body)
puts JSON.pretty_generate(resp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I run the above code locally, I get a &lt;a href=&quot;https://gist.github.com/8811864&quot;&gt;JSON-formatted report&lt;/a&gt;
explaining the numerous problems with my SSL client, including a list of
known-insecure cipher suites. As you can see, it&#39;s not looking so good (I&#39;ve
elided the complete list of cipher suites for brevity):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;given_cipher_suites&amp;quot;: [
    &amp;quot;TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384&amp;quot;,
    &amp;lt;...&amp;gt;
    &amp;quot;TLS_EMPTY_RENEGOTIATION_INFO_SCSV&amp;quot;
  ],
  &amp;quot;ephemeral_keys_supported&amp;quot;: true,
  &amp;quot;session_ticket_supported&amp;quot;: true,
  &amp;quot;tls_compression_supported&amp;quot;: true,
  &amp;quot;unknown_cipher_suite_supported&amp;quot;: false,
  &amp;quot;beast_vuln&amp;quot;: false,
  &amp;quot;able_to_detect_n_minus_one_splitting&amp;quot;: false,
  &amp;quot;insecure_cipher_suites&amp;quot;: {
    &amp;quot;TLS_DHE_DSS_WITH_DES_CBC_SHA&amp;quot;: [
      &amp;quot;uses keys smaller than 128 bits in its encryption&amp;quot;
    ],
    &amp;quot;TLS_DHE_RSA_WITH_DES_CBC_SHA&amp;quot;: [
      &amp;quot;uses keys smaller than 128 bits in its encryption&amp;quot;
    ],
    &amp;quot;TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA&amp;quot;: [
      &amp;quot;is open to man-in-the-middle attacks because it does not authenticate the server&amp;quot;
    ],
    &amp;quot;TLS_ECDH_anon_WITH_AES_128_CBC_SHA&amp;quot;: [
      &amp;quot;is open to man-in-the-middle attacks because it does not authenticate the server&amp;quot;
    ],
    &amp;quot;TLS_ECDH_anon_WITH_AES_256_CBC_SHA&amp;quot;: [
      &amp;quot;is open to man-in-the-middle attacks because it does not authenticate the server&amp;quot;
    ],
    &amp;quot;TLS_ECDH_anon_WITH_RC4_128_SHA&amp;quot;: [
      &amp;quot;is open to man-in-the-middle attacks because it does not authenticate the server&amp;quot;
    ],
    &amp;quot;TLS_RSA_WITH_DES_CBC_SHA&amp;quot;: [
      &amp;quot;uses keys smaller than 128 bits in its encryption&amp;quot;
    ],
    &amp;quot;TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA&amp;quot;: [
      &amp;quot;is open to man-in-the-middle attacks because it does not authenticate the server&amp;quot;
    ],
    &amp;quot;TLS_SRP_SHA_WITH_AES_128_CBC_SHA&amp;quot;: [
      &amp;quot;is open to man-in-the-middle attacks because it does not authenticate the server&amp;quot;
    ],
    &amp;quot;TLS_SRP_SHA_WITH_AES_256_CBC_SHA&amp;quot;: [
      &amp;quot;is open to man-in-the-middle attacks because it does not authenticate the server&amp;quot;
    ]
  },
  &amp;quot;tls_version&amp;quot;: &amp;quot;TLS 1.2&amp;quot;,
  &amp;quot;rating&amp;quot;: &amp;quot;Bad&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look at that, ten known insecure cipher suites! TLS compression! Rating &amp;quot;Bad&amp;quot;!
There is good news, however: it&#39;s possible to provide your own list of
ciphers to Net::HTTP when creating a connection:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require &amp;quot;net/https&amp;quot;
require &amp;quot;uri&amp;quot;
require &amp;quot;json&amp;quot;

uri = URI.parse(&amp;quot;https://www.howsmyssl.com/a/check&amp;quot;)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
# corrected ciphers
http.ciphers = &amp;quot;DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2&amp;quot;
resp = JSON.parse(http.request(Net::HTTP::Get.new(uri.request_uri)).body)
puts JSON.pretty_generate(resp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I run this code, I get a
&lt;a href=&quot;https://gist.github.com/8811878&quot;&gt;different report with no insecure ciphers listed&lt;/a&gt;, but
unfortunately, the overall score for my TLS is still &amp;quot;Bad&amp;quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;given_cipher_suites&amp;quot;: [
    &amp;quot;TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384&amp;quot;,
    &amp;lt;...&amp;gt;
    &amp;quot;TLS_EMPTY_RENEGOTIATION_INFO_SCSV&amp;quot;
  ],
  &amp;quot;ephemeral_keys_supported&amp;quot;: true,
  &amp;quot;session_ticket_supported&amp;quot;: true,
  &amp;quot;tls_compression_supported&amp;quot;: true,
  &amp;quot;unknown_cipher_suite_supported&amp;quot;: false,
  &amp;quot;beast_vuln&amp;quot;: false,
  &amp;quot;able_to_detect_n_minus_one_splitting&amp;quot;: false,
  &amp;quot;insecure_cipher_suites&amp;quot;: {
  },
  &amp;quot;tls_version&amp;quot;: &amp;quot;TLS 1.2&amp;quot;,
  &amp;quot;rating&amp;quot;: &amp;quot;Bad&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is because there are other insecure options apart from the ciphers, and at
the moment Ruby does not have hooks to set OpenSSL options from within Ruby code.
There is a &lt;a href=&quot;https://bugs.ruby-lang.org/issues/9450&quot;&gt;patch to allow setting options on the SSL context&lt;/a&gt;,
applying it should allow us to provide our own, more secure options to SSL
using Ruby.
I&#39;ve stolen and re-purposed a
&lt;a href=&quot;https://gist.github.com/zenhob/8813977&quot;&gt;script that will install the patched Ruby&lt;/a&gt;
using rbenv. If you&#39;d like to try this out yourself, that script might be a
good starting point.&lt;/p&gt;
&lt;p&gt;Once you have a version of Ruby with the above patch applied, we can set SSL
options in our TLS testing code, using the
&lt;a href=&quot;https://gist.github.com/jmhodges/d6480f5f81f25b0dfa15#file-gistfile1-txt-L853&quot;&gt;patch proposed in the original thread&lt;/a&gt; as a guide:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require &amp;quot;net/https&amp;quot;
require &amp;quot;uri&amp;quot;
require &amp;quot;json&amp;quot;

uri = URI.parse(&amp;quot;https://www.howsmyssl.com/a/check&amp;quot;)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
# avoid unsafe cipher suites
http.ciphers = &amp;quot;DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2&amp;quot;
# provide options for the SSL context
http.ssl_options = OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_COMPRESSION &amp;amp; ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS
resp = JSON.parse(http.request(Net::HTTP::Get.new(uri.request_uri)).body)
puts JSON.pretty_generate(resp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I run the above test code with a patched Ruby, the report improves:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;given_cipher_suites&amp;quot;: [
    &amp;quot;TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384&amp;quot;,
    &amp;lt;...&amp;gt;
    &amp;quot;TLS_EMPTY_RENEGOTIATION_INFO_SCSV&amp;quot;
  ],
  &amp;quot;ephemeral_keys_supported&amp;quot;: true,
  &amp;quot;session_ticket_supported&amp;quot;: true,
  &amp;quot;tls_compression_supported&amp;quot;: false,
  &amp;quot;unknown_cipher_suite_supported&amp;quot;: false,
  &amp;quot;beast_vuln&amp;quot;: false,
  &amp;quot;able_to_detect_n_minus_one_splitting&amp;quot;: false,
  &amp;quot;insecure_cipher_suites&amp;quot;: {
  },
  &amp;quot;tls_version&amp;quot;: &amp;quot;TLS 1.2&amp;quot;,
  &amp;quot;rating&amp;quot;: &amp;quot;Probably Okay&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Probably okay! A positively glowing appraisal from our TLS testing service.
This demonstrates what is possible with some fairly small changes to Ruby&#39;s
OpenSSL bindings and the right SSL options.&lt;/p&gt;
&lt;p&gt;While we&#39;re able to get some positive results in the end, this is still mostly
bad news. Ruby&#39;s maintainers are pushing back on improving the known-insecure
defaults already provided by Ruby&#39;s OpenSSL bindings, and there is no official
release of Ruby that will even allow users to provide more secure options.
On the bright side, the &lt;a href=&quot;https://bugs.ruby-lang.org/issues/9450&quot;&gt;patch to add &lt;code&gt;ssl_options&lt;/code&gt; to Net::HTTP&lt;/a&gt;
is pretty non-controversial, and will probably make it into Ruby at some
point. This at least will allow HTTP clients to work around it in their own
Ruby code.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Become an internet DJ using pulseaudio</title>
    <link href="https://zackhobson.com/2020/05/26/internet-radio-pulseaudio-darkice-icecast/"/>
    <updated>2020-05-26T00:00:00Z</updated>
    <id>https://zackhobson.com/2020/05/26/internet-radio-pulseaudio-darkice-icecast/</id>
    <content type="html">&lt;p&gt;This is a tutorial on how to use pulseaudio to stream the audio output of any
program as an internet radio station.&lt;/p&gt;
&lt;p&gt;I arrived at this solution because I have kind of a weird music set up: My
collection of mostly Ogg Vorbis files are on a network-attached storage device
in my apartment, exposed to the local network as a DAAP service. I can use
rhythmbox to browse and play my music via this service.&lt;/p&gt;
&lt;p&gt;When I became interested in internet radio, I learned that my setup wasn&#39;t
conducive to streaming. Most tools that make broadcasting easy are designed to
work with local files, but I specifically wanted to keep using rhythmbox and
DAAP to play my music, even when broadcasting. It turns out that if you&#39;re
already using pulseaudio, this is not that difficult to do! In fact, it comes
with a significant upside: You can stream the sound from &lt;em&gt;any&lt;/em&gt; program you want,
and even include a microphone monitor!&lt;/p&gt;
&lt;p&gt;Obviously there are a lot of ways to do this, but if you want to do things
exactly as I did, this is what you&#39;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the radio station: an internet-accessible host for running &lt;code&gt;icecast2&lt;/code&gt;, I use a Linode VPS&lt;/li&gt;
&lt;li&gt;the recording studio: a computer that uses &lt;code&gt;pulseaudio&lt;/code&gt; for sound, I use Ubuntu on a Thinkpad X1 Carbon&lt;/li&gt;
&lt;li&gt;an audio player of your choice, I use &lt;code&gt;rhythmbox&lt;/code&gt; but anything will do&lt;/li&gt;
&lt;li&gt;the command-line tool &lt;code&gt;darkice&lt;/code&gt; for streaming pulseaudio to icecast&lt;/li&gt;
&lt;li&gt;the GUI tool &lt;code&gt;pavucontrol&lt;/code&gt; (Pulse Audio Volume Control) for managing the inputs and outputs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The radio station&lt;/h3&gt;
&lt;p&gt;The first thing you&#39;ll need is an internet-accessible host for running your
radio station. For this, I&#39;m using a cheap Linode VPS with the name
&lt;code&gt;radio.butts.team&lt;/code&gt; pointed at it, on which I install &lt;code&gt;icecast2&lt;/code&gt; as root:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# as root on the icecast host
apt install icecast2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, &lt;code&gt;icecast2&lt;/code&gt; will start automatically and only needs to have the config
updated to add the password we will use to authenticate when broadcasting:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# as root on the icecast host
vim /etc/icecast2/icecast.xml # edit authentication block to set passwords
service icecast2 restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it! Your internet radio station is now waiting for you to start
streaming. It&#39;s worth mentioning that at this point you can easily use a
conventional player like Internet DJ Console to broadcast, and skip the rest of
this tutorial.&lt;/p&gt;
&lt;p&gt;If you want the flexibilty of using pulseaudio instead, what follows is a
method for configuring your workstation to do just that.&lt;/p&gt;
&lt;h3&gt;The recording studio&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; The remainder of these examples are meant to be run on the machine you
use to play music.&lt;/p&gt;
&lt;h4&gt;Step 1: broadcasting music&lt;/h4&gt;
&lt;p&gt;The trick to streaming via &lt;code&gt;pulseaudio&lt;/code&gt; while still having normal use of your
computer audio is to create a new sink specifically for your radio stream:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pactl load-module module-null-sink sink_name=Radio sink_properties=&amp;quot;device.description=&#39;Radio&#39;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In pulse, sound comes from a &lt;em&gt;source&lt;/em&gt; and outputs to a &lt;em&gt;sink&lt;/em&gt;. This command
creates a &lt;em&gt;null sink&lt;/em&gt; named &amp;quot;Radio&amp;quot; that will be used to isolate the radio
stream from other audio on the computer.&lt;/p&gt;
&lt;p&gt;In order to stream that audio to your radio station for broadcasting, we&#39;ll use &lt;code&gt;darkice&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install darkice
cp /etc/share/darkice.cfg.gz .
gunzip darkice.cfg.gz
vim darkice.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;darkice.cfg&lt;/code&gt; update the input section to use pulse:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[input]
device = pulse
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, update the icecast2 config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[icecast2-0]
server = &amp;lt;radio station hostname&amp;gt;
password = &amp;lt;radio station password&amp;gt;
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The remaining config blocks in the file (for streaming to other types of
service) can be deleted.&lt;/p&gt;
&lt;p&gt;With this done, you can start &lt;code&gt;darkice&lt;/code&gt; to enable streaming to your radio station:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;darkice -c darkice.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have an audio sink and a streaming source we can use &lt;code&gt;pavucontrol&lt;/code&gt;
to configure them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install pavucontrol
pavucontrol
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &amp;quot;Recording&amp;quot; tab of &lt;code&gt;pavucontrol&lt;/code&gt; (make sure &amp;quot;All Streams&amp;quot; are showing)
you&#39;ll see a new device that streams all audio to your radio station. Set it to
monitor the &amp;quot;Radio&amp;quot; sink that you created earlier.&lt;/p&gt;
&lt;p&gt;Then, in the &amp;quot;Playback&amp;quot; tab of &lt;code&gt;pavucontrol&lt;/code&gt; find the app that you want to
stream from and set it to output to &amp;quot;Radio&amp;quot;.&lt;/p&gt;
&lt;p&gt;That&#39;s it! If everything worked, the audio from your seleted app is now
streaming over the internet!&lt;/p&gt;
&lt;h4&gt;Step 2: the microphone monitor&lt;/h4&gt;
&lt;p&gt;At this point, we can use any app as an audio source for our internet radio
station, but we don&#39;t yet have a way to use our microphone! To enable it, we
need a way to use the microphone as an audio source that can be pointed at the
Radio sink. This is accomplished by adding a &lt;em&gt;loopback&lt;/em&gt; interface:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pactl load-module module-loopback latency_msec=1 sink=Radio sink_dont_move=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now in the &amp;quot;Recording&amp;quot; tab of &lt;code&gt;pavucontrol&lt;/code&gt; there is another entry for the
loopback interface, which you can then point at your microphone. You&#39;ll want to
&lt;strong&gt;leave it muted except when you&#39;re broadcasting your mic&lt;/strong&gt;, or your microphone
audio will always be broadcast on the stream, probably not what you intended!&lt;/p&gt;
&lt;h4&gt;Step 3: make it permanent&lt;/h4&gt;
&lt;p&gt;Once all this is done, you might not want to re-do all of this the next time
you restart your computer, so be sure to add your &lt;code&gt;pulseaudio&lt;/code&gt; config to the end
of &lt;code&gt;/etc/pulse/default.pa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# at the end of /ect/pulse/default.pa
load-module module-null-sink sink_name=Radio sink_properties=&amp;quot;device.description=&#39;Radio&#39;&amp;quot;
load-module module-loopback latency_msec=1 sink=Radio sink_dont_move=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will ensure that pulseaudio is configured on system start, but you will
still need to start &lt;code&gt;darkice&lt;/code&gt; and use &lt;code&gt;pavucontrol&lt;/code&gt; to resume streaming.&lt;/p&gt;
&lt;h3&gt;Listening to the radio&lt;/h3&gt;
&lt;p&gt;Visiting your radio station host and port with a web browser will show you a
useful interface for accessing your stream. Here&#39;s the icecast UI for my own
radio station: &lt;a href=&quot;http://radio.butts.team/&quot;&gt;radio.butts.team&lt;/a&gt;. I recommend using VLC, but any media player
should be able to stream it.&lt;/p&gt;
&lt;h3&gt;Caveats&lt;/h3&gt;
&lt;p&gt;The default Ubuntu volume controls will mess up your stream if you touch them!
If you don&#39;t stick with &lt;code&gt;pavucontrol&lt;/code&gt; exclusively when you&#39;re streaming, a
volume change will likely reset your music program to output to your speakers.
This can probably be alleviated with the right pulseaudio settings but I
haven&#39;t figure this out yet.&lt;/p&gt;
&lt;p&gt;It&#39;s probably too easy to broadcast your mic accidentally, I might be able to
alleviate this as well by setting it mute by defaut or something similar. I
haven&#39;t figured this out yet either.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>The last three years</title>
    <link href="https://zackhobson.com/2023/06/21/workfrom/"/>
    <updated>2023-06-21T00:00:00Z</updated>
    <id>https://zackhobson.com/2023/06/21/workfrom/</id>
    <content type="html">&lt;p&gt;I was so excited to finally get to work at Workfrom! This was a company that
&lt;a href=&quot;https://www.linkedin.com/in/darrenbuckner/&quot;&gt;Darren Buckner&lt;/a&gt;, &lt;a href=&quot;http://jewel.mlnarik.com/&quot;&gt;Jewel Mlnarik&lt;/a&gt; and myself had co-founded a decade or so
ago, but we learned eventually that there wasn&#39;t much need for a full-time
technologist, so I went to work at New Relic.&lt;/p&gt;
&lt;p&gt;While I&#39;d spent a little time advising and consulting over those years, it
wasn&#39;t until about three years ago that Workfrom realized that it was time to
build a new product. This coincided with the end of my time at New Relic, so I
finally got my chance: I went to be the head of product engineering at
Workfrom.&lt;/p&gt;
&lt;p&gt;By the time I arrived, Jewel had moved on and my counterpart in product was
&lt;a href=&quot;http://brookehurford.com/&quot;&gt;Brooke Hurford&lt;/a&gt;. With Darren and &lt;a href=&quot;https://www.linkedin.com/in/jessicaporec/&quot;&gt;Jess Porec&lt;/a&gt; directing strategy and
talking to existing community members, we started looking for new ways to
enable collaboration for remote workers.&lt;/p&gt;
&lt;p&gt;At first, I spent most of my time catching up on our existing deployed
resources and reviewing code in the Places app that Darren and Jewel had built.
I also kept a technical blog that I published nearly every day. It mostly
contained research and info dumps about what I was working on, and why. I liked
to joke during this time that I was basically a full-time blogger for two
people.&lt;/p&gt;
&lt;p&gt;Eventually we started prototyping some of our new ideas for connecting our
community. We ran our own &lt;a href=&quot;https://meet.jit.si/&quot;&gt;Jitsi Meet&lt;/a&gt; instance and experimented with
embedding it in web pages. This was pretty fun, I specifically remember
publishing my notes one day with a meeting embedded in it, so when Darren and
Brooke went to read them I&#39;d be there, just hanging out, in my notes! While
this was a fun trick, it soon became clear that Jitsi Meet wasn&#39;t the best
platform to build on. It wasn&#39;t designed to be customized in the ways we
wanted, at least not without a deep knowledge of Jitsi&#39;s internals.&lt;/p&gt;
&lt;p&gt;We were not interested in cobbling together or maintaining a real-time video
and audio service by hand, so we looked around for other solutions. We soon
found &lt;a href=&quot;https://daily.co/&quot;&gt;Daily&lt;/a&gt;, which offered a nearly perfect solution: Real time comms,
embeddable in a web page, via a straightforward Javascript library dependency.&lt;/p&gt;
&lt;p&gt;With Daily it was relatively easy to get going, and we had a working prototype
pretty quickly. I cobbled together a stack based on Next.js, Blitz RPC, and
React/Typescript for the application. DigitalOcean had just launched an &amp;quot;app
platform&amp;quot; that we could hook up to GitLab for automatic deployment with Docker.&lt;/p&gt;
&lt;p&gt;With the bones of the product taking shape, we started building out our dream
hangout in earnest. Brooke and I cranked out one feature after another over two
years as it matured into an actual SaaS product that we could offer to
customers. During this time we had near-weekly production demos (we did our
best never to demo unreleased features) and also the time of our lives. I love
this work! Sometimes you just want clear requirements and a runway, and we had
it. Customers loved it too! One user described it as &amp;quot;Zoom meets MySpace,&amp;quot;
which is a soundbite that I still use to this day.&lt;/p&gt;
&lt;p&gt;This product has many happy customers now, you can get an idea of how it works
by visiting &lt;a href=&quot;https://cafeforteams.com/&quot;&gt;CafeForTeams.com&lt;/a&gt;. So that&#39;s the last three years, more or less.
What a great time it&#39;s been. I should republish some of those technical blog
posts on here, probably.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>A simple search component without external tooling</title>
    <link href="https://zackhobson.com/2023/08/31/web-component/"/>
    <updated>2023-08-31T00:00:00Z</updated>
    <id>https://zackhobson.com/2023/08/31/web-component/</id>
    <content type="html">&lt;p&gt;&lt;notes-search data-src=&quot;/posts/index.json&quot; data-placeholder=&quot;Search by title and press enter.&quot;&gt;&lt;/notes-search&gt;&lt;/p&gt;
&lt;script lang=&quot;javascript&quot; src=&quot;https://zackhobson.com/static/notes-search.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;I keep hearing that it&#39;s now possible to build &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Components&quot;&gt;self-contained web
components&lt;/a&gt; using only native web technologies. Until now, creating a
component that can be freely incorporated into another web page has required
layers of inscrutable translators and compilers and bundlers between the code
and the browser.&lt;/p&gt;
&lt;p&gt;Here&#39;s the code in this page that enables the search interface above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;notes-search
  data-src=&amp;quot;/posts/index.json&amp;quot;
  data-placeholder=&amp;quot;Search by title and press enter.&amp;quot;&amp;gt;
&amp;lt;/notes-search&amp;gt;

&amp;lt;script lang=&amp;quot;javascript&amp;quot; src=&amp;quot;/static/notes-search.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s just an element using a custom tag &lt;code&gt;notes-search&lt;/code&gt; and a &lt;code&gt;script&lt;/code&gt; tag to
load the &lt;a href=&quot;https://zackhobson.com/static/notes-search.js&quot;&gt;&lt;code&gt;notes-search.js&lt;/code&gt;&lt;/a&gt; script that defines the custom
element. Click the link above to take a look at the script. That&#39;s the entire
thing, a web component in a single file that can be loaded on any web page!&lt;/p&gt;
&lt;h2&gt;The component&lt;/h2&gt;
&lt;p&gt;To build this component I used the custom element API and shadow DOM to create
a little self-contained DOM that can be styled without affecting the rest of
the page. It starts with a class definition:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class NotesSearch extends HTMLElement {

  constructor() {
    super();

    const wrapper = document.createElement(&#39;div&#39;);
    const form = wrapper.appendChild(document.createElement(&#39;form&#39;));
    const input = form.appendChild(document.createElement(&#39;input&#39;));
    input.setAttribute(&#39;name&#39;, &#39;q&#39;);
    input.setAttribute(&#39;autocomplete&#39;, &#39;off&#39;);
    input.setAttribute(&#39;placeholder&#39;, this.getAttribute(&#39;data-placeholder&#39;));
    const button = form.appendChild(document.createElement(&#39;button&#39;));
    button.textContent = &#39;Search&#39;;
    const results = wrapper.appendChild(document.createElement(&#39;ul&#39;));

    // ...
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the element is constructed, we create a &lt;code&gt;wrapper&lt;/code&gt; div and populate it with
the basic controls. The input placeholder is fetched from an attribute
&lt;code&gt;data-placeholder&lt;/code&gt;. This is my least favorite part of the code since I am used
to using JSX or some other template language, but there isn&#39;t much of it.&lt;/p&gt;
&lt;p&gt;We also set up the form submission and fetch the index data needed for the
search:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    // Handle form submit.
    form.addEventListener(
      &#39;submit&#39;,
      this._searchSubmitFormHandler(input, results),
    );

    // Select text on focus.
    input.addEventListener(&#39;focus&#39;, e =&amp;gt; e.target.select());

    // We aren&#39;t using await because constructor isn&#39;t async.
    this._fetchNotesIndex()
      .then(index =&amp;gt; {
        this.index = index;
      })
      .catch(e =&amp;gt; {
        alert(&#39;Failed to fetch notes index.&#39;, e);
      });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not an advanced tool at all: For illustration purposes I generated a
simple JSON list of my posts and I&#39;m using that as the search index. A better
component might have a search backend to call, but this is just a little static
website.&lt;/p&gt;
&lt;h3&gt;Connecting it up&lt;/h3&gt;
&lt;p&gt;You may have noticed that the constructor didn&#39;t actually add anything to the
document yet. For that, we are going to implement &lt;code&gt;connectedCallback&lt;/code&gt; and
utilize the shadow DOM API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  connectedCallback() {
    this.attachShadow({ mode: &#39;open&#39; });
    const style = document.createElement(&#39;style&#39;);
    style.textContent = this.style;
    this.shadowRoot.append(style, this.wrapper);
    this.input.focus();
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we call &lt;code&gt;attachShadow&lt;/code&gt;, which defines &lt;code&gt;this.shadowRoot&lt;/code&gt;, to which we
append our style element and our wrapper element. Finally, we focus the search
input.&lt;/p&gt;
&lt;h3&gt;Styling it&lt;/h3&gt;
&lt;p&gt;This value of &lt;code&gt;this.style&lt;/code&gt; is a static string value on the class that contains
all of the CSS for the component. Because we are only styling the shadow DOM,
we don&#39;t have to worry about style conflicts or pollution, we can use
whatever elements and class names we want!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  style = `
    :host {
      --form-border: #ccc;
      --form-bg: #fefefe;
      --focus-border: blue;
      --input-text: #000;
    }
    form {
      display: inline-flex;
      outline: 1px solid var(--form-border);
      border-radius: 0.25rem;
      padding: 0.5rem;
      width: 100%;
      background: var(--form-bg);
    }
    /* ... */
  `;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are other ways we could load our styles. For instance, if you wanted your
CSS to be in a different file (better syntax highlighting!), you could add a
&lt;code&gt;&amp;lt;link rel=&amp;quot;stylesheet&amp;quot;&amp;gt;&lt;/code&gt; to your shadow DOM instead.&lt;/p&gt;
&lt;p&gt;Also: The &lt;code&gt;:host&lt;/code&gt; CSS selector is the shadow DOM equivalent of the &lt;code&gt;:root&lt;/code&gt;
selector, for the purpose that we are using it today. Here, I&#39;m using it to
define all the colors in one place.&lt;/p&gt;
&lt;h3&gt;Doing the thing&lt;/h3&gt;
&lt;p&gt;Our form submit handler is implemented in the method
&lt;code&gt;_searchSubmitFormHandler&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // return a submit callback for the search form
  _searchSubmitFormHandler(input, results) {
    return async e =&amp;gt; {
      e.preventDefault();
      if (!input.value) {
        results.replaceChildren();
        return;
      }

      const value = `${input.value}`.toLowerCase();
      const resultRows = this.index.notes
        .filter(note =&amp;gt; note.title.toLowerCase().indexOf(value) &amp;gt;= 0)
        .map(note =&amp;gt; {
          const row = document.createElement(&#39;li&#39;);
          const item = row.appendChild(document.createElement(&#39;a&#39;));
          item.textContent = note.title;
          item.href = note.url;
          return row;
        });
      results.replaceChildren(...resultRows);
      input.select();
    };
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Given an &lt;code&gt;input&lt;/code&gt; element and a &lt;code&gt;results&lt;/code&gt; element, this method returns a handler
that performs the search and displays the results. Again, the search itself is
&lt;em&gt;very&lt;/em&gt; primitive since it&#39;s mostly for illustration purposes. We also select
the contents of the input element after the search, as a little treat.&lt;/p&gt;
&lt;h3&gt;Fetching the index&lt;/h3&gt;
&lt;p&gt;For completeness, here is &lt;code&gt;_fetchNotesIndex&lt;/code&gt;, which is called in the
constructor to load the index. It&#39;s just a wrapper around &lt;code&gt;fetch&lt;/code&gt; that pulls
the index URL from the &lt;code&gt;data-src&lt;/code&gt; attribute of the custom element:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // fetch the notes index
  async _fetchNotesIndex() {
    const notesUrl = this.getAttribute(&#39;data-src&#39;);
    try {
      const request = await fetch(notesUrl);
      return await request.json();
    } catch (e) {
      console.error(&#39;Failed to fetch notes index.&#39;, e);
      return {};
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Upsides&lt;/h2&gt;
&lt;p&gt;A component like this can be dropped into a legacy web app pretty easily,
regardless of how it was made. I am going to try building more little
components to get the hang of it. I didn&#39;t even get to incorporate &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot&quot;&gt;&lt;code&gt;&amp;lt;slot&amp;gt;&lt;/code&gt;&lt;/a&gt; yet.&lt;/p&gt;
&lt;h2&gt;The Downsides&lt;/h2&gt;
&lt;p&gt;I am already missing a couple of niceties from my usual React-based stack. The
big one is that I can&#39;t use TypeScript, but I can see how it could be possible
to incorporate with a relatively small increase in complexity (as compared to
say, a full React app). I probably won&#39;t bother for small components like this,
but I think it would make a difference if you wanted to use this technique in a
large codebase.&lt;/p&gt;
&lt;p&gt;I&#39;m unsure how this tech could be used to construct an entire application, but
it seems like it&#39;d be possible to do. I like the idea of making an actual web
app without all those translation layers.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Embedding Mastodon posts with a web native component</title>
    <link href="https://zackhobson.com/2023/09/03/more-web-component/"/>
    <updated>2023-09-03T00:00:00Z</updated>
    <id>https://zackhobson.com/2023/09/03/more-web-component/</id>
    <content type="html">&lt;p&gt;&lt;embed-post data-src=&quot;https://butts.team/@zenhob/110987984593500666&quot; data-maxheight=&quot;325&quot; data-maxwidth=&quot;800&quot;&gt;&lt;/embed-post&gt;&lt;/p&gt;
&lt;script async=&quot;&quot; src=&quot;https://zackhobson.com/static/js/embed-post.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;This is the &lt;a href=&quot;https://zackhobson.com/static/js/embed-post.js&quot;&gt;source code for the embed widget&lt;/a&gt; that (hopefully) appears
at the top of this post. Here is how it&#39;s being used in this page:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;embed-post
  data-src=&amp;quot;https://butts.team/@zenhob/110987984593500666&amp;quot;
  data-maxheight=&amp;quot;325&amp;quot;
  data-maxwidth=&amp;quot;800&amp;quot;
&amp;gt;&amp;lt;/embed-post&amp;gt;
&amp;lt;script async=&amp;quot;async&amp;quot; src=&amp;quot;/static/js/embed-post.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, there isn&#39;t much to it. After my &lt;a href=&quot;https://zackhobson.com/2023/08/31/web-component/&quot;&gt;last post, where I learned
about native web components&lt;/a&gt;, I started to wonder how hard it would
be to use this technique to build a widget for embedding Mastodon posts. This
is something I&#39;ve never done before, but it wasn&#39;t that hard to figure out, so
I wrote about the process. There are helpful links throughout, mostly to MDN,
because that&#39;s my favorite reference. Since I already went into technical
detail about web components in the last post, I will skip over many of those
details here.&lt;/p&gt;
&lt;h2&gt;Figuring it out&lt;/h2&gt;
&lt;p&gt;First off, let&#39;s try &lt;a href=&quot;https://butts.team/@zenhob/110987984593500666.json&quot;&gt;adding &lt;code&gt;.json&lt;/code&gt; to the end of the post URL&lt;/a&gt; and
loading that in the browser. Sure enough, that displays a JSON representation
of the post! Could it be that simple? Sadly, attempting to fetch that URL from
within this page using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API&quot;&gt;&lt;code&gt;fetch()&lt;/code&gt;&lt;/a&gt; fails with a CORS error. That makes
sense: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS&quot;&gt;CORS&lt;/a&gt; is a safeguard to prevent web apps from making unauthorized
requests on behalf of the user, and this interface isn&#39;t meant to be available
to an outside web application. This seems like a dead end.&lt;/p&gt;
&lt;p&gt;After an unsuccessful web search, I ended up poking around in the Mastodon
code. Here we can find &lt;a href=&quot;https://github.com/mastodon/mastodon/blob/main/config/initializers/cors.rb#L26&quot;&gt;the code describing CORS behavior for
Mastodon&lt;/a&gt;, and noticed that the &lt;code&gt;/api&lt;/code&gt; path is excepted from
CORS, indicating that it is for external use! It turns out there is an endpoint
under this path that Mastodon provides explicitly for this purpose:
&lt;a href=&quot;https://docs.joinmastodon.org/methods/oembed/&quot;&gt;&lt;code&gt;/api/oembed&lt;/code&gt;&lt;/a&gt;. This is a pretty roundabout way to figure that out,
but it didn&#39;t take long.&lt;/p&gt;
&lt;p&gt;With this knowledge, we can fetch &lt;em&gt;something&lt;/em&gt; related to the post, given the
URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const hostname = new URL(postUrl).host
const resp = await fetch(`https://${hostname}/api/oembed?url=${postUrl}`)
const embed = await resp.json()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What we get is a &lt;a href=&quot;https://docs.joinmastodon.org/methods/oembed/#response&quot;&gt;response specifically for embedding&lt;/a&gt;, which
contains a snippet of HTML. This snippet declares an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; pointing at the
original post, and loads some JavaScript. It looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe
  src=&amp;quot;https://butts.team/@zenhob/110987984593500666/embed&amp;quot;
  class=&amp;quot;mastodon-embed&amp;quot;
  allowfullscreen=&amp;quot;allowfullscreen&amp;quot;
&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;script src=&amp;quot;https://butts.team/embed.js&amp;quot; async=&amp;quot;async&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s rarely a great idea to accept raw HTML and insert into your UI, there are
serious security implications! For a service under my control, with my own
post, this seems like something that can be done safely. Given that, we are
just going to shovel it into the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML&quot;&gt;&lt;code&gt;innerHTML&lt;/code&gt;&lt;/a&gt; of the component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.#content.innerHTML = embed.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works! At this point the post is visible through an awkwardly sized
viewport on the page. The &lt;a href=&quot;https://docs.joinmastodon.org/methods/oembed/&quot;&gt;docs for &lt;code&gt;/api/oembed&lt;/code&gt;&lt;/a&gt; explain how to
specify the size of the embedded frame, so as long as our styles match those
sizes, our embed should fit perfectly. The optional attributes &lt;code&gt;data-maxwidth&lt;/code&gt;
and &lt;code&gt;data-maxheight&lt;/code&gt; are used to configure the styles and the API call.&lt;/p&gt;
&lt;h2&gt;Wrapping it up&lt;/h2&gt;
&lt;p&gt;By this point the bulk of core functionality exists, but there is still some
polishing to do. User-provided &lt;code&gt;maxheight&lt;/code&gt; and &lt;code&gt;maxwidth&lt;/code&gt; values are applied
using &lt;a href=&quot;https://www.devdoc.net/web/developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables.html&quot;&gt;CSS variables&lt;/a&gt; in inline styles. I added a little &amp;quot;loading&amp;quot;
message that&#39;s visible until the fetch completes and the embed is visible.&lt;/p&gt;
&lt;p&gt;The code for this component makes use of some JavaScript features that the last
one did not, like static and private members. This is mostly because I haven&#39;t
really written much JavaScript lately and I am unused to our age of evergreen
browsers, where I can use a more modern JS dialect without issue.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Type checking JavaScript with TypeScript</title>
    <link href="https://zackhobson.com/2023/09/05/type-checking-js-with-typescript/"/>
    <updated>2023-09-05T00:00:00Z</updated>
    <id>https://zackhobson.com/2023/09/05/type-checking-js-with-typescript/</id>
    <content type="html">&lt;p&gt;Unlike the web component stuff, the thing I am curious about today is not
especially new. Since I am accustomed to using TypeScript, I felt myself
missing the type checker when working on straightforward browser-targeted
JavaScript.&lt;/p&gt;
&lt;h3&gt;TypeScript for JavaScript&lt;/h3&gt;
&lt;p&gt;Just using TypeScript would not be a big leap here, I am already using a basic
build system for the posting parts of this blog. However, I rather like that I
can code directly to the browser! Also there are still ways to statically
analyze JS code with the TypeScript compiler, so we&#39;re going to do that
instead, just to see how useful it is.&lt;/p&gt;
&lt;p&gt;Say you already have a web app with a couple JavaScript modules. The first step
is probably to install TypeScript if it isn&#39;t installed yet, and to create a
&lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ yarn add typescript
$ cat &amp;gt; tsconfig.json
{
  &amp;quot;compilerOptions&amp;quot;: {
    &amp;quot;target&amp;quot;: &amp;quot;ES6&amp;quot;,
    &amp;quot;allowJs&amp;quot;: true,
    &amp;quot;checkJs&amp;quot;: true,
    &amp;quot;noEmit&amp;quot;: true
  },
  &amp;quot;include&amp;quot;: [&amp;quot;static/js/*&amp;quot;, &amp;quot;static/*.js&amp;quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To get the &lt;code&gt;tsc&lt;/code&gt; compiler to behave the way we need, we specify a few things.
The &lt;code&gt;target&lt;/code&gt; is ES6, which is the dialect of JS that we&#39;re using. &lt;code&gt;allowJs&lt;/code&gt; and
&lt;code&gt;checkJs&lt;/code&gt; make sure that JS code is loaded and checked, otherwise it will only
analyze TypeScript code. Finally the &lt;code&gt;noEmit&lt;/code&gt; ensures that we aren&#39;t emitting
any code, since we&#39;re just using the type checker. I am also putting my own
paths in the &lt;code&gt;include&lt;/code&gt; list.&lt;/p&gt;
&lt;h3&gt;Type-checking my code&lt;/h3&gt;
&lt;p&gt;Once I have &lt;code&gt;tsc&lt;/code&gt; installed and configured I can run it with &lt;code&gt;npx&lt;/code&gt; or as a command in my &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ npx tsc
static/notes-search.js:79:16 - error TS2339: Property &#39;select&#39; does not exist
on type &#39;EventTarget&#39;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oh hey, I got a type mismatch in my plain old JavaScript! It happens in an
event listener, where I am selecting the search input text on focus:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const input = document.createElement(&#39;input&#39;)
// ...
input.addEventListener(&#39;focus&#39;, e =&amp;gt; e.target.select())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The call to &lt;code&gt;select()&lt;/code&gt; happens to be safe, because the event listener is being
applied to an input element, which has that method. However, the type checker
isn&#39;t buying it. There are a couple of ways to fix this!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We can cast to a known type using supported JSDoc annotations&lt;/li&gt;
&lt;li&gt;We can ignore the callback args and capture &lt;code&gt;input&lt;/code&gt; in the callback instead&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;First, let&#39;s try casting with JSDoc. This works because the TypeScript compiler
utilizes JSDoc annotations in type analysis. Here, we&#39;re just casting the
target object to the correct type before we use it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input.addEventListener(&#39;focus&#39;, e =&amp;gt; {
  const target = /** @type {HTMLInputElement} */ (e.target)
  target.select()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works and is happily accepted by the TypeScript checker. It&#39;s a little
noisy though. What about the second option?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input.addEventListener(&#39;focus&#39;, () =&amp;gt; input.select())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is accepted because the inferred type of &lt;code&gt;input&lt;/code&gt; is already correct, so no
cast is required. The type checker loves it! I like it too, it&#39;s even slightly
clearer than the original code.&lt;/p&gt;
&lt;h3&gt;What about implicit &lt;code&gt;any&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;I&#39;m already out of stuff to fix! But there is another knob we can mess with. By
default, the TypeScript checker will implicitly apply the &lt;code&gt;any&lt;/code&gt; type to values
where no type is specified or inferred. I usually have the &lt;a href=&quot;https://www.typescriptlang.org/tsconfig#noImplicitAny&quot;&gt;&amp;quot;implicit any&amp;quot;
option disabled&lt;/a&gt; in my TypeScript code bases, let&#39;s see what
happens when we do that here:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ npx tsc
&amp;lt;...list of errors elided&amp;gt;

Found 12 errors in 2 files.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look at that! There appear to be 12 places where the type of an expression is
not able to be inferred. Of course, it&#39;s all member declarations and method
parameters. Everything else is initialized with a value so it gets a type.&lt;/p&gt;
&lt;p&gt;Most of these were actually fairly easy to add, until I got to some code that
filters and maps over fetched data. I almost stopped here, but I became pretty
curious about how I&#39;d even do it. This is the code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const resultRows = this.index.notes
  .filter(note =&amp;gt; note.title.toLowerCase().indexOf(matchValue) &amp;gt;= 0)
  .map(note =&amp;gt; {
    const row = document.createElement(&#39;li&#39;)
    const item = row.appendChild(document.createElement(&#39;a&#39;))
    item.textContent = note.title
    item.href = note.url
    return row
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem here is that the &lt;code&gt;note&lt;/code&gt; parameter for &lt;em&gt;both&lt;/em&gt; inline callbacks
cannot be inferred by the type checker. Since they both accept the same object
type, I decide to declare it separately instead of repeating it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @typedef {object} Note
 * @property {string} title
 * @property {string} url
 */

const resultRows = this.index.notes
  .filter(
    /** @param {Note} note */ note =&amp;gt;
      note.title.toLowerCase().indexOf(matchValue) &amp;gt;= 0,
  )
  .map(
    /** @param {Note} note */ note =&amp;gt; {
      const row = document.createElement(&#39;li&#39;)
      const item = row.appendChild(document.createElement(&#39;a&#39;))
      item.textContent = note.title
      item.href = note.url
      return row
    },
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not entirely sure this is worth it! This kind of thing would make it easier to
use if it was a reusable module, but for a small self-contained component it
feels like overkill to me. I do enjoy the warm embrace of a type checker, so
I&#39;ll leave it in place.&lt;/p&gt;
&lt;p&gt;It&#39;s nice to be able to use modern validation tools on my browser-targeted
JavaScript code without committing to a bundler or build system. It shouldn&#39;t
really be a surprise, since TypeScript and JavaScript are still semantically
identical, but it&#39;s fun to see it in action.&lt;/p&gt;
</content>
  </entry>
</feed>

