9Most people have lots of data stored in online services, many of
10which have APIs. API connectors allow the user to access this
11data from within their Urbit.
13A security driver allows the user to make authenticated requests
14to the service's API, but the user still needs to know the API
15and send and receive JSON. An API connector puts a layer of
16porcelain over this to allow the user easier control over their
19Each connector should perform two basic functions. First, it
20should expose a tree of the data in the service that's accessible
21through `.^` and FUSE. Second, it should expose event streams as
22actions occur in the service. Let's take a look at how the `%gh`
23app accomplishes both for Github.
25After starting `%gh` (`|start %gh`), let's look at the root of
26the tree that `%gh` exposes:
28 ~your-urbit:dojo> .^(arch %gy /=gh=)
29 [fil=~ dir={[p=~.issues q=~]}]
31`%gh` is currently a skeleton -- it contains examples of all the
32functionality necessary for a connector, but many endpoints
33aren't implemented. As we can see here, only issues are
34implemented. We can explore this tree:
36 ~your-urbit:dojo> .^(arch %gy /=gh=/issues)
37 [fil=~ dir={[p=~.by-repo q=~] [p=~.mine q=~]}]
42 .^(issue:gh %gx /=gh=/issues/by-repo/philipcmonktest/testing/7)
44 login.user:.^(issue:gh %gx /=gh=/issues/by-repo/philipcmonktest/testing/7)
47The other thing an API connector needs to do is expose event
48streams. One straightforward way to use this capacity is with
51 :pipe|connect %gh /listen/philipcmonktest/testing/issues/'issue_comment' %public
53Now creating an issue results in the following message in your
54`%public` `:talk` channel:
56 ~your-urbit[philipcmonktest@github]: opened issue #11: i found a bug!
58You'll also get notifications of issue status changes and
61As is often the case, the best way to write an API connector is
62to copy an existing one and modify it. We'll dissect the Github
63connector here. This code is in `/=home=/app/gh/hoon`, and it's
64also reproduced at the bottom of this page (in case the code
65drifts out of sync with this doc).
67We'll go over the two parts to an API connector (one-time reading vs listening
68for events) separately.
72A connector exposes a tree of data. Every read request has a
73`care`, which is generally either `%x` or `%y. `%x` is a request
74for a particular piece of data, while `%y` is a request for a
75directory listing. Roughly, `%x` means Unix `cat` and `%y` means
78> Sometimes you wish to expose a tree where a part of the path
79> can't be enumerated. For example, a `%y` of `/issues/by-repo`
80> "should" produce a list of all Github users, but we don't do
81> that because it's too long. Instead, we just produce our own
82> username (from the `web.plan` file). You can still access
83> repos from other users, you just don't see them in the
86The usual flow for implementing this tree of data makes heavy use
87of the `connector` library. This library is well documented in
88the source, so check out `/=home=/lib/connector/hoon`.
90The `connector` library needs to be initialized with definitions
91of `move` and `sub-result` (all the types of data that can be
92returned by a place). This can just be a line at the top of the
95 =+ connector=(connector move sub-result) :: Set up connector library
97Most of the Github-specific logic is in `++places`, which is a
98list of all the places we can request. A place consists of:
104 sigh-x/$-(jon/json (unit sub-result))
105 sigh-y/$-(jon/json (unit arch))
108- `guard`, the type of the paths we should match. For example,
109 to match `/issues/<user>/<repo>` use `{$issues @t @t $~}`.
111- `read-x`, called when someone tries to read the place with
112 `care` `%x`. Should produce a single move, usually either a
113 `%diff` response if we can immediately answer or a `%hiss`
114 http request if we need to make a request to the api. See the
115 `++read-*` functions in `++helpers` for some common handlers.
117- `read-y`, same as `read-x` except with care `%y`.
119- `sigh-x`, called when an http response comes back on this
120 place. You're given the json of the result, and you should
121 produce either a result or null. Null represents an error.
122 If you didn't create an http request in `read-x`, then this
123 should never be called. Use `++sigh-strange` from `++helpers`
124 to unconditionally signal an error.
126- `sigh-y`, same as `sigh-x` except with care `%y`. Note that a
127 `%y` request must produce an arch, unlike a `%x` request,
128 which may produce data of any mark.
130Filling out the list of places is a lot of grunt work, but most
131places are fairly straightforward, and the `++read-*` helper
132functions are useful. Check out the library source for more
135Besides the list of places, we just need to handle the flow of
136control. `++peek` and `++peer-scry` are the interface we expose
137to the rest of the system while `++sigh-httr` and `++sigh-tang`
138are used to handle responses when we make HTTP requests.
140- `++peek` should usually just produce `~`. If there are cases
141 where we can respond directly to a `.^` request without
142 blocking on anything, we could do it here, but it's generally
143 not worth the hassle since the same logic should be duplicated
146- `++peer-scry` is where the actual handling for a read request
147 goes. In general, we just need to call `++read` from the
148 `connector` library with the bone, list of places, care, and
149 path. This will match the path to the appropriate place and
150 run either `++read-x` or `++read-y`, depending on the care.
152- If `++read-x` or `++read-y` made a successful API request, then
153 the response will come back on `++sigh-httr`. Here, we just
154 need to parse out the wire and call `++sigh` from the
155 `connector` library with the list of places, care, path, and
156 HTTP result. This will match the path to the appropriate place
157 and run either `++sigh-x` or `++sigh-y`, depending on the care.
159- If `++read-x` or `++read-y` made an API request that failed,
160 then we'll get a stack trace in `++sigh-tang`. Here, we just
161 print it out and move on.
163That's really all there is to the reading portion of API
166One of the most accessbile ways to jump into Arvo programming is
167to just add more places to an existing API connector. It's
168useful, small in scope, and comes in bite-sized chunks since most
169places are less than ten lines of code.
173Listening for events is fairly service-specific. In some
174services, we poll for changes. The Twitter connector has an
175example of this, but note that it predates the `connector`
176library and is thus more complicated than it needs to be, and the
177interface it exposes isn't standard. In Github, we power our
178event streams with webhooks.
180For Github, when someone subscribes to
181`/listen/<user>/<repo>/<events...>`, we want to produce
182well-typed results when they occur.
184We only want to create one webhook per event, so in our state we
185have `hook`, which is a map of event names to the set of bones
186which are subscribed to that event. We must always keep this
189Flow of control starts in `++peer-listen`, where we just call
190`++listen`. In `++listen`, for each event in the list of events,
191we check to see if we have that hook set up (by checking whether
192it exists in `hook`). If so, we call `++update-hook` to add the
193current bone to the set of listeners. Otherwise, we call
194`++create-hook`, which sends a request to Github to set up the
195new webhook. We also create an entry in `hook` with the current
198When we created the webhook, we told Github to send the event to
199`/~/to/gh/gh-<event>.json?anon&wire=/`. This turns into a poke.
200Let's parse out this url. The first `gh` is the app name to
201poke. The next portion, `gh-<event>` is the mark to convert the
202JSON to. For example, when the "issues" event fires, it'll get
203converted to mark `gh-issues`. This mark has a conversion
204routine from `json` to the `issues` type, which is defined in the
205`gh` structure (`/=home=/sur/gh/hoon`). Once this conversion
206happens, the `gh` app will get poked with the correctly marked
209In `++poke`, we check `hook` to find the subscribers to the event
210that was fired, and we update them.
212That's really all there is to it. Webhook flow isn't well
213standardized, so even if your chosen service is powered by
214webhooks, your listening code might look rather different. The
215point is to expose the correct interface.
218## Github API Connector Code
220In case the code in `/=home=/app/gh/hoon` drifts out of sync with
224:: This is a connector for the Github API v3.
226:: You can interact with this in a few different ways:
228:: - .^({type} %gx /=gh={/endpoint}) to read data or
229:: .^(arch %gy /=gh={/endpoint}) to explore the possible
232:: - subscribe to /listen/{owner}/{repo}/{events...} for
233:: webhook-powered event notifications. For event list, see
234:: https://developer.github.com/webhooks/.
236:: This is written with the standard structure for api
237:: connectors, as described in lib/connector.hoon.
241/+ gh-parse, connector
245 ++ move (pair bone card)
247 $% {$diff sub-result}
248 {$them wire (unit hiss)}
249 {$hiss wire {$~ $~} $httr {$hiss hiss}}
252 :: Types of results we produce to subscribers.
257 {$gh-list-issues (list issue:gh)}
258 {$gh-issues issues:gh}
259 {$gh-issue-comment issue-comment:gh}
264 :: Types of webhooks we expect.
267 $% {$gh-issues issues:gh}
268 {$gh-issue-comment issue-comment:gh}
271=+ connector=(connector move sub-result) :: Set up connector library
274 hook/(map @t {id/@t listeners/(set bone)}) :: map events to listeners
276:: ++ prep _`. :: Clear state when code changes
282 ^- (list place:connector)
283 =+ (helpers:connector ost.hid wir "https://api.github.com")
284 => |% :: gh-specific helpers
285 ++ sigh-list-issues-x
287 %+ bind ((ar:jo issue:gh-parse) jon)
288 |= issues/(list issue:gh)
289 gh-list-issues+issues
291 ++ sigh-list-issues-y
293 %+ bind ((ar:jo issue:gh-parse) jon)
294 |= issues/(list issue:gh)
295 :- `(shax (jam issues))
296 (malt (turn issues |=(issue:gh [(rsh 3 2 (scot %ui number)) ~])))
301 read-y=(read-static %issues ~)
306 :* guard={$issues $~}
308 read-y=(read-static %mine %by-repo ~)
312 ^- place :: /issues/mine
313 :* guard={$issues $mine $~}
314 read-x=(read-get /issues)
315 read-y=(read-get /issues)
316 sigh-x=sigh-list-issues-x
317 sigh-y=sigh-list-issues-y
319 ^- place :: /issues/by-repo
320 :* guard={$issues $by-repo $~}
324 =+ /(scot %p our.hid)/home/(scot %da now.hid)/web/plan
325 =+ .^({* acc/(map knot plan-acct)} %cx -)
327 ((read-static usr:(~(got by acc) %github) ~) pax)
331 ^- place :: /issues/by-repo/<user>
332 :* guard={$issues $by-repo @t $~}
334 read-y=|=(pax/path (get /users/[-.+>.pax]/repos))
338 %+ bind ((ar:jo repository:gh-parse) jon)
339 |= repos/(list repository:gh)
340 [~ (malt (turn repos |=(repository:gh [name ~])))]
342 ^- place :: /issues/by-repo/<user>/<repo>
343 :* guard={$issues $by-repo @t @t $~}
344 read-x=|=(pax/path (get /repos/[-.+>.pax]/[-.+>+.pax]/issues))
345 read-y=|=(pax/path (get /repos/[-.+>.pax]/[-.+>+.pax]/issues))
346 sigh-x=sigh-list-issues-x
347 sigh-y=sigh-list-issues-y
349 ^- place :: /issues/by-repo/<user>/<repo>
350 :* guard={$issues $by-repo @t @t @t $~}
352 |=(pax/path (get /repos/[-.+>.pax]/[-.+>+.pax]/issues/[-.+>+>.pax]))
354 read-y=(read-static ~)
357 %+ bind (issue:gh-parse jon)
365:: When a peek on a path blocks, ford turns it into a peer on
366:: /scry/{care}/{path}. You can also just peer to this
369:: We hand control to ++scry.
373 ^- {(list move) _+>.$}
376 (read:connector ost.hid (places %read pax) i.pax t.pax)
378:: HTTP response. We make sure the response is good, then
379:: produce the result (as JSON) to whoever sent the request.
382 |= {way/wire res/httr}
383 ^- {(list move) _+>.$}
384 ?. ?=({$read care @ *} way)
392 (sigh:connector (places ren style pax) ren pax res)
394:: HTTP error. We just print it out, though maybe we should
395:: also produce a result so that the request doesn't hang?
398 |= {way/wire tan/tang}
399 ^- {(list move) _+>.$}
400 %- (slog >%gh-sigh-tang< tan)
401 [[ost.hid %diff null+~]~ +>.$]
403:: We can't actually give the response to pretty much anything
404:: without blocking, so we just block unconditionally.
407 |= {ren/@tas tyl/path}
408 ^- (unit (unit (pair mark *)))
411::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
412:: Webhook-powered event streams (/listen) ::
413::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
415:: To listen to a webhook-powered stream of events, subscribe
416:: to /listen/<user>/<repo>/<events...>
418:: We hand control to ++listen.
422 ^- {(list move) _+>.$}
424 ~& [%bad-listen-path pax]
428:: This core handles event subscription requests by starting or
429:: updating the webhook flow for each event.
436 ++ abet :: Resolve core.
437 ^- {(list move) _+>.$}
440 ++ send-hiss :: Send a hiss
443 =+ wir=`wire`[%x %listen pax]
444 +>.$(mow [[ost.hid %hiss wir `~ %httr [%hiss hiz]] mow])
446 :: Create or update a webhook to listen for a set of events.
450 =+ pax=pax :: TMI-proofing
456 ?: (~(has by hook) i.events)
457 $(+>+ (update-hook i.events), events t.events)
458 $(+>+ (create-hook i.events), events t.events)
466 =+ clean-event=`tape`(turn (trip event) |=(a/@tD ?:(=('_' a) '-' a)))
468 %+ ~(put by hook) (crip clean-event)
470 (~(get by hook) (crip clean-event))
471 *{id/@t listeners/(set bone)}
472 [id (~(put in listeners) ost.hid)]
475 =+ [(trip i.pax) (trip i.t.pax)]
476 "https://api.github.com/repos/{-<}/{->}/hooks"
479 %- taco %- crip %- pojo %- jobe :~
482 events+a+~[s+event] ::(turn `(list ,@t)`t.t.pax |=(a=@t s/a))
486 "http://107.170.195.5:8443/~/to/gh/gh-{-}.json?anon&wire=/"
488 [%'content_type' s+%json]
493 :: Add current bone to the list of subscribers for this event.
498 =+ hok=(~(got by hook) event)
501 %+ ~(put by hook) event
502 hok(listeners (~(put in listeners.hok) ost.hid))
506:: Pokes that aren't caught in more specific arms are handled
507:: here. These should be only from webhooks firing, so if we
508:: get any mark that we shouldn't get from a webhook, we reject
509:: it. Otherwise, we spam out the event to everyone who's
510:: listening for that event.
513 |= response/hook-response
514 ^- {(list move) _+>.$}
515 =+ hook-data=(~(get by hook) (rsh 3 3 -.response))
517 ~& [%strange-hook hook response]
519 :: ~& response=response
521 %+ turn (~(tap in listeners.u.hook-data))
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list