packages feed

urbit-api (empty) → 0.2.0.0

raw patch · 4 files changed

+360/−0 lines, 4 filesdep +aesondep +basedep +bytestring

Dependencies added: aeson, base, bytestring, conduit, conduit-extra, http-client, modern-uri, req, req-conduit, text, uuid

Files

+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2015 Urbit++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.
+ README.md view
@@ -0,0 +1,81 @@+# Haskell Urbit API++[![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)+[![Hackage](https://img.shields.io/hackage/v/urbit-api.svg?style=flat)](https://hackage.haskell.org/package/urbit-api)+[![builds.sr.ht status](https://builds.sr.ht/~ben/urbit-api.svg)](https://builds.sr.ht/~ben/urbit-api?)++++This library helps you talk to your Urbit from Haskell, via HTTP.++The Urbit API is a command-query API that lets you hook into apps 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-api` handles most of the path, session, and HTTP request stuff+automatically. See the+[haddocks](https://hackage.haskell.org/package/urbit-api/docs/Urbit-API.html)+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.++## Example usage++```haskell+import qualified Data.Aeson as Aeson+import Data.Aeson ((.=))+import qualified Data.Text as Text+import qualified Data.UUID.V4 as UUID++import Urbit.API++main :: IO ()+main = do+  let fakezod = Ship+    { uid = "0123456789abcdef",+      name = "zod",+      lastEventId = 1,+      url = "http://localhost:8081",+      code = "lidlut-tabwed-pillex-ridrup"+    }++  -- Establish connection+  sess <- connect ship++  -- Send a message by poking the chat-hook+  uuid <- UUID.nextRandom+  poke sess ship "zod" "chat-hook" "json" $+    Aeson.object+      [ "message"+          .= Aeson.object+            [ "path" .= Text.pack "/~/~zod/mc",+              "envelope"+                .= Aeson.object+                  [ "uid" .= UUID.toText uuid,+                    "number" .= (1 :: Int),+                    "author" .= Text.pack "~zod",+                    "when" .= (1602118786225 :: Int),+                    "letter" .= Aeson.object ["text" .= Text.pack "hello world from haskell!"]+                  ]+            ]+      ]+```++## 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
+ Urbit/API.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- |+-- Module: Urbit.API+-- Copyright: © 2020–present Ben Sima+-- License: MIT+--+-- Maintainer: Ben Sima <ben@bsima.me>+-- Stability: experimental+-- Portability: non-portableo+--+-- === About the Urbit API+--+-- The Urbit 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.API+  ( -- * Types+    Ship (..),+    Session,++    -- * Functions+    connect,+    poke,+    ack,+    subscribe,+  )+where++import Conduit (ConduitM, runConduitRes, (.|))+import qualified Conduit+import qualified Control.Exception as Exception+import Data.Aeson ((.=))+import qualified Data.Aeson as Aeson+import Data.ByteString (ByteString)+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Network.HTTP.Client as HTTP+import Network.HTTP.Req ((=:))+import qualified Network.HTTP.Req as Req+import qualified Network.HTTP.Req.Conduit as Req+import qualified Text.URI as URI++-- | Some information about your ship needed to establish connection.+data Ship = Ship+  { -- | A random string for your channel+    uid :: Text,+    -- | 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@+    url :: Text,+    -- | Login code, @+code@ in the dojo. Don't share this publically+    code :: Text+  }+  deriving (Show)++channelUrl :: Ship -> Text+channelUrl Ship {url, uid} = url <> "/~/channel/" <> uid++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.+connect :: Ship -> IO Session+connect ship =+  Req.useURI <$> (URI.mkURI $ url ship <> "/~/login") >>= \case+    Nothing -> error "could not parse ship url"+    Just uri ->+      Req.runReq Req.defaultHttpConfig $+        Req.responseCookieJar <$> either con con uri+  where+    body = "password" =: (code ship)+    con (url, opts) =+      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 ->+  -- | 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 ->+  -- | The actual JSON message, serialized via aeson+  a ->+  IO Req.BsResponse+poke sess ship shipName app mark json =+  Req.useURI <$> (URI.mkURI $ channelUrl ship) >>= \case+    Nothing -> error "could not parse ship url"+    Just uri ->+      Req.runReq Req.defaultHttpConfig $+        either con con uri+  where+    con (url, opts) =+      Req.req+        Req.POST+        url+        (Req.ReqBodyJson body)+        Req.bsResponse+        $ opts <> Req.cookieJar sess+    body =+      [ Aeson.object+          [ "id" .= nextEventId ship,+            "action" .= Text.pack "poke",+            "ship" .= shipName,+            "app" .= app,+            "mark" .= mark,+            "json" .= json+          ]+      ]++-- | Acknowledge receipt of a message. (This clears it from the ship's queue.)+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"+    Just uri ->+      Req.runReq Req.defaultHttpConfig $+        either con con uri+  where+    con (url, opts) =+      Req.req+        Req.POST+        url+        (Req.ReqBodyJson body)+        Req.bsResponse+        $ opts <> Req.cookieJar sess+    body =+      [ Aeson.object+          [ "action" .= Text.pack "ack",+            "event-id" .= eventId+          ]+      ]++instance Req.MonadHttp (ConduitM i o (Conduit.ResourceT IO)) where+  handleHttpException = Conduit.liftIO . Exception.throwIO++-- | Subscribe to ship events on some path.+subscribe ::+  -- | Session cookie from 'connect'+  Session ->+  -- | Your ship+  Ship ->+  -- | 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"@+  ConduitM ByteString Conduit.Void (Conduit.ResourceT IO) a ->+  IO a+subscribe sess ship path fn =+  Req.useURI <$> (URI.mkURI $ url ship <> "/" <> path) >>= \case+    Nothing -> error "could not parse ship url"+    Just uri -> runConduitRes $ do+      either con con uri $ \request manager ->+        Conduit.bracketP+          (HTTP.responseOpen request manager)+          HTTP.responseClose+          Req.responseBodySource+          .| fn+  where+    con (url, opts) =+      Req.req'+        Req.POST+        url+        Req.NoReqBody+        $ opts <> Req.cookieJar sess
+ urbit-api.cabal view
@@ -0,0 +1,50 @@+name:                urbit-api+version:             0.2.0.0+synopsis:            Talk to Urbit from Haskell+description:+    @urbit-api@ 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/haskell-urbit-api+license:             BSD3+license-file:        LICENSE+author:              Ben Sima+maintainer:          bsima@me.com+copyright:           2020 Ben Sima+category:            Web+build-type:          Simple+cabal-version:       >=1.10+extra-source-files:  README.md++library+  default-language:    Haskell2010+  exposed-modules:+    Urbit.API+  build-depends:+    base >= 4.7 && < 5,+    aeson,+    bytestring,+    conduit,+    conduit-extra,+    http-client,+    modern-uri,+    req,+    req-conduit,+    text,+    uuid++-- executable urlock+--   hs-source-dirs:      .+--   main-is:             Main.hs+--   default-language:    Haskell2010+--   build-depends:+--     base >= 4.7 && < 5,+--     urbit-api+--+-- Test-Suite test-urbit-api+--   type: exitcode-stdio-1.0+--   main-is: test.hs+--   build-depends:+--     base >= 4.7 && < 5,+--     urbit-api