Today I Learned

A Hashrocket project

Git Diff Matches Paths 📁

Today I learned that the git-diff command accepts a <path> argument. I used this knowledge to construct a command that runs only the RSpec test files on my branch that differ from master.

(other-branch)$ git diff master --name-only spec | xargs rspec

This is handy on a big test suite; when you can’t run every test, make sure you run the ones that you have changed.

See man git-diff for more info.

Turn on coordinates in Minecraft Realm

In a Minecraft Realm the option to turn on the coordinate system is not available without turning on cheats. That is not good because then you’ll lose achievements.

Many tutorials recommend downloading the realm locally, flipping the switch for coordinates, and then uploading that copy of your world back to the realm. Turns out that some server commands will work in the Realm… coordinates being one of them!

Enter this into your chat window to enable coordinates:

/gamerule showcoordinates true

You can, of course, turn them back off via:

/gamerule showcoordinates false

Auth with gh on remote machine

gh is a new github cli in the alpha phase right now hosted at https://github.com/cli/cli. It’s auth right now is through the browser which doesn’t work great on a remote machine.

When you try to do something with gh that should connect to github you’ll get a bunch of errors and this:

$ gh pr create
Please open the following URL manually:
https://github.com/login/oauth/authorize?client_id=XXXXXXXXXXXXXXXXXX&redirect_uri=http%3A%2F%2Flocalhost%3A34869%2Fcallback&scope=repo&state=XXXXXXXXXXXXXXXXXXXX

This is running a server in the background that is waiting for a request, don’t kill the process until the authentication is complete.

You can copy that url to your browser, authenticate with github and then you are redirected to a localhost url where the server is hosted

Copy the localhost url that you were redirected to, open a new window and then curl the localhost url.

$ curl http://localhost:34869/callback&scope=repo&state=XXXXXXXXXXXXXXXXXXXX

Now you should be authenticated and the gh process should continue.

Search in dotfiles with ripgrep

ripgrep is a very fast searching file system searching utility written in Rust. General usage is like this:

rg something directory-name

This is great, but when searching in my dotfiles for a configuration I don’t find what I’m looking for. Any file whose filename starts with . is considered hidden and these files are not searched by default with ripgrep.

To search hidden files (or dot files) use the --hidden flag.

rg --hidden "alias git" ~

Now, if there is a configuration that is overriding git in my dot files, I’ll find it!

Mac Notes Default Text Size 🔎

I use Mac Notes a lot during pairing and brainstorming. It works, and it’s right there on the Mac!

One issue I’ve had is that the default text size on Mac Notes is unreadably small on a big monitor or iMac. I always end up creating a note, then bumping the text size with CMD-SHIFT ..

Of course, there’s a better way. Set the default font size in preferences.

image

Expo PushNotifications with pop-ups on Android

Expo has an amazing push notification service. In order to display push notifications in a pop-up style (the default on iOS) on Android, you must do two things:

  1. Create an Android push notification channel
  2. Set the priority and channelId in the push notification payload to expo.

Create channel example:

import React from "react";
import { Platform } from "react-native";
import { Notifications } from "expo";

class App extends React.Component<Props> {
  componentDidMount() {
    if (Platform.OS === "android") {
      Notifications.createChannelAndroidAsync("chat-messages", {
        name: "Messages",
        priority: "max",
        sound: true,
        vibrate: [0, 250, 500, 250]
      });
    }
  }

  render() {
    // ...
  }
}

Send PN with priority and channel example:

curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/send" -d '{
  "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
  "title":"New Message",
  "body": "Are you there?",
  "channelId": "chat-messages",
  "priority": "max"
}'

Byline Class and Time Tag 📰

I’ve been hacking on TIL to optimize our site for Safari Reader mode. A lot of this is black box, because Apple doesn’t have easy-to-find documentation about how the feature works.

One thing that seems to matter is a class called byline for your author information, and an HTML tag time for the publication date. Here’s an example, borrowed from this site:

<p class="byline">
  <%= name %>
  <time datetime="<%= inserted_at %>">
    <%= inserted_at %>
  </time>
</p>

It gives us relevant reader information, shown here in Safari:

image

Here’s the PR:

https://github.com/hashrocket/tilex/pull/650

Ergodox vim input for numpad keys can be wonky

I use the ergodox ez keyboard. When you’re setting it up you can select either the numpad value of a key, or the shifted/regular value. These will behave slightly differently depending on your settings for application keypad mode. This is something that is set at the application layer, so you may end up dealing with vim recieing <esc>Ok if you are trying to type +. You can either try to find the way to get your application (in my case Manjaro’s terminal emulator) to change the keypad mode (I couldn’t find that), or you can setup an innoremap section in your vimrc to cover the cases where you definitely don’t want the escape sequence.

:inoremap <Esc>Oq 1
:inoremap <Esc>Or 2
:inoremap <Esc>Os 3
:inoremap <Esc>Ot 4
:inoremap <Esc>Ou 5
:inoremap <Esc>Ov 6
:inoremap <Esc>Ow 7
:inoremap <Esc>Ox 8
:inoremap <Esc>Oy 9
:inoremap <Esc>Op 0
:inoremap <Esc>On .
:inoremap <Esc>OQ /
:inoremap <Esc>OR *
:inoremap <Esc>Ol +
:inoremap <Esc>OS -
:inoremap <Esc>OM <Enter>

This sequence is for PuTTY, but you can see which key to put after the <Esc>O by looking at what registers on keydown events. Alternatively, for ergodox users, only use the shifted/regular values for keys.

Meta Charset Top That! 🔝

When writing HTML, make sure that your meta charset tag is in the first 1024 bytes on the page.

Per MDN:

The <meta> element declaring the encoding must be inside the <head> element and within the first 1024 bytes of the HTML as some browsers only look at those bytes before choosing an encoding.

What does this mean practically? Keep this tag near the top of your document, and use a website validator like https://validator.w3.org to check your tag placement.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta

Prefetch images in ReactNative

When a react native app boots and there is a known set of user/group avatar urls received from an API, it may be a better user experience to load them before hand.

React Native’s Image component has a prefetch method that will save the image into the device’s image cache:

import { Image } from "react-native";

const App = () => {
  const { users, loading } = useUserFetcher();

  users.forEach(u => Image.prefetch(u.avatarURL));

  return (
    <View>
      <UserList users={users} loading={loading} />
    </View>
  );
};

Type "text/javascript" is Redundant

Have a script tag in your HTML annotated with <script type="text/javascript">? Per the HTML5 specification, the MIME type of a script tag, when omitted, is text/javascript. We are encouraged to omit it.

Delete that code and go green 💚.

Per MDN:

Omitted or a JavaScript MIME type: This indicates the script is JavaScript. The HTML5 specification urges authors to omit the attribute rather than provide a redundant MIME type. In earlier browsers, this identified the scripting language of the embedded or imported (via the src attribute) code. JavaScript MIME types are listed in the specification.

Source:

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script

Swap between STI classes in Rails

Rails allows you to swap between single table inheritance classes with the becomes method. This method creates a new instance of the desired class, and passes the attributes from the original record to the new instance.

So if I have two classes, User and Admin, where Admin inherits from User:

class User
  # ...
end

class Admin < User
  # ...
end

I can change between the two classes using becomes:

user = User.first # returns an instance of the User class
user.class # => User
user.id # => 1
user.name # => Joe Hashrocket

admin = user.becomes(Admin) # returns an instance of the Admin class
admin.class # => Admin
admin.id # => 1
admin.name # => Joe Hashrocket

RN fetch on Android requires mime type

ReactNative on Android requires a mime type when uploading files with FormData. But the only way to get the mime type of a user-chosen file is to read the file or files into memory after a user select one or more. In all of my use cases, my server didn’t care if the mimetype was in the FormData.

The easy solution is to just set the mimetype to a binary file type (e.g. application/octet-stream):

let data = new FormData();
data.append("file", {
  name: "cool.pdf",
  uri: "///files/cool.pdf",
  // MIME-type required on Android
  type: "application/octet-stream"
});

fetch("http://example.com/upload-file", data);

Disable Spring in Rails with `DISABLE_SPRING`

This is fairly self explanatory. If you don’t want spring running, because maybe you forget it’s on and then you’re confused when you’re making changes to configuration and you don’t see those changes manifest, then you can disable spring with:

DISABLE_SPRING=true

But be careful about where you put this. If you are using dotenv and you place it in your .env file it will not take effect. dotenv sets its environment after spring has started.

This is something that you should put in bashrc or zshrc.

export DISABLE_SPRING=true

Ecto queries can be combined

Queries can take other queries to build on top of, which is very useful for conditional query logic:

def find_posts(group_id, title) do
  posts =
    from(posts in Phoenix.Post,
      where: posts.group_id == ^group_id,
    )
    |> title_search(title)
    |> Phoenix.Repo.all()
end

defp title_search(query, title) do
  query
    |> where([posts], posts.title == ^title)
end

defp title_search(query, nil) do
  query
end

💅 styled-components limit prop names

💅 styled-components is one of my absolute favorite libraries! ❤️ I just have to be careful not use prop names for my components that could be interpreted as styles.

For example:

interface Props {
  position: "left-side" | "right-side";
  item: Item;
}

const ListItem= styled.View<{position: "left-side" | "right-side"}>(({position}) => ({
  backgroundColor: position === "left-side" ? "burlywood" : "skyblue";
}));

const ListItemContainer: React.FC<Props> = ({position, item}) => {
  return (
    <ListItem position={position}>
      <Text>{item.name}</Text>
    </ListItem>
  );
}

<ListItemContainer item={item} position="left-side" />

This will return an error because in StyleSheet, position must be one of absolute or relative

Do float division with `fdiv`

Usually in ruby, when I need to perform floating point division I call .to_f on either the numerator or denominator, like this:

1 / 5.to_f
# 0.2

While many people might read this as floating point division instead of integer division, reading it that way gets trickier when those numbers are variables and the .to_f happens somewhere else.

You can be more explicit about the operation you are performing with .fdiv.

1.fdiv(5)
# 0.2

This works with floats as well, so that you can use fdiv in any context.

1.to_f.fdiv(5.to_f)
# 0.2

Remove newlines from strings in Rails

Rails has a method called squish that will remove newlines from strings. It works very nicely with heredocs, where you may want readability but don’t really want the newlines.

Without squish:

<<~SQL
  update posts
  set status = 'public'
  where status is null;
SQL
# "update posts\nset status = 'public'\nwhere status is null;\n"

With squish:

<<~SQL.squish
  update posts
  set status = 'public'
  where status is null;
SQL
# "update posts set status = 'public' where status is null;"

Couldn't decrypt `config/credentials.yml.enc`

Rails 5.2 introduced the credentials system for managing secure credentials in your application. You can edit the file with rails credentials:edit, which is pretty dope. But if you do like I did and you start your project on one machine and go to try and edit that file on a different machine after you pull down the repository from Github, it will generate a new master.key file for you. This will not work to decrypt the file, and you’ll get the error above. You need to make sure that you grab the file from wherever you started your application from. In my case it was as simple as using scp to grab the file from a different machine.

How to manually edit ufw rules

Usually you don’t want to manually edit ufw, but in this case I just needed to update an ip address across multiple ip4 and ip6 rules. Turns out there is config file that can be very carefully edited:

$ sudo vim /lib/ufw/user.rules
$ sudo vim /lib/ufw/user6.rules

Very carefully update the ip addresses and then reload ufw:

$ sudo ufw reload

Constrain a route to a specific host

If you have two hosts that point to one rails app, like apple.myapp.com and orange.myapp.com and you’d like a specific route to only be availabe on orange then you can constrain the route based on the host like this:

constaints(host: 'orange.myapp.com') do
  get '/thing/:id', to: 'thing#show'
end

And if you had additional host for banana that should also get this route:

constaints(host: ['orange.myapp.com', 'banana.myapp.com']) do
  get '/thing/:id', to: 'thing#show'
end

But in this case only the subdomain differs so we can do this:

constaints(subdomain: ['orange', 'banana']) do
  get '/thing/:id', to: 'thing#show'
end

Git Interactive Single Key 👇

Today I became familiar with the Git configuration interactive.singleKey. In the Hashrocket dotfiles this is enabled by default, and I’m not sure how common it is out in the wild. To quote the docs:

In interactive commands, allow the user to provide one-letter input with a single key (i.e., without hitting enter).

This allows git add --patch to be very powerful by enabling a quick vote up or down on a patch with a single keystroke.

Make sure you have the Perl module Term::ReadKey installed; it is a dependency.

What's my public IP? #automation #linux #terminal

Ever wonder what your public IP looks like? When I do, I usually search duckduckgo.com for “what’s my ip” and it is nice enough to tell me that at the top above the search results.

But what if I want to do that at the terminal, and perhaps use that information in a script?

TIL about ipinfo.io! curl it and you get your ip, hostname, city, state, zipcode, country, ISP, timezone, even lat & lng coordinates of the rough location!

curl ipinfo.io
{
  "ip": "111.111.111.111",
  "hostname": "redacted.comcast.com",
  "city": "Chicago",
  "region": "Illinois",
  "country": "US",
  "loc": "41.8500,-87.6500",
  "org": "AS7922 Comcast Cable Communications, LLC",
  "postal": "60666",
  "timezone": "America/Chicago",
  "readme": "https://ipinfo.io/missingauth"
}

This information is not fully accurate, but it’s good enough for scripts, and because it comes back in json form you can pipe it to jq to extract info or read it directly into your scripts.

See git history of a renamed file

If you rename a file, git won’t show history of the previous name:

$ git log --pretty=oneline things/text.txt
8567d... Move file into things directory

However, the --follow flag will allow you to see the history of commits beyond the rename:

$ git log --follow --pretty=oneline things/text.txt
8567d... Move file into things directory
1458a... Fix something
0aac5... Add something

Docs:

--follow
  Continue listing the history of a file beyond renames (works only for a single file).

Import Absolute Paths in Typescript Jest Tests

In order to avoid this:

// project/__tests__/stuff/someDistantCousin.test.ts
import { thing } from '../../src/stuff/someDistantCousin'
import { wrapFunction } from '../testUtils/firebase'

And to write this instead:

import { thing } from 'src/stuff/someDistantCousin'
import { wrapFunction } from 'tests/testUtils/firebase'

There are 2 things to configure:

  1. Jest Config (i.e. jest.config.js, package.json, etc…)
  2. Typscript Config: tsconfig.json

jest.config.js

module.exports = {
  moduleNameMapper: {
    'src/(.*)': '<rootDir>/src/$1',
    'tests/(.*)': '<rootDir>/__tests__/$1',
  },
}

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "src/*": ["src/*"],
      "tests/*": ["__tests__/*"]
    }
  }
}

Show All Docker Containers (Running & Stopped)

docker ps was a confusing command for me, because I thought that it would show all containers by default. But trying to run a container that has been stopped will give you a an error that there is a container in use with that name. Running docker ps will be fruitless because that container will not be listed by default. You can easily view all your containers, running & stopped, with:

docker ps -a

H/T to the docs lol - https://docs.docker.com/engine/reference/commandline/ps/

How to typescript react native list refs

VirtualLists (i.e. SectionList, FlatList) have always been dificult for me to describe in typescript, but the following seems to work:

interface Props {
  list: MutableRefObject<SectionList<any> | undefined>;
}

Example:

import React, { MutableRefObject } from "react";
import { SectionList, View, TouchableOpacity, Text } from "react-native";

interface Props {
  listRef: MutableRefObject<SectionList<any> | undefined>;
}

const Button = (props: Props) => {
  return (
    <TouchableOpacity
      onPress={() => {
        props.listRef.current?.scrollToLocation({
          itemIndex: 0,
          sectionIndex: 0,
          animated: true
        });
      }}
    >
      <Text>Scroll Up</Text>
    </TouchableOpacity>
  );
};

const Screen = () => {
  const listRef = useRef<SectionList<any>>();

  return (
    <View>
      <SectionList ref={listRef} />
      <Button listRef={listRef} />
    </View>
  );
};

Search and Replace Control Characters In Vim

In vim, if you want to search and replace non-printable control characters (for example you have ^M scattered throughout your file and it’s messing up an import) you can use ctrl+v+<char>. The ctrl+v allows you to type the control character, but you must hold down control for both of the key presses.

:%s/ctrl+v+M/

This would search and replace all non-printable ^M characters in your text.

Slack Hot-Keys Rock

