Skip to content

Instantly share code, notes, and snippets.

@MineBartekSA
Last active March 18, 2024 21:03
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save MineBartekSA/1d42d6973ddafb82793fd49b4fb06591 to your computer and use it in GitHub Desktop.
Save MineBartekSA/1d42d6973ddafb82793fd49b4fb06591 to your computer and use it in GitHub Desktop.
CatBox - An implementation of catbox.moe API in Bash
#!/bin/bash
#
# CatBox v2.0
# An implementation of catbox.moe API in Bash
# Author: MineBartekSA
# Gist: https://gist.github.com/MineBartekSA/1d42d6973ddafb82793fd49b4fb06591
# Change log: https://gist.github.com/MineBartekSA/1d42d6973ddafb82793fd49b4fb06591?permalink_comment_id=4596132#gistcomment-4596132
#
# MIT License
#
# Copyright (c) 2023 Bartłomiej Skoczeń
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
VERSION="2.0"
CATBOX_HOST="https://catbox.moe/user/api.php"
LITTER_HOST="https://litterbox.catbox.moe/resources/internals/api.php"
HASH_FILE="$HOME/.catbox"
CURL_ADD=""
RESET="\e[0m"
BOLD="\e[1m"
RED="\e[91m"
YELLOW="\e[93m"
## Utils
function no_color() {
unset RESET BOLD RED YELLOW
}
function version() {
echo -e $BOLD"CatBox"$RESET" v"$VERSION >&5
echo "A catbox.moe API implementation in Bash"
}
function usage() {
[ -z $1 ] && version || echo $1
echo
echo "Usage: catbox <command> [arguments] [options]"
echo
echo "Commands:"
echo " user [user hash] - Gets current or sets global user hash. Pass 'off' to remove global user hash"
echo " file <filename(s)> - Upload files to catbox.moe"
echo " temp <filename(s)> [expiary] - Upload files to litterbox.catbox.moe"
echo " url <url(s)> - Upload files from URLs to catbox.moe"
echo " delete <filenames(s)> - Delete files from catbox.moe"
echo " album - Album Managment"
echo
echo "Global options:"
echo " -s, --silent - Only output upload links (stderr will still show)"
echo " -S, --silent-all - Silent option but also silences stderr"
echo " -n, --no-color - Disable output coloring"
echo " -u, --user-hash[=] - Pass user hash"
echo " -V, --verbose - Show verbose output (in album)"
}
function has_hash() {
[ -z "$HASH" ] && [ -z "$USER_HASH" ] && echo false || echo true
}
## Command functions
function upload_files() {
declare -i fail=0
for file in "${@:2}"
do
name=$(basename -- "$file")
echo -e $BOLD"$name"$RESET":"
if ! ( [ -f "$file" ] || [ -L "$file" ] || [ "$file" == "-" ] )
then
echo -e $BOLD$RED"File '$file' doesn't exist!"$RESET >&2
fail+=1
continue
fi
link=$(curl --fail-with-body -F reqtype=fileupload $CURL_ADD -F "fileToUpload=@$file" $1)
if [ $? -ne 0 ]
then
echo -e $BOLD$RED"Failed to upload: "$RESET$RED$link$RESET >&2
fail+=1
continue
fi
echo -n $link | xclip -selection clipboard
echo -en "Uploaded to: "$BOLD
echo $link >&5
echo -en $RESET
done
[ $fail -eq $[$#-1] ] && exit 2
return 0
}
function catbox_command() {
curl -s --fail-with-body -F reqtype=$1 $CURL_ADD "${@:2}" $CATBOX_HOST &
pid=$!
if [ ! $SILENT ]
then
echo -en "\e[sPlase wait... |" >&5
declare -i stage=1
while ps -p $pid > /dev/null
do
case $stage in
0 | 4)
echo -en "\e[1D|" >&5
;;
1 | 5)
echo -en "\e[1D/" >&5
;;
3 | 7)
echo -en "\e[1D\\" >&5
;;
2 | 6)
echo -en "\e[1D-" >&5
;;
esac
stage+=1
[ $stage -eq 8 ] && stage=0
sleep 0.1
done
echo -ne "\e[u\e[KDone!" >&5
fi
wait $pid
}
function generic_command() {
declare -i fail=0
for item in "${@:5}"
do
echo -en $BOLD"$($3 "$item")"$RESET": "
res=$(catbox_command $1 -F "$2=$item")
if [ $? -eq 0 ]
then
$4 "$res"
else
[ $SILENT ] && echo -en $RED"$item: " >&2 || echo -en "\e[u"
echo -e $RED$res$RESET >&2
fail+=1
fi
done
[ $fail -eq $[$#-4] ] && exit 2
return 0
}
function url_success() {
echo -en "\e[u"
echo $* >&5
echo -n $* | xclip -selection clipboard
}
function upload_urls() {
generic_command urlupload url "basename -- " url_success $@
}
function delete_success() {
echo -e "\e[uSuccesfully deleted"
}
function delete_files() {
echo "Deleting..."
generic_command deletefiles files echo delete_success $@
}
function album_usage() {
echo "Usage: catbox album <command> [arguments]"
echo
echo -e $BOLD$YELLOW"Note: Every album command requires user hash"
echo -e " For title or description, double quote every text longer than one word"$RESET
echo
echo "Commands:"
echo " create <title> <description> <file(s)> - Create album"
echo " edit <short> <title> <description> [file(s)] - Modify album"
echo " add <short> <file(s)> - Add files to an album"
echo " remove <short> <file(s)> - Remove files from an album"
echo " delete <short> - Delete album"
}
function album_create() {
files="${@:3}"
echo "Creating album..."
if [ $VERBOSE ]
then
echo "Title : $1" >&5
echo "Description: $2" >&5
echo "Files : $files" >&5
fi
album=$(catbox_command createalbum -F "title=$1" -F "desc=$2" -F "files=$files")
if [ $? -ne 0 ]
then
exec >&2
echo -e $RED$BOLD"Failed to create a new album!"$RESET
echo -e $RED$album$RESET
exit 2
fi
echo -n $album | xclip -selection clipboard
echo -e "\nAlbum created successfully"
if [ $VERBOSE ]
then
echo "Album short: ${album:21}" >&5
echo "Album url : $album" >&5
else
echo "${album:21} | $album" >&5
fi
}
function album_edit() {
files="${@:4}"
echo "Modifing album..."
if [ $VERBOSE ]
then
echo "Album Short: $1" >&5
echo "Title : $2" >&5
echo "Description: $3" >&5
echo "Files : $files" >&5
fi
res=$(catbox_command editalbum -F "short=$1" -F "title=$2" -F "desc=$3" -F "files=$files")
if [ $? -ne 0 ]
then
exec >&2
echo -e $RED$BOLD"Failed to modify album!"$RESET
echo -e $RED$res$RESET
exit 2
fi
echo -e "\nAlbum modified successfully"
}
function album_add() {
files="${@:2}"
echo "Adding files to the album..."
if [ $VERBOSE ]
then
echo "Album short: $1"
echo "Files : $files"
fi
res=$(catbox_command addtoalbum -F "short=$1" -F "files=$files")
if [ $? -ne 0 ]
then
exec >&2
echo -e $RED$BOLD"Failed to add files to the album!"$RESET
echo -e $RED$res$RESET
exit 2
fi
echo -e "\nSuccessfully added files to the album"
}
function album_remove() {
files="${@:2}"
echo "Removing files from the album..."
if [ $VERBOSE ]
then
echo "Album short: $1"
echo "Files : $files"
fi
res=$(catbox_command removefromalbum -F "short=$1" -F "files=$files")
if [ $? -ne 0 ]
then
exec >&2
echo -e $RED$BOLD"Failed to remove files from the album!"$RESET
echo -e $RED$res$RESET
exit 2
fi
echo -e "\nSuccessfully removed files from the album"
}
function album_delete() {
echo "Deleting albums..."
generic_command deletealbum short echo delete_success $@
}
## Start
# Check if curl exists
curl --version >> /dev/null
if [ $? -ne 0 ]
then
echo -e $RED"cURL not found!"$RESET >&2
echo "Please check if you have cURL installed on your system" >&2
exit 3
fi
# Setup a file descriptor for bypassing silent option
exec 5<&1
# Handle global options
declare -i count=1
while [ $count -le $# ]
do
case ${!count} in
-S | --silent-all)
exec 2>/dev/null
set -- "${@:1:$count-1}" -s -s "${@:$count+1}"
;;
-s | --silent)
exec >/dev/null
SILENT=1
;;
-h | --help | --usage)
exec 5>/dev/null
usage
exit 0
;;
-v | --version)
version
exit 0
;;
-n | --no-color)
no_color
;;
-u | --user-hash | --user-hash=*)
if [[ ${!count} == --user-hash=* ]]
then
HASH=${!count:12}
else
get=$[$count+1]
HASH=${!get}
set -- "${@:1:$count-1}" "${@:$count+1}"
fi
[ ! -z "$HASH" ] && CURL_ADD="-F userhash=$HASH "
;;
-V | --verbose)
VERBOSE=1
;;
*)
count+=1
continue
esac
set -- "${@:1:$count-1}" "${@:$count+1}"
done
unset count no_color
# Read user hash if it was not given through global options
if [ -z ${HASH+x} ] && [ -f $HASH_FILE ]
then
while read line
do
if [[ $line != \#* ]] && [ "$line" != "" ]
then
USER_HASH=$line
CURL_ADD="-F userhash=$USER_HASH "
break
fi
done < $HASH_FILE
unset line
fi
# Handle commands
case $1 in
version)
version
;;
help | usage)
exec 5>&1
usage
;;
user)
if [ -z $2 ]
then
if [ "$(has_hash)" == "true" ]
then
if ! [ -z "$HASH" ]
then
echo "User hash given!"
echo -n "User hash: "
echo $HASH >&5
else
echo "User hash present!"
echo -n "User hash: "
echo $USER_HASH >&5
fi
echo "CatBox will act as you"
else
echo "No user hash"
echo "CatBox will act annonymously"
fi
elif [ "$2" == "off" ]
then
rm $HASH_FILE
echo "CatBox will now upload annonymously"
else
echo -e "# CatBox v2 User Hash\n$2" > $HASH_FILE
echo "User hash set!"
echo "CatBox will now upload files to your account"
fi
;;
file)
if [ $# -eq 1 ]
then
exec >&2
echo "Usage: catbox file <filename> [<filename>...] - Upload files to catbox.moe"
echo "Anonymously uploaded files cannot be deleted"
exit 1
fi
[ "$(has_hash)" == "false" ] && echo "Uploading annonymously..." || echo "Uploading..."
upload_files $CATBOX_HOST "${@:2}"
;;
temp)
if [ $# -lt 2 ]
then
exec >&2
echo "Usage: catbox temp <filename> [<filename>...] [1h/12h/24h/72h] - Upload files to litterbox.catbox.moe"
echo "Only the given expiry times are supported"
echo "By default, temporary files will expire after an hour"
exit 1;
fi
[[ ${@: -1:1} == @(1|12|24|72)h ]] && time=${@: -1:1} && end=-1 || time=1h || end=0
CURL_ADD="-F time=$time"
echo "Uploading temporarily..."
upload_files $LITTER_HOST "${@:2:$#-1$end}"
;;
url)
if [ $# -eq 1 ]
then
exec >&2
echo "Usage: catbox url <url> [<url>...] - Upload files from urls to catbox.moe"
echo "Anonymously uploaded files cannot be deleted"
exit 1
fi
[ "$(has_hash)" == "false" ] && echo "Uploading annonymously..." || echo "Uploading..."
upload_urls "${@:2}"
;;
delete)
if [ $# -eq 1 ]
then
exec >&2
echo "Usage: catbox delete <filename> [<filename>...] - Delete files from your catbox.moe account"
echo "This command required a catbox.moe account"
echo "Please add your user hash by using the catbox user command"
echo "Filenames must be the names of files already hosted on catbox.moe"
echo "Anonymously uploaded files cannot be deleted"
exit 1
elif [ "$(has_hash)" == "false" ]
then
exec >&2
echo -e $BOLD$RED"No user hash!"$RESET
echo -e $RED"Please add your user hash"
echo -e "Use the catbox user command to do so"$RESET
exit 1
fi
delete_files ${@:2}
;;
album)
if [ $# -gt 1 ] && [ "$(has_hash)" == "false" ]
then
exec >&2
echo -e $BOLD$RED"No user hash!"$RESET
echo -e $RED"Please add your user hash"
echo -e "Use the catbox user command to do so"$RESET
exit 1
fi
case $2 in
create)
if [ $# -lt 5 ]
then
exec >&2
echo "Usage: catbox album create <title> <description> <filename> [<filename> ...] - Create an album with given title, description, and files"
echo -e $YELLOW"For title or description, double quote every text longer than one word"$RESET
echo "Filenames must be the names of files already hosted on catbox.moe"
exit 1
fi
album_create "$3" "$4" ${@:5}
;;
edit)
if [ $# -lt 5 ]
then
exec >&2
echo "Usage: catbox album edit <short> <title> <description> [<filename> ...] - Modify the entirety of the album"
echo -e $YELLOW"For title or description, double quote every text longer than one word"
echo -e "Filenames are not necessary, but given none, the album will become empty"$RESET
echo "Filenames must be the names of files already hosted on catbox.moe"
exit 1
fi
album_edit $3 "$4" "$5" ${@:6}
;;
add)
if [ $# -lt 4 ]
then
exec >&2
echo "Usage: catbox album add <short> <filename> [<filename> ...] - Add files to the album"
echo "Filenames must be the names of files already hosted on catbox.moe"
exit 1
fi
album_add $3 ${@:4}
;;
remove)
if [ $# -lt 4 ]
then
exec >&2
echo "Usage: catbox album remove <short> <filename> [<filename> ...] - Remove files from the album"
echo "Filenames must be the names of files already hosted on catbox.moe"
exit 1
fi
album_remove $3 ${@:4}
;;
delete)
if [ $# -lt 3 ]
then
echo "Usage: catbox album delete <short> [<short> ...] - Delete album(s)" >&2
exit 1
fi
album_delete ${@:3}
;;
*)
exec >&2
album_usage
exit 1
esac
;;
*)
exec >&2
exec 5>&2
usage
exit 1
esac
@MineBartekSA
Copy link
Author

@carbolymer Thanks again for your comment! This feature is now in the gist as well!

@alsoGAMER
Copy link

@MineBartekSA i've submitted this script to the aur under this repo https://aur.archlinux.org/packages/catbox-bash, if this is an issue for you, let me know and i'll delete it

@MineBartekSA
Copy link
Author

@alsoGAMER Thank you very much for submitting my little script to the AUR.

@VMpc
Copy link

VMpc commented Jun 3, 2023

Added support for litterbox.catbox.moe (Temp files)

--- a/catbox
+++ b/catbox
@@ -30,6 +30,7 @@
   echo "Commands:"
   echo "   user [userhash]            - Gets or sets current userhash. If you pass 'off' then it will make you anonymous"
   echo "   file <filename(s)>         - Uploads files to catbox.moe"
+  echo "   temp <filename(s)>         - Uploads files to litterbox.catbox.moe"
   echo "   url <url(s)>               - Uploads files from URLs to catbox.moe"
   echo "   delete <filenames(s)>      - Deletes files from catbox.moe. Requires userhash"
   echo "   album                      - Album Managment"
@@ -59,6 +60,7 @@
 }
 
 HOST="https://catbox.moe/user/api.php"
+LITTERHOST="https://litterbox.catbox.moe/resources/internals/api.php"
 
 if [ $# -eq 0 ]
 then usage
@@ -137,6 +139,31 @@
       fi
     done
   fi
+elif [ $1 == "temp" ] 
+then
+  # Litterbox upload
+
+  if [ $# -lt 3 ]
+  then
+      echo "Usage: catbox temp <filename> [<filename>...] [1h/12h/24h/72h] - Uploads files to LitterBox.CatBox.moe"
+      exit 1;
+  fi
+  one=0
+  for file in "${@:1:$#-1}"
+  do
+    if [ $one -ne 1 ]; then one=1; continue; fi
+    if [ -f "$file" ] || [ -L "$file" ]
+    then
+      name=$(basename -- "$file")
+      echo -en "\e[1m$name\e[0m:\n"
+      link=`curl -F "reqtype=fileupload" -F "time=${*: -1:1}" -F "fileToUpload=@$file" $LITTERHOST`
+      echo -en "\n"
+      echo -en "Uploaded to: \e[1m$link\n"
+      echo -n $link|xclip -selection clipboard
+    else
+      echo -e "\e[91mFile $file dose not exists!\e[0m"
+    fi
+  done
 elif [ $1 == "url" ]
 then
   # Url Command

@MineBartekSA
Copy link
Author

@VMpc Thank you very much for your suggestion!
I've added the Litterbox support with a bit of modification.
The expiry time is now truly optional and defaults to 1h.

Also, if you want only litterbox, I suggest you look at my litterbox gist.

@MineBartekSA
Copy link
Author

Hello Catbox users!

I've come to you bearing good news!
Catbox v2.0 is now out!

Catbox v2.0

This version comes with many changes and additions, mainly because catbox was rewritten from scratch.
After the recent temp command addition, I thought that catbox code was ancient, clunky, and just dog shit.
After 4 and a half years it was time for some changes, so I sat down and rewrote catbox.

My two main goals for this rewrite were:

  • Remove as much duplication as possible
  • Make the code more maintainable

I'd say that I managed to achieve my main goals pretty well.
As for my other goals, I wanted to make the script more script friendly, more uniform, and generally better.

A quick list of these changes (from biggest to smallest):

  • Added global options (-s, -S, -n, -u, -V)
  • Added support for - (stdin) while uploading files (you no longer need to pass /dev/stdin)
  • Better error handling and reporting (all errors are now sent to stderr)
  • Made output more uniform
  • Reworded, fixed, and added more output text

This would be the end of the quick update, you should be able to grasp everything that has changed from the script itself.
But if you want to read more about these changes, feel free to keep reading my ramblings.

Global options

With v2 I've added 5 global options. (not counting -h/--help/--usage or -v/--version)
Global options can be passed in any part of the command.
You can put them in the beginning, end, or even in the middle of the command.
For example, catbox file file1 -s file2 will only upload files file1 and file2, while being in silent mode showing only links.

These options are more or less self-explanatory, but I'd like to go into a bit more detail about them here:

  • --silent - Silent mode

    The coveted mode!
    Most forks change catbox to be more silent, so I heard you and added a global option to suppress most output to stdout.
    Only important output will be sent to stdout, so only links, version, and verbose output will show up.
    Also, please note that this mode does not suppress any stderr output, so when using the file command, cURL's transfer info will still show up.

  • --silent-all - Silent all mode

    Pretty much the same as --silent with the exception that it will also suppress stderr output.
    This output mode should be the best for scripts and CI usage.

  • --no-color - Disable colouring

    This one is pretty much self-explanatory.
    Any output will no longer be coloured.
    Useful when using a terminal or CI output logs with no colour support.

  • --user-hash= - Pass user hash

    You can now pass the user hash in the command itself with no need to set it globally.
    If you have a user hash set globally, this option will disable reading that file.
    When using -u or --user-hash, the next argument will be interpreted as the user hash.
    You can also pass it as a single argument using --user-hash=<your user hash>.
    It is also possible to use this option to be anonymous despite the global user hash file by passing -u "" or --user-hash=.
    Also pretty useful in scripts and CI.

  • --verbose - Verbose output

    This is the least useful option.
    When using most of the album commands, more output will be shown.
    That's it.

Errors

In this section, I'd like to talk about two major changes:

  • Errors now are properly sent to stderr
  • The script now consistently outputs three exit codes (not counting 0)

First, let's go over the exit codes:

  • exit code 1

    Catbox will emit the exit code of 1 if the command lacks arguments.
    For example, just running catbox will show you the help message, but also will emit 1 since the command was not fully written.
    On the other hand, running catbox help will show you the same message but will emit 0 since help is a valid catbox command.
    Any usage message will emit an exit code of 1.
    This will might help you in debugging when using catbox in a script or CI.

  • exit code 2

    Catbox will emit the exit code of 2 when the command fully failed.
    For example, when running catbox file nonexistant, catbox will emit 2, since no file was properly uploaded.
    On the other hand, when running catbox file nonexistent existing-file, catbox will emit 0 since it did not fully fail.

  • exit code 3

    Catbox will only emit the exit code of 3 when the only dependency is missing.
    This is useful in an event when you are using catbox in a script or CI.
    You might not be the owner of these machines, so this exit code will tell you that you are missing cURL.

Now, let's talk about the second thing.

I've taken my time to make sure that any error message, produced by me, cURL, or the API, will be properly sent to stderr.
Any red text you might see on your terminal when using catbox was sent to stderr.

Another thing to note is, all usage messages that are sent out alongside the 1 exit code, are sent out to stderr

Uniform output?

Maybe calling catbox output uniform is a bit of a stretch, but I'd say it's more uniform than it was.

There are three output formats depending on how many and the type of requests the script is making to catbox.moe.

  • Uploading format

    This output format is used by the file and temp commands.
    It's pretty much identical to how it looked in previous versions.
    I opted to not make any changes since the default cURL transfer status perfectly shows what you might want to see.
    I did make some small changes to it, like removing the empty line, adding API error reporting, and small text changes.

  • Multiple request format

    This output format is used by the url, delete, and album delete commands.
    This format now shows you a Please wait... message with a spinner to indicate that the script is waiting for a response.
    API errors are not properly colour-coded and are sent to stderr.

  • Single request format

    This output format is used by most album commands.
    It differs the most from the previous versions.
    Without the --verbose options, I opted to have the least amount of information presented.

The only outlier is the user command since it does not conform to any of the formats above.
It does not make any requests to catbox.moe, so it has its own simple output format.
This version of the user command better presents you your user hash and foregoes repeating it when setting it.
You can also see if you are using a user hash passed with the --user-hash option or not.

Technical changes

In this section, I'd like to point out a few technical things and the new script structure.

Catbox v2.0 has a completely altered internal structure.
You can segment it into the following segments:

  • Constants
  • Util and command functions
  • Start and global option handler
  • Read user hash file
  • Command handler

If someone wonders how catbox handles global options, here is a quick explanation.
Catbox goes through all arguments and tries to match them to one of the options.
When an argument matches, a proper action is executed and then the argument is deleted.

The command handler, or the big case statement at the end of the script, handles the interpretation of catbox commands.
For every command, it should only contain the usage message, simple data processing, and a call to a matching function.
Simple commands, like user, can be fully implemented in the command handler, but more complex commands must use a function.
Command handler should also check for prerequisites, like user hash, and inform the user about them.
These constraints should always be followed to maintain ease of maintenance and better readability.


If you read through all of this, you have my utmost thanks!
For anyone else, thank you for looking at my little script and I hope it will be useful to you!

@alsoGAMER
Copy link

thank you @MineBartekSA for all the work u're putting into this project - I'll update the version on the AUR soon!

@ostrich
Copy link

ostrich commented Mar 18, 2024

Thanks for this script, @MineBartekSA. If interested, here's a patch to support wl-copy if $WAYLAND_DISPLAY is set:

--- a/catbox
+++ b/catbox
@@ -99,7 +99,13 @@
             fail+=1
             continue
         fi
-        echo -n $link | xclip -selection clipboard
+        
+        if [ -n "$WAYLAND_DISPLAY" ]; then
+            echo -n $link | wl-copy
+        else
+            echo -n $link | xclip -selection clipboard
+        fi
+        
         echo -en "Uploaded to: "$BOLD
         echo $link >&5
         echo -en $RESET

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment