diff --git a/README.md b/README.md index f8c0e59..ed5e798 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,25 @@ running on your Urbit. You can submit commands (called "pokes") and subscribe to responses. See the `test.hs` file for some example usages. + +## Design + +The Urbit vane `eyre` is responsible for defining the API interface. The path to +the API is `/~/channel/...`, where we send messages to the global log (called +`poke`s) which are then dispatched to the appropriate apps. To receive +responses, we stream messages from a path associated with the app, such as +`/mailbox/~/~zod/mc`. Internally, I believe Urbit calls these `wire`s. + +`urbit-airlock` handles most of the path, session, and HTTP request stuff +automatically. See the haddocks for more details. + +This library is built on req, conduit, and aeson, all of which are very stable +and usable libraries for working with HTTP requests and web data. + +## TODO + +- fix test suite on travis (OOM when trying to compile urbit) +- more sophisticated test cases, also use cabal test instead of homegrown thing +- add an exe that wraps the library with a cli +- port to ghcjs +- put some examples in the docs diff --git a/Urbit/Airlock.hs b/Urbit/Airlock.hs index b979eec..d7f2757 100644 --- a/Urbit/Airlock.hs +++ b/Urbit/Airlock.hs @@ -5,10 +5,44 @@ {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} +-- | +-- Module: Urbit.Airlock +-- Copyright: © 2020–present Ben Sima +-- License: MIT +-- +-- Maintainer: Ben Sima +-- Stability: experimental +-- Portability: non-portableo +-- +-- === About the Urbit API +-- +-- The "Urbit Airlock" API is a command-query API that lets you hook into apps +-- running on your Urbit. You can submit commands and subscribe to responses. +-- +-- The Urbit vane @eyre@ is responsible for defining the API interface. The HTTP +-- path to the API is @\/~\/channel\/...@, where we send messages to the global +-- log (called @poke@s) which are then dispatched to the appropriate apps. To +-- receive responses, we stream messages from a path associated with the app, +-- such as @\/mailbox\/~\/~zod\/mc@. Internally, I believe Urbit calls these +-- @wire@s. +-- +-- === About this library +-- +-- This library helps you talk to your Urbit from Haskell, via HTTP. It handles +-- most of the path, session, and HTTP request stuff automatically. You'll need +-- to know what app and mark (data type) to send to, which path/wire listen to, +-- and the shape of the message. The latter can be found in the Hoon source +-- code, called the @vase@ on the poke arm. +-- +-- This library is built on req, conduit, and aeson, all of which are very +-- stable and usable libraries for working with HTTP requests and web data. +-- Released under the MIT License, same as Urbit. module Urbit.Airlock - ( Ship (..), - App, - Mark, + ( -- * Types + Ship (..), + Session, + + -- * Functions connect, poke, ack, @@ -19,7 +53,6 @@ where import Conduit (ConduitM, runConduitRes, (.|)) import qualified Conduit import qualified Control.Exception as Exception -import Control.Lens () import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson import Data.ByteString (ByteString) @@ -33,16 +66,16 @@ import qualified Text.URI as URI -- | Some information about your ship needed to establish connection. data Ship = Ship - { -- | A random string for your channel. + { -- | A random string for your channel uid :: Text, - -- | The `@p` of your ship. - name :: ShipName, - -- | Track the latest event we saw (needed for poking). + -- | The @\@p@ of your ship + name :: Text, + -- | Track the latest event we saw (needed for poking) lastEventId :: Int, - -- | Network access point, with port if necessary. Like - -- 'https://sampel-palnet.arvo.network', or 'http://localhost:8080'. + -- | Network access point, with port if necessary, like + -- @https://sampel-palnet.arvo.network@, or @http://localhost:8080@ url :: Text, - -- | Login code, `+code` in the dojo. Don't share this publically. + -- | Login code, @+code@ in the dojo. Don't share this publically code :: Text } deriving (Show) @@ -50,18 +83,10 @@ data Ship = Ship channelUrl :: Ship -> Text channelUrl Ship {url, uid} = url <> "/~/channel/" <> uid -type App = Text - -type Path = Text - -type Mark = Text - --- | The `@p` for the ship (no leading ~). -type ShipName = Text - nextEventId :: Ship -> Int nextEventId Ship {lastEventId} = lastEventId + 1 +-- | A wrapper type for the session cookies. type Session = HTTP.CookieJar -- | Connect and login to the ship. @@ -75,22 +100,24 @@ connect ship = where body = "password" =: (code ship) con (url, opts) = - Req.req Req.POST url (Req.ReqBodyUrlEnc body) Req.ignoreResponse $ + Req.req Req.POST url (Req.ReqBodyUrlEnc body) Req.bsResponse $ opts -- | Poke a ship. poke :: Aeson.ToJSON a => + -- | Session cookie from 'connect' Session -> + -- | Your ship Ship -> - -- | To what ship will you send the poke? - ShipName -> - -- | Which gall application are you trying to poke? - App -> - -- | What mark should be applied to the data you are sending? - Mark -> + -- | Name of the ship to poke + Text -> + -- | Name of the gall application you want to poke + Text -> + -- | The mark of the message you are sending + Text -> a -> - IO Req.IgnoreResponse + IO Req.BsResponse poke sess ship shipName app mark json = Req.useURI <$> (URI.mkURI $ channelUrl ship) >>= \case Nothing -> error "could not parse ship url" @@ -103,7 +130,7 @@ poke sess ship shipName app mark json = Req.POST url (Req.ReqBodyJson body) - Req.ignoreResponse + Req.bsResponse $ opts <> Req.cookieJar sess body = [ Aeson.object @@ -117,7 +144,14 @@ poke sess ship shipName app mark json = ] -- | Acknowledge receipt of a message. (This clears it from the ship's queue.) -ack :: Session -> Ship -> Int -> IO Req.IgnoreResponse +ack :: + -- | Session cookie from 'connect' + Session -> + -- | Your ship + Ship -> + -- | The event number + Int -> + IO Req.BsResponse ack sess ship eventId = Req.useURI <$> (URI.mkURI $ channelUrl ship) >>= \case Nothing -> error "could not parse ship url" @@ -130,7 +164,7 @@ ack sess ship eventId = Req.POST url (Req.ReqBodyJson body) - Req.ignoreResponse + Req.bsResponse $ opts <> Req.cookieJar sess body = [ Aeson.object @@ -144,11 +178,14 @@ instance Req.MonadHttp (ConduitM i o (Conduit.ResourceT IO)) where -- | Subscribe to ship events on some path. subscribe :: + -- | Session cookie from 'connect' Session -> + -- | Your ship Ship -> - Path -> + -- | The path to subscribe to. + Text -> -- | A handler conduit to receive the response from the server, e.g. - -- 'Data.Conduit.Binary.sinkFile "my-file.out"'. + -- @Data.Conduit.Binary.sinkFile "my-file.out"@ ConduitM ByteString Conduit.Void (Conduit.ResourceT IO) a -> IO a subscribe sess ship path fn = diff --git a/shell.nix b/shell.nix index 05ec319..1f53a43 100644 --- a/shell.nix +++ b/shell.nix @@ -4,9 +4,12 @@ nixpkgs.mkShell { name = "urbit-airlock-shell"; buildInputs = [ nixpkgs.ormolu.bin + (nixpkgs.pkgs.haskell.packages.${compiler}.ghcWithPackages (hp: with hp; [ - aeson base bytestring conduit conduit-extra http-client lens - modern-uri req req-conduit text uuid wai wai-extra + + aeson base bytestring conduit conduit-extra http-client modern-uri + req req-conduit text uuid + ])) ]; } diff --git a/urbit-airlock.cabal b/urbit-airlock.cabal index 810fec5..9b7705d 100644 --- a/urbit-airlock.cabal +++ b/urbit-airlock.cabal @@ -1,7 +1,11 @@ name: urbit-airlock version: 0.1.0.0 --- synopsis: --- description: +synopsis: Talk to Urbit from Haskell +description: + @urbit-airlock@ is a Haskell library that helps you connect to the Urbit + API. + . + Built on req, conduit, and aeson for stability and simplicity. homepage: https://github.com/bsima/urbit-airlock license: BSD3 license-file: LICENSE @@ -24,14 +28,11 @@ library conduit, conduit-extra, http-client, - lens, modern-uri, req, req-conduit, text, - uuid, - wai, - wai-extra + uuid -- executable urlock -- hs-source-dirs: . diff --git a/urbit-airlock.nix b/urbit-airlock.nix index b1383c8..36fdf3d 100644 --- a/urbit-airlock.nix +++ b/urbit-airlock.nix @@ -1,15 +1,15 @@ { mkDerivation, aeson, base, bytestring, conduit, conduit-extra -, http-client, lens, modern-uri, req, req-conduit, stdenv, text -, uuid, wai, wai-extra +, http-client, modern-uri, req, req-conduit, stdenv, text, uuid }: mkDerivation { pname = "urbit-airlock"; version = "0.1.0.0"; src = ./.; libraryHaskellDepends = [ - aeson base bytestring conduit conduit-extra http-client lens - modern-uri req req-conduit text uuid wai wai-extra + aeson base bytestring conduit conduit-extra http-client modern-uri + req req-conduit text uuid ]; homepage = "https://github.com/bsima/urbit-airlock"; + description = "Talk to Urbit from Haskell"; license = stdenv.lib.licenses.bsd3; }