urbit-api

Access your urbit ship the ruby way. It's a Martian gem.


Keywords
ruby, urbit, urbit-api
License
MIT
Install
gem install urbit-api -v 0.5.0

Documentation

Urbit::Api

The Ruby interface to the Urbit HTTP API

This library wraps the Urbit ship http interface exposing it as a Ruby gem.

awesome urbit badge Gem Version License

Installation

Add this line to your application's Gemfile:

gem 'urbit-api'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install urbit-api

Usage

> bin/console

# This will instantiate a ship that connects to the fake `~zod` dev server by default
# See Urbit docs for more info: https://urbit.org/using/develop/
[1] pry(main)> ship = Urbit.new
=> #<Urbit::Ship:0x00007fa74b87f920 ...

OR... with config file...
> ship = Urbit.connect(config_file: '_config-barsyr-latreb.yml')

> ship.logged_in?
=> false

> ship.login
=> #<Urbit::Ship:0x00007fa74b87f920 ...

> ship.logged_in?
=> true

> ship.to_s
=> "a Ship(name: '~barsyr-latreb', host: 'http://localhost', port: '8080')"

> channel = ship.subscribe(app: 'graph-store', path: '/updates')
=> a Channel (Open) on ~barsyr-latreb(name: 'Channel-0', key: '1622836437b540b4')

# Subscribing works by opening a Channel. Your ships has a collection of all it's open Channels.
> channel = ship.open_channels.first
=> a Channel (Open) on ~barsyr-latreb(name: 'Channel-0', key: '1622836437b540b4')

# Notice that it's the same one.

# Every Channel has a unique key to identify it.
> channel.key
=> "16142890875c348d"

# The Channel has a Receiver that will now be listening on the app and path you specified. Each time an event is sent in it will be stored in the receiver's facts collection.
> channel.receiver.facts.count
=> 12

# Perform any action through landscape that would initiate an update into %graph-store...
# In this case I have added a comment to a local notebook.
> channel.receiver.facts.last
=> a Fact({:ship=>{:name=>"~barsyr-latreb", :host=>"http://localhost", :port=>"8080"}, :resource=>"~barsyr-latreb/test0-996", :acknowleged=>true, :is_graph_update=>true})

#  Your ship keeps a collection of all the messages sent to urbit:
> channel.sent_messages.collect {|m| m.to_s}
=> [
    "a Message({:action=>"poke", :app=>"hood", :id=>1, :json=>"Opening Airlock", :mark=>"helm-hi", :ship=>"barsyr-latreb"})",
    "a Message({:action=>"subscribe", :app=>"graph-store", :id=>2, :path=>"/updates", :ship=>"barsyr-latreb"})",
    "a Message({"id"=>3, "action"=>"ack", "event-id"=>"0"})",
    "a Message({"id"=>4, "action"=>"ack", "event-id"=>"1"})",
    "a Message({"id"=>5, "action"=>"ack", "event-id"=>"2"})"
   ]

#
# --------------------------------------------------------------------
# Poke
# --------------------------------------------------------------------
#
> ship.poke(app: 'hood', mark: 'helm-hi', message: 'Opening Airlock')
=> a Channel (Open) on ~barsyr-latreb(name: 'Channel-0', key: '1630355920a717e1')

#
# --------------------------------------------------------------------
# Scry
# --------------------------------------------------------------------
#
# Retrieving your ship's base hash using scry....
> ship.scry(app: 'file-server', path: '/clay/base/hash')
# => {:status=>200, :code=>"ok", :body=>"\"e75k5\""}

#
# --------------------------------------------------------------------
# Spider
# --------------------------------------------------------------------
#
# Creating a new Notebook in "My Channels" using %spider....
> create_json = %Q(
        {"create": {"resource": { "ship": "~zod", "name": "random_name"},
        "title": "Testing",
        "description": "Testing Un-Managed Graph Creation",
        "associated" : {"policy": {"invite": {"pending": []}}},
        "module": "publish", "mark": "graph-validator-publish"}}
  )
> ship.spider(mark_in: 'graph-view-action', mark_out: 'json', thread: 'graph-create', data: create_json)
# => {:status=>200, :code=>"ok", :body=>"\"e75k5\""}

#
# --------------------------------------------------------------------
# %graph-store
# --------------------------------------------------------------------
#
> puts ship.graph_names
~barsyr-latreb/dm-inbox
~darlur/announce
~bitbet-bolbel/urbit-community-5.963
~winter-paches/top-shelf-6391
~winter-paches/the-great-north-7.579
~barsyr-latreb/test0-996
~fabled-faster/test-chat-a-5919
~barsyr-latreb/test1-4287
~darrux-landes/welcome-to-urbit-community
~millyt-dorsen/finance-2.962
~fabled-faster/interface-testing-facility-683
~darlur/help-desk-4556
=>

# Reference a graph by name and return a single node.
> puts ship.graph(resource: '~winter-paches/top-shelf-6391').node(index: "170.141.184.505.207.751.870.046.689.877.378.990.080")
a Node({:index=>"170.141.184.505.207.751.870.046.689.877.378.990.080", :author=>"witfyl-ravped", :contents=>[{"text"=>"the patches don't really bother me though tbh"}], :time_sent=>1629316468195, :is_parent=>false, :child_count=>0})
=>