Do you like slack? Chances are it doesn’t really matter whether you do or don’t. It’s a matter of course at this point that you will probably work with it. Because that’s the case, you should at least strive to be as aware of neat features as possible!

To that end, you should check out the built-in hot-key help using:

MacOS: Command + / Linux/Windows: Ctrl + /

I’m particularly fond of the “Navigation” and “Channels & Direct Messages”!

No ifconfig on Linux?

If you are on a Linux machine and trying to run ifconfig you may get Command not found: ifconfig. That is because some Linux distros don’t come with it bundled by default.

On Manjaro Linux which is an Arch Linux based distro you can use pacman to install the package containing ifconfig called net-tools:

sudo pacman -S net-tools

On Debian based distros you can use:

sudo apt-get install net-tools

If you don’t want to install net-tools you can try using the ip command:

ip addr

This will return the interfaces on your machine and their IP bindings.

How to redirect standard error

2> Allows us to redirect standard error.

Taking advantage of rm’s ability to not delete files recursively can come in handy, especially when writing clean up scripts, but it can be noisy when you don’t care.

Give this file structure:

~/
  tmpfile1.txt
  tmpfile2.txt
  tmpfile3.txt
  tmpfile4.txt
  do-not-delete/
    secrets.yml

I may want to delete all the files, but not touch the directories (to keep file removal simple)

$ rm *
rm: cannot remove 'do-not-delete': Is a directory

$ ls
~/
  do-not-delete/
    secrets.yml

Because the directory error message comes over stderr, we can simply redirect it to /dev/null to ignore it:

rm * 2> /dev/null

Most recent branches

You can get a list of all the branches in git using the very programmatically named for-each-ref command. This command by itself isn’t very useful and needs quite a bit of tailoring for it to produce the information you want.

Just by itself it returns all refs

git for-each-ref
# returns all the references

This shows all refs (branches, tags, stashes, remote branches). To just get branches pass an argument that corresponds to the .git directory that contain branch refs.

git for-each-ref ref/heads

To reduce the number of refs it produces use --count

git for-each-ref --count=5
# returns just 5 refs

This outputs a line that looks like this:

c1973f1ae3e707668b500b0f6171db0a7c464877 commit refs/heads/feature/some-feature

Which is a bit noisy. To reduce the noise we can use --format.

git for-each-ref --format="$(refname)"
# line is just: refs/heads/feature/some-feature

Which can be shortened with :short

git for-each-ref --format="$(refname:short)"
#just: feature/some-feature

To get most recent branches we’ll need to sort. The - in front of the field name reverses the sort.

git for-each-ref --sort=-committerdate

All together:

git for-each-ref \
--format="%(refname:short)" \
--count=5 \
--sort=-committerdate \
refs/heads/

How to only stage deleted files

If I delete a bunch of files and just want to stage the deleted ones, I can use xargs to add them:

$ git ls-files --deleted | xargs git add

Example output:

$ git status

  deleted: lib/to/deleted_1.txt
  deleted: lib/to/deleted_2.txt
  modified lib/that/changed_1.txt
  deleted: lib/to/deleted_3.txt
  deleted: lib/to/deleted_4.txt
  modified lib/that/changed_2.txt
  deleted: lib/to/deleted_5.txt
  modified lib/that/changed_3.txt
$ git ls-files --deleted | xargs git add
Changes to be committed:

  deleted: lib/to/deleted_1.txt
  deleted: lib/to/deleted_2.txt
  deleted: lib/to/deleted_3.txt
  deleted: lib/to/deleted_4.txt
  deleted: lib/to/deleted_5.txt

Changes not staged for commit:

  modified lib/that/changed_1.txt
  modified lib/that/changed_2.txt
  modified lib/that/changed_3.txt

How to change the placeholder text color

The TextInput component in react-native has a property of placeholderTextColor that allows you to configure the placeholder text color! However, react-native-web does not have this property and it must be done in css. So if you were using react-native-web you would need to include .css file:

// screen.tsx
import "./styles.css";
import { TextInput } from "react-native";

const Screen = () => (
  <TextInput
    testID="change-placeholder-color"
    placeholder="enter some text"
    defaultValue=""
  />
);
/************* 
*
*  styles.css
*
**************/
[data-testid="change-placeholder-color"]::-webkit-input-placeholder {
  color: skyblue;
}
[data-testid="change-placeholder-color"]::-moz-placeholder {
  color: skyblue;
}
[data-testid="change-placeholder-color"]::-ms-placeholder {
  color: skyblue;
}
[data-testid="change-placeholder-color"]::placeholder {
  color: skyblue;
}

Import GraphQL Queries Where You Need Them (CRA)

If you want to export/import your GraphQL queries in your Create-React-App, you’ll need to add a library to make it work. You can use a library called grapql.macro to use .gql or .graphql files in your app.

If you’re also using jest, you’ll need to add jest-transform-graphql and update your jest configuration to pick up these file types.

Source: https://www.apollographql.com/docs/react/integrations/webpack/#jest

Do not prefix TypeScript interface names

TLDR: If you’re writing TypeScript, you’re using intellisense, and it will yell at you if you try to use an interface as a variable. So there is no need anymore for naming conventions like IMyInterface or AMyAbstractClass.

When I first started using TypeScript in 2018 I saw a lot of people’s code prefixing interfaces with “I”:

interface ICar {
    color: string
    horsepower: number
}

function drive(car: ICar) { /*...*/ }

However, that is a convention borrowed from the past (other languages). It used to be mentioned in the TypeScript handbook to not prefix, but that generated more argument than resolution. The handbook code examples, however, do not use prefixes.

I believe modern dev environments have relieved us from the need to include type and contextual information into the names of many code things, not just interfaces.

Now we get proper type and context from things like intellisense and language servers, whether it’s inferred or strictly typed. This frees us to have names for things that can be more descriptive of the processes and data in question.

Control your magic strings in Firebase projects

Firebase’s real-time database JavaScript API makes it really easy to wind up with a ton of magic strings in your codebase. Since you access data via “refs” like:

firebase.database.ref(`customers/1`).update({ name: 'Mike' })

In that example, “customers/1” is a magic string that points to some data in the database. Magic strings are well known as something to avoid in software development. And here we are updating a database with one, yikes!

These can easily be managed via patterns. I’ve been abstracting these into their own modules in an “API-like” pattern. For example:

// api/customers.ts
import { protectedRef } from 'protected-ref'

export const rootRef = 'customers'
export const getCustomerRef = (customerID: string) => protectedRef(rootRef, customerID)
export const updateCustomer = (customerID: string, updateDocument: CustomerUpdateDocument) => getCustomerRef(customerID).update(updateDocument)

And then use it like:

import { updateCustomer } from '../api/customers'

updateCustomer('1', { name: 'Mike' })

Also protected-ref is a firebase package that manifested from this TIL: https://til.hashrocket.com/posts/hyrbwius3s-protect-yourself-against-firebase-database-refs

What does that git alias do? 🕵️‍♂️

I have some git aliases in my dotfiles and sometimes I use an alias for too long that I actually forget what it does under the hood.

I can open my ~/.gitconfig file in nvim (which I have an alias for) and search for the line that introduced the alias, but when pairing sometimes it’s easier and faster to just use git help to get the definition of an alias:

git help doff
'doff' is aliased to 'reset HEAD^'

Postgres `null` and `where <VALUE> not in`

Always watch out for null in Postgres. When null sneaks into a result set it may confuse the results of your query.

Without nulls a where in query could look like this:

psql> select 'found it' as c0 where 1 in (1);
    c0
----------
 found it
 (1 row)

For the where in clause a null does not change the results.

psql> select 'found it' as c0 where 1 in (null, 1);
    c0
----------
 found it
(1 row)

The where not in formulation however is sensitive to null. Without a null it looks like this:

psql> select 'found it' as c0 where 17 not in (1);
    c0
----------
 found it
(1 row)

Add in the null and the results can be counterintuitive:

psql> select 'found it' as c0 where 17 not in (1, null);
 c0
----
(0 rows)

Watch out for those nulls!!

Browsers have a Web Cryptography API

All major versions of browser implement a Web Cryptography API for obtaining random numbers:

const numbers = new Uint32Array(1);
window.crypto.getRandomValues(numbers);
console.log(numbers);
// => [1627205277]

However, the methods are overridable, not read-only, and are vulnerable to polyfill attacks. This shouldn’t be used yet and is still being developed, but I found out the API exists.