While you're there get yourself a phone number
Make sure you have your home desk mounted, as per the setup instructions.
git clone
this repo.
It's not mandatory, but you might consider shutting your urbit down before copying all the files.
In the root directory for the repo run this:
cp -r src/* /urbit/path/your-urbit/home/
On your ship, run this:
|init-auth-basic
The URL is api.twilio.com
The username is the Account SID you can find in the account console dashboard
The password is the Account Token.
In the dojo, run this:
+https://api.twilio.com/2010-04-01/Accounts.json
Make sure this works and doesn't give you an error.
Now start the app:
|start %sms
Configure the app for your Twilio account. Of course you should use your Account SID and your Twilio phone number:
:sms|set-acct 'AC2222222'
:sms|set-number '+13145557766'
Please note the single quotes. Also, Twilio specifies that exact format for numbers, with the plus and no dashes.
Assuming everything went well, you should now be able to send a text, like so:
:sms|send '+13145554242' 'Here is your text, buddy'
Okay, now that you're sending texts, let's set us up for receiving texts.
https://www.twilio.com/console/phone-numbers/incoming
- If you try to send a text and get this:
[! gall: %sms: sigh: no /tang/request [%ap-lame %sms %sigh]]
This means you need to setup your authentication.
|init-auth-basic
- If you get a message about "Your AccountSid or AuthToken was incorrect.", then apparently you ran
|init-auth-basic
with a bad password. Good news is you can run it again.
- A quick note before talking about how I got this thing up and running. If you've read The Little Schemer, the Sixth Commandment states: "Simplfy only after the function is correct". If you're new to this, my suggestion is: go slowly. Start with something that already works, some bit of sample code, maybe. Change something small, and make sure it works. Refactor, make sure you didn't break anything. Then change something else.
- The docs are good (and improving), but less thorough than one might hope. If you're reading through source and you're wondering what
epur
is, head over to google and search forepur site:urbit.org/docs
. You can find uses in the reference docs and see if it's used in some example code. - This can work for glyphs as well, especially if you know the six-letter encoded version of it. So you're reading some source and you come across
:+
ask Mr. Google forcollus site:urbit.org/docs
, and it'll take you right to it. - Otherwise, you can put the glyph itself in quotes:
":+" site:urbit.org/docs
, which you might have to do on occasion, since sometimes you'll see two characters that aren't a "proper" glyph. - For digging through the actual code, ask grep to find it for you. If you want to know how
epur
is used in the wild,cd
into your home directory and rungrep -r epur *
. - If you just want to go straight to the definition of something,
grep
for "++ word". (Please note, this is two spaces). - Also, save yourself a lot of time before you get started by learning the
following:
- There are two kinds of "strings". There's a
cord
, which is a string constant, and there's atape
, which is a string as a list of characters. Tapes can do string interpolation and parsing. Cords can't. - If you want to make a cord, use
soq
s, i.e. single quotes. If you want a tape, usedoq
s, i.e. double quotes. - I'd say over 50% of my
nest-fail
errors stemmed from not having figured this out sooner.
- There are two kinds of "strings". There's a
- Although before you get started on something like this, you should really read through the arvo tutorials and make sure you have the troubleshooting page handy.
- There's surprisingly little code here, which you're welcome to peruse. If you'd like to learn about the process for getting something working, some of my stumbling blocks, and lessons learned, read on!
- I spent some time familiarizing myself with API by sending some POSTs via curl and Postman and referring to the docs about sending messages.
- For the app, first thing we'll need is a security driver. Read through Twilio documentation about their REST API to figure out what they're using.
- They're using HTTP Basic auth. So I copied sec/com/github.hoon to sec/com/twilio.hoon.
- There's really only one thing that needs to change in this file, which is the test URL. I dug through the documentation to figure out something generic that would work if you're authenticated and fail otherwise. In this case
+https://api.twilio.com/2010-04-01/Accounts
- I tested out sending POSTs to the Twilio REST endpoint from dojo. This did not work particularly well. I tried JSON. Status 400. I tried sending as a URL-encoded string. Status 400.
- Since it was working in Postman, I looked at what that was sending in the headers. Turns out I needed to set the content-type to "application/x-www-form-urlencoded".
- There didn't seem to be any obvious way of doing that from dojo. Inspecting the code, it looked like it was expecting JSON, which Twilio wasn't interested in. Since I was going to have to move this functionality into an app at some point, I switched over to developing that.
- I copied out the
up.hoon
app from the HTTP request docs. I used that to try to send the request to the account info page. - Problem: I couldn't figure out how to make an authenticated request.
I ended up asking on
:talk
and got pointed in the right direction. The%hiss
call needs a(unit identity)
. My copy/pasted code forup.hoon
was using~
, which eyre interprets as "no authentication. The person on talk (sorry, I can't credit) noted that the gmail app was using\
~]`. It seems that eyre intreprets that as "go lookup auth for me".(
tecsic), which is shorthand for
[ - I tried using
\
~and got a
nest-fail, which means I needed to update the signature (is this the "moss"?) for
++cardin the core, since
up.hoonhad written "no authentication" into the
++card` definition. - Once I updated that, everything worked fine and I got the authenticated request sent. Then I ran around the room giving random people high-fives.
- Now it was time to figure out how to actually send a post. There wasn't anything specific in the docs about sending a post, which meant digging through source, puzzling through what was going on, and making something happen.
- The GET command was sending a
$purl
, but this didn't have any space for the actual body, and the header part where I specify that the contents are x-www-form-urlencoded. - I grepped around the source to figure out which apps were doing a
%post
. I discovered that gh.hoon, gmail.hoon, et al. were doing a post. I also discovered that most of the code doing a POST was using JSON, so for at least some of this, I'd have to roll my own solution. - Apparently gh.hoon and gmail.hoon were using the mark
%hiss
. So I had to figure out how a%hiss
works. - Ultimately, I ended up looking at
arvo/zuse.hoon
. All the code inarvo
is very clean and streamlined and also somewhat opaque. It's easy to read once you get your head around it, but if you're looking for something to copy/paste to get started, it's not the first place to look. - Anyway, in
zuse.hoon
I discover that the moss for hiss is a purl and a moth. (A moth?) Amoth
is{p/meth q/math r/(unit octs)}
.meth
is the method, in this case%post
. (Being from Oklahoma, when I readmeth
, I tend to think of something else...)math
is "semiparsed headers". And the body apparently gets shunted into a(unit octs)
. - The good news here is that I could now grep for
math
and see where that's being used. This sent me back to app/gmail.hoon, where it constructs amoth
. inpoke-gmail-req
. So I copied that over to text.hoon, and set about getting it to work. - Again the copy-pasted moss for up.hoon was assuming we'd send a GET request, which only needs a
$purl
.nest-fail
. Once I changed the mark for the cage to$hiss
, things started working just fine. The post went through, my phone received a text, and again I ran around the room high-fiving strangers and kissing babies. - (Alternately, I could have increased the genericity of the moss, so it's just expecting a
$cage
, but decided to keep things specific and let the compiler know if I've screwed up anything. This is the eternal tradeoff of functional programming, between keeping things defined specifically and working through spurious failures or making things generic and dealing with types you weren't expecting. Usually best to err toward being more specific.) - Now it was time for some serious refactoring, since the app still had all the
%on
and%off
logic. Then I decided to make the message something you passed in. At this point, I had anest-fail
, and fixed this by changing the type passed in to @. (Note: I actually changed up a couple of things, which made debugging thenest-fail
tricky. Hence my note above about going slow.) - I had been assigning a few variables, which worked fine, but didn't seem to be "idiomatic" hoon, so I refactored those out to call functions inline. I
used the rad
%-
syntax and even a%+
. Like a boss. - I created a mark for an SMS message, which was mar/sms.hoon. Pretty straightforward.
- Then I created a generator to send a message from dojo without having to do any nonsense with the
&atom
or even&sms
. - Note on gen: make sure your parameter list ends in
$~
, otherwise you'll get somenest-fail
for your debugging enjoyment. - In retrospect, I should have done the gen and mark thing way sooner, since generally this makes executing the app easier.
- Twilio forces you to use the account number as part of the SMS sending URL. Since I don't want to commit my Twilio account info to github, I added that as a part of the SMS mark. Then, in another "should have done this sooner", I added the account and the "from" phone number to the generator, since they're really unlikely to change for me (or anyone else using this for that matter). So now I only have to provide the "to" number and the message.
- And at this point, it's pretty much usable as-is, other than the bit of editing you'd need to do on the generator.
- Time to publish to github, cleanup this doc, and let people know what's up.
-
Creating an API connector was a bit tough getting starting. The docs for Twilio are organized topically, which is good if you're oriented towards a task, like, "I want to receive a call from my web service, how do I do that?" If you're more wondering, "What all data does Twilio expose that I might find useful?"
-
As it happens, I already had this. The "Account Info" page, i.e.
https://api.twilio.com/2010-04-01/Accounts.json
, lists "subresource_uris". As it happens, I had never really looked at the contents. I just saw, "Oh, hey! I'm logged in now!" and stopped reading. -
As a first pass, I tried getting the list of Messages working i.e.
https://api.twilio.com/2010-04-01/Accounts/{account-id}/Messages.json
. This is the same URL as we use to send a message, only with a GET. This would be a quick-and-somewhat-dirty way of listening for responses. Webhooks would certainly be more responsive, but this can get us up and running. -
(NOTE: running this get from the dojo with +https://... was a poor choice. I was worried I'd crashed my ship for a moment.)
-
Actually, the first thing I need to do is build up the API tree, sarting with
/
directory. -
I added a
++place
for accounts, which is the next level up. It occurs to me that we don't actually need the++place
s to mirror the Twilio REST structure, but I suspect this will make life easier. -
I got a
peek slam fail
for accounts. I googled the docs and skimmed theford
reference. We haveslam
,slap
,maul
,maim
. This Urbit is a very violent system. -
Okay, quick rewind here. I should first make sure the
gh
app is working. Let me connect to that... -
Well, that's something. Getting the same
peek slam fail
forgh
. -
Spun my tires for a while trying to get the
gh
app running, or at least figure out why it's getting the oddpeek slam fail
. There are many moving parts to this app, most of which I don't understand. And looking at the repo for arvo, it would seem there's a new version coming soon. Thus, time to pivot... -
As a revised first pass, I've decided to add some kind of polling system to the sms app that's already written. Thus I'll read messages from
Messages.json
and write them toclay
, if we've had any changes. -
(Note to self: Make sure this exact thing hasn't already been done. Seems like mirroring a webpage would be a fairly common use-case)
-
In preparation for this, I refactored the app. I made a pair of marks for the account and a phone number, and a matching pair of generators to set these within the app, and updated the app to make these part of its state.
-
Updated the setup above so that you run the generators to set your account and your "from" phone number.
-
I like this solution much better than editing the generator. Instead of editing files, the only native Unix you have to do is copy files. After that, it all happens on your ship.
-
(Note to self: There's actually a way to do this entirely natively, not even copying files. Something to do with making a desk and letting people sync from it. I'd still want to have my files mirrored on github, on the off chance someone fixes something and wants to do a pull-request.)
-
But before doing that, I should probably figure out how to parse JSON. I'm already getting JSON back in the body for the reply to the POST, which has the text of the text message. If I extracted the message text from that, I wouldn't have to be passing the message around with the state. So that's my next step.
-
I was struggling trying to figure out how to get the meaty parts of the
(unit octs)
out so I can turn it into a cord so I can run it throughpoja
and parse it.grep -r
comes to my aid and notes thatlib/oauth1.hoon
was doing exactly that. It's a hellacious bit of code, and rather than blindly copying and pasting, I thought I'd make sure I knew what it was doing.
(rash q.u.bod (more pam (plus ;~(less pam prn))))
-
You can follow along here, if you'd like. It's a thrill ride.
-
Ultimately the thing I needed was the
q
out of the body, which is a(need octs)
, which can be done by asking forq:(need body)
. Hand that topoja
and you have your very own++json
. -
But... what is this
++json
anyway? Let's look at the definition inzuse.hoon
:
++ json :: normal json value $@ $~ :: null
$% {$a p/(list json)} :: array
{$b p/?} :: boolean
{$o p/(map @t json)} :: object
{$n p/@ta} :: number
{$s p/@t} :: string
== ::
-
It's either a null or a
book
. Abook
is a union type, which you can read about by searching for ["buccen"](KM direct link to a google page). I recall a book was used in the HTTP request doc. Anyway, the thing we want is an object, but it's not obvious how I dig out thep
, and then how I cast the result into a map. -
After lots and lots of toying around in futility, I decide there's about a 99% chance someone has already done this. So I brute-forced a solution, by grepping for every instance of
json
and looking at what that's doing. This leades me to themar/gh/issues.hoon
. They are extracting the object usingom:jo
. So I can get a map version of the body like so:
=+ bod=(need ((om:jo some) jon))
- At which point, getting the
body
field (i.e. the actual message) is as easy as doing this:
=+ bod=(~(got by obj) 'body')
(I like how I say "easy" like this wasn't hours and hours of work to arrive at.)
-
I should really be using
~(get by obj)
and checking to make sure we don't get null back, but this works for now. -
However, when I try to take the
message
out of the state, it didn't like that. Fortunately, it gave me a message aboutprep
. I had this line at the bottom which I had at some point commented-out. So I had to uncomment this out.
:: ++ prep _`. :: computed when the source file changes;
-
Well, as fun as all that was, it would seem I'll need something more heavy-duty if I'm going to be reading and writing a lot from the twilio API. As such, I made a
sur
(which I believe is called asurface
) for Twilio. This is a file containing a core containing a mold for the data we get back from the Twilio API. -
Side note: if you want to use the surface, you need to import it with a
/-
.
/- twilio
-
Wait! I just realized something. All this time I've been thinking of
mold
as something that grows on stale bread. (This is probably because of the use of the word "moldy") However, what it probably refers to is "mold" like "here's a mold for a plastic dinosaur". Like "injection mold". Man, this makes way more sense. -
And for this, actually Postman is a better guide than the docs. The actual API's JSON uses snake_case, whereas the docs would lead me to believe they were using some variety of CamelCase.
-
KM: Something about how
ot
works, and guesswork on what we should be passing for the++fist
-
So what's new with Hoon? Let's try loading the app and seeing what happens.
-
Addresses now seem to require a cab. So instead of
+>
, we need_+>
. I should look up why the change, but not right now. -
Also, apparently there are
buc
s I need for things that didn't need them (for some reason). -
And
poja
might be missing... Dang, a whole lot of things missing! Fortunately, we have lib/oldzuse.hoon which shows some mappings.
- Right now we can make a text message. What happens if someone writes back? Well, it goes in a black hole. To actually get replies and maybe do something with them, we'd need an API connector with a webhook setup.
- Once that's setup, we can read the account number and the phone number from within the account info, and won't need to pass that into the app directly (necessarily). Although asking people to hack one file isn't exactly the Spanish Inquisition, but still.