# You can also reference a graph by its index in the graphs collection.
> puts ship.graphs[3].node(index: "170.141.184.505.207.751.870.046.689.877.378.990.080")
a Node({:index=>"170.141.184.505.207.751.870.046.689.877.378.990.080", :author=>"witfyl-ravped", :contents=>[{"text"=>"the patches don't really bother me though tbh"}], :time_sent=>1629316468195, :is_parent=>false, :child_count=>0})
=>

# Return the contents of the 5 oldest nodes of a graph
> graph = ship.graph(resource: '~winter-paches/top-shelf-6391')
> graph.oldest_nodes(count: 5).sort.each {|n| p n.contents};nil
[{"text"=>"watching the 2020 stanley cup finals (tampa (sigh) just went up 2-0 in game 3) and i thought: \"the great north has to have a hockey chat, eh?\""}]
[{"text"=>"we'll see if this has legs. ;)"}]
[{"text"=>"shortie! now 2-1 tampa."}]
[{"text"=>"looks like tampa's going to go up 2-1. as a canadian this geographically depresses me. :/"}]
[{"text"=>"anyone in the stands?"}]
=>

# A single Node. In this case, the 3rd oldest node in the graph.
> puts graph.nodes[2].contents
{"text"=>"shortie! now 2-1 tampa."}
=>

# Getting the next newer Node. Remember that it always returns an Array, hence the '#first'.
> puts graph.nodes[2].next.first.contents
{"text"=>"looks like tampa's going to go up 2-1. as a canadian this geographically depresses me. :/"}
=>

# Return the indexes of the newest 5 nodes of a graph
> ship.graph(resource: '~winter-paches/top-shelf-6391').newest_nodes(count: 5).sort.each {|n| p n.index};nil
"170.141.184.505.209.257.330.601.508.862.548.770.816"
"170.141.184.505.209.375.247.350.471.711.992.578.048"
"170.141.184.505.209.545.972.004.310.065.795.301.376"
"170.141.184.505.209.627.337.970.761.265.544.429.568"
"170.141.184.505.209.644.102.846.398.558.514.446.336"
=>

# Fetching nodes older relative to another node. (See indexes above)
> puts (node = ship.graph(resource: '~winter-paches/top-shelf-6391').node(index: "170.141.184.505.209.644.102.846.398.558.514.446.336"))
a Node({:index=>"170.141.184.505.209.644.102.846.398.558.514.446.336", :author=>"winter-paches", :contents=>[{"text"=>"yep. that's how i did it as a kid. harry caray was the white sox announcer before he turned traitor and went to the cubs."}], :time_sent=>1629419046028, :is_parent=>false, :child_count=>0})
=>

> puts node.previous
a Node({:index=>"170.141.184.505.209.627.337.970.761.265.544.429.568", :author=>"pathus-hiddyn", :contents=>[{"text"=>"Lol oh man I haven’t listened to a baseball game on the radio in forever. It is great isn’t it. "}], :time_sent=>1629418137668, :is_parent=>false, :child_count=>0})
=>

> node.previous(count: 4).each {|n| p n.index};nil
"170.141.184.505.209.257.330.601.508.862.548.770.816"
"170.141.184.505.209.375.247.350.471.711.992.578.048"
"170.141.184.505.209.545.972.004.310.065.795.301.376"
"170.141.184.505.209.627.337.970.761.265.544.429.568"

#
# --------------------------------------------------------------------
# %group-store
# --------------------------------------------------------------------
#
# Show the paths of all your current Groups
> ship.groups.list
=> Hammock Coast
The Darlur System
The Great North

# Leave the highlighted Group above
> group = ship.groups[title: 'Hammock Coast']
=> a Group({:title=>"Hammock Coast", :description=>"Martians living in God's Country: Coastal South Carolina between Georgetown and Myrtle Beach", :host=>"~darlur", :key=>"hammock-coast", :member_count=>3, :pending_invites=>"?", :hidden=>false})

> group.leave
=> {:status=>200, :code=>"ok", :body=>"null"}

# The Group is no longer in your list of Groups
> ship.groups.list
=> The Darlur System
The Great North

# (Re-) Join the Group
> ship.groups.join(host: "~darlur", name: "hammock-coast")

# A group knows about it's Graphs
> g = ship.groups[title: 'Hammock Coast']
=> a Group({:title=>"Hammock Coast", :description=>"Martians living in God's Country: Coastal South Carolina between Georgetown and Myrtle Beach", :host=>"~darlur", :key=>"hammock-coast", :member_count=>3, :pending_invites=>"?", :hidden=>false})

> g.graphs.map {|g| g.name}
=> ["the-beach-2315"]

> g.graphs.map {|g| g.title}
=> ["The Beach"]

# The group is now back in your list of Groups (With large groups this may take a moment)
> ship.groups.list
=> Hammock Coast
The Darlur System
The Great North

# Create a new group
> ship.groups.create(name: 'group-4', title: 'Fourth Group', description: "4th Group")

> ship.groups.list
=> Fourth Group
Hammock Coast
The Darlur System
The Great North

# Send out an invitation to your new group
> group = ship.groups[path: '~barsyr-latreb/group-4']
=> a Group({:host=>"~barsyr-latreb", :key=>"group-4", :member_count=>0, :pending_invites=>"?", :hidden=>false})

> group.invite(ship_names: ['~winter-paches'], message: 'hello!')
=> {:status=>200, :code=>"ok", :body=>"null"}

# Archive your new group
> group.delete
=> {:status=>200, :code=>"ok", :body=>"null"}

#
# --------------------------------------------------------------------
# %settings-store
# --------------------------------------------------------------------
#
# Show all your ship's settings as a list of {desk: , bucket:}
> ship.settings.list
desk: landscape
  buckets: ["calm: 5 entries", "display: 2 entries", "urbit-visor-permissions: 1 entries"]
desk: bitcoin
  buckets: ["btc-wallet: 2 entries"]
=>

# Behind the scenes your ship has now done the following to retrieve the settings
# and we are now also listening for changes in any settings on the ship...
> ship.subscribe(app: 'settings-store', path: '/all')

# Settings for a single desk
> ship.settings[desk: 'landscape']
=> a Setting({:desk=>"landscape", :buckets=>#<Set: {#<Urbit::Bucket:0x00007fa6c99aa7a0 @name="calm", @entries={"hideUtilities"=>false, "hideGroups"=>false, "hideAvatars"=>true, "hideUnreads"=>false, "hideNicknames"=>true}>, #<Urbit::Bucket:0x00007fa6c99aa6b0 @name="display", @entries={"backgroundType"=>"color", "background"=>"0xa8.90ea"}>, #<Urbit::Bucket:0x00007fa6c99aa638 @name="urbit-visor-permissions", @entries={"https://urbitdashboard.com"=>["shipName", "shipURL", "scry", "subscribe", "poke", "thread"]}>}>})

> ship.settings[desk: 'landscape'].entries(bucket: 'calm')
=> {"hideUtilities"=>false, "hideGroups"=>true, "hideAvatars"=>true, "hideUnreads"=>false, "hideNicknames"=>true}

# Go to Landscape and toggle the "Hide Groups" button inside the Calm Engine settings page...
> ship.settings[desk: 'landscape'].entries(bucket: 'calm')
=> {"hideUtilities"=>false, "hideGroups"=>false, "hideAvatars"=>true, "hideUnreads"=>false, "hideNicknames"=>true}

# Alternatively you can directly access the bucket instance using this syntax:
)> ship.settings[desk: 'landscape'][bucket: 'calm']
=> a Bucket({:name=>"calm", :entries=>{"hideUtilities"=>false, "hideGroups"=>false, "hideAvatars"=>true, "hideUnreads"=>false, "hideNicknames"=>true}})

# Then you can also set them yourself if you like.
> ship.settings[desk: 'landscape'][bucket: 'calm'][key: 'hideGroups'] = true

# Your Group tiles will now disappear. And:
> ship.settings[desk: 'landscape'][bucket: 'calm']
=> a Bucket({:name=>"calm", :entries=>{"hideUtilities"=>false, "hideGroups"=>true, "hideavatars"=>true, "hideUnreads"=>false, "hideNicknames"=>true}})

# You can add a new Bucket to a Desk.
> ship.settings[desk: 'landscape'].add_bucket(name: 'mars-base-10', entries: {"current_pane" => 1, "current-view" => "graph-rover"})
=>

> ship.settings.list
desk: garden
  buckets: ["tiles: 1 entries"]
desk: landscape
  buckets: ["calm: 5 entries", "display: 2 entries", "urbit-visor-permissions: 1 entries", "mars-base-10: 2 entries"]
desk: bitcoin
  buckets: ["btc-wallet: 2 entries"]
=>

# And remove an entry...
> ship.settings[desk: 'landscape'][bucket: 'mars-base-10'].remove_entry(key: 'current_view')
=>

# And remove it entirely if you change your mind.
> ship.settings[desk: 'landscape'].remove_bucket(name: 'mars-base-10')
=>

Configuration

Configure your ship using a config file or constructor keyword arguments. Either or both can be used; the keyword args will override any values set via config file.

Supported keys:

  • code - the auth code
  • host - the ship's host (e.g., 'localhost' or 'myship.net')
  • name - the ship's name (e.g, '~zod')
  • port - the open www port on your ship ('80' by default)

Config File

See _config.yml for an example config file. This will connect to a local fake zod, see creation instructions below.

ship = Urbit.new(config_file: 'my-moon.yml')

Constructor Keyword Arguments

ship = Urbit.new(host: '127.0.0.1', port: '8080')

Testing

bin/test

Tests assume that an instance of a "fake" development Urbit ship (one not connected to the live network) will be running, available at http://localhost:8080.

"fake" ~zod

To create this development ship:

./urbit -F zod

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/urbit-api.

License

The gem is available as open source under the terms of the MIT License.