urbit-api 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ require 'set'
2
+ require 'urbit/node'
3
+ require 'urbit/parser'
4
+
5
+ module Urbit
6
+ class Graph
7
+ attr_reader :host_ship_name, :name, :ship
8
+
9
+ def initialize(ship:, graph_name:, host_ship_name:)
10
+ @ship = ship
11
+ @name = graph_name
12
+ @host_ship_name = host_ship_name
13
+ @nodes = SortedSet.new
14
+ end
15
+
16
+ def add_node(node:)
17
+ @nodes << node
18
+ end
19
+
20
+ def host_ship
21
+ "~#{@host_ship_name}"
22
+ end
23
+
24
+ #
25
+ # This method doesn't have a json mark and thus is not (yet) callable from the Airlock.
26
+ # Answers a %noun in `(unit mark)` format.
27
+ #
28
+ # def mark
29
+ # r = self.ship.scry(app: 'graph-store', path: "/graph/#{self.to_s}/mark")
30
+ # end
31
+
32
+ #
33
+ # Finds a single node in this graph by its index.
34
+ # The index here should be the atom representation (as returned by Node#index).
35
+ #
36
+ def node(index:)
37
+ self.fetch_node(index).first
38
+ end
39
+
40
+ #
41
+ # Answers an array with all of this Graph's currently attached Nodes, recursively
42
+ # inluding all of the Node's children.
43
+ #
44
+ def nodes
45
+ self.fetch_all_nodes if @nodes.empty?
46
+ @all_n = []
47
+ @nodes.each do |n|
48
+ @all_n << n
49
+ n.children.each do |c|
50
+ @all_n << c
51
+ end
52
+ end
53
+ @all_n
54
+ end
55
+
56
+ def newest_nodes(count: 10)
57
+ count = 1 if count < 1
58
+ return self.fetch_newest_nodes(count) if @nodes.empty? || @nodes.count < count
59
+ self.nodes.reverse[0..(count - 1)]
60
+ end
61
+
62
+ def oldest_nodes(count: 10)
63
+ count = 1 if count < 1
64
+ return self.fetch_oldest_nodes(count) if @nodes.empty? || @nodes.count < count
65
+ self.nodes[0..(count - 1)]
66
+ end
67
+
68
+ def resource
69
+ "#{self.host_ship}/#{self.name}"
70
+ end
71
+
72
+ #
73
+ # Answers the {count} newer sibling nodes relative to the passed {node}.
74
+ #
75
+ def newer_sibling_nodes(node:, count:)
76
+ self.fetch_sibling_nodes(node, :newer, count)[0..(count - 1)]
77
+ end
78
+
79
+ #
80
+ # Answers the {count} older sibling nodes relative to the passed {node}.
81
+ #
82
+ def older_sibling_nodes(node:, count:)
83
+ self.fetch_sibling_nodes(node, :older, count)[0..(count - 1)]
84
+ end
85
+
86
+ #
87
+ # the canonical printed representation of a Graph
88
+ def to_s
89
+ "a Graph(#{self.resource})"
90
+ end
91
+
92
+ private
93
+
94
+ def fetch_all_nodes
95
+ self.fetch_nodes("#{self.graph_resource}/",
96
+ AddGraphParser,
97
+ "add-graph")
98
+ end
99
+
100
+ def fetch_newest_nodes(count)
101
+ self.fetch_nodes("#{self.graph_resource}/node/siblings/newest/kith/#{count}/",
102
+ AddNodesParser,
103
+ "add-nodes")
104
+ end
105
+
106
+ def fetch_node(index_atom)
107
+ self.fetch_nodes("#{self.graph_resource}/node/index/kith/#{index_atom}/",
108
+ AddNodesParser,
109
+ "add-nodes")
110
+ end
111
+
112
+ def fetch_oldest_nodes(count)
113
+ self.fetch_nodes("#{self.graph_resource}/node/siblings/oldest/kith/#{count}/",
114
+ AddNodesParser,
115
+ "add-nodes")
116
+ end
117
+
118
+ def fetch_sibling_nodes(node, direction, count)
119
+ self.fetch_nodes("#{self.graph_resource}/node/siblings/#{direction}/kith/#{count}/#{node.index}/",
120
+ AddNodesParser,
121
+ "add-nodes")
122
+ end
123
+
124
+ #
125
+ # Answers an array of Nodes that were fetched or an empty array if nothing found.
126
+ #
127
+ def fetch_nodes(endpoint, parser, node)
128
+ r = self.ship.scry(app: 'graph-store', path: endpoint)
129
+ if (200 == r[:status])
130
+ body = JSON.parse(r[:body])
131
+ if (p = parser.new(for_graph: self, with_json: body["graph-update"][node]))
132
+ return p.add_nodes
133
+ end
134
+ end
135
+ []
136
+ end
137
+
138
+ def graph_resource
139
+ "/graph/#{self.resource}"
140
+ end
141
+ end
142
+ end
data/lib/urbit/message.rb CHANGED
@@ -1,18 +1,24 @@
1
1
  require 'faraday'
2
- require 'json'
3
2
 
4
3
  module Urbit
5
4
  class Message
6
5
  attr_accessor :id
7
- attr_reader :action, :app, :channel, :json, :mark
6
+ attr_reader :app, :channel, :contents, :mark
8
7
 
9
- def initialize(channel, action, app = nil, mark = nil, json = nil)
10
- @action = action
11
- @app = app
12
- @channel = channel
13
- @id = 0
14
- @json = json
15
- @mark = mark
8
+ def initialize(channel:, app: nil, mark: nil, contents: nil)
9
+ @app = app
10
+ @channel = channel
11
+ @contents = contents
12
+ @id = 0
13
+ @mark = mark
14
+ end
15
+
16
+ #
17
+ # The value for "action" that the inbound API expects for this message type.
18
+ # defaults to "poke" for historical reasons, but each subclass should override appropriately.
19
+ #
20
+ def action
21
+ "poke"
16
22
  end
17
23
 
18
24
  def channel_url
@@ -33,10 +39,10 @@ module Urbit
33
39
 
34
40
  def to_h
35
41
  {
36
- action: action,
42
+ action: self.action,
37
43
  app: app,
38
44
  id: id,
39
- json: json,
45
+ json: contents,
40
46
  mark: mark,
41
47
  ship: ship.untilded_name
42
48
  }
@@ -51,7 +57,6 @@ module Urbit
51
57
  req.headers['Cookie'] = self.ship.cookie
52
58
  req.headers['Content-Type'] = 'application/json'
53
59
  req.body = request_body
54
- # puts req.body.to_s
55
60
  end
56
61
 
57
62
  # TODO: handle_error if response.status != 204
data/lib/urbit/node.rb ADDED
@@ -0,0 +1,112 @@
1
+ require 'set'
2
+
3
+ module Urbit
4
+ class Node
5
+ attr_accessor :node_json
6
+ def initialize(graph:, node_json:)
7
+ @graph = graph
8
+ @post_h = node_json['post']
9
+ @children_h = node_json['children']
10
+ @persistent = false
11
+ @index = nil
12
+ end
13
+
14
+ def ==(another_node)
15
+ another_node.raw_index == self.raw_index
16
+ end
17
+
18
+ def <=>(another_node)
19
+ self.time_sent <=> another_node.time_sent
20
+ end
21
+
22
+ def eql?(another_node)
23
+ another_node.raw_index == self.raw_index
24
+ end
25
+
26
+ def hash
27
+ self.raw_index.hash
28
+ end
29
+
30
+ def author
31
+ @post_h["author"]
32
+ end
33
+
34
+ def children
35
+ @children = SortedSet.new
36
+ if @children_h
37
+ @children_h.each do |k, v|
38
+ @children << (n = Urbit::Node.new(graph: @graph, node_json: v))
39
+ # Recursively fetch all the children's children until we reach the bottom...
40
+ n.children.each {|c| @children << c} if !n.children.empty?
41
+ end
42
+ end
43
+ @children
44
+ end
45
+
46
+ def contents
47
+ @post_h['contents']
48
+ end
49
+
50
+ def persistent?
51
+ @persistent
52
+ end
53
+
54
+ #
55
+ # Answers the memoized @index or calculates it from the raw_index.
56
+ #
57
+ def index
58
+ return @index if @index
59
+ @index = self.index_to_atom
60
+ end
61
+
62
+ #
63
+ # Answers the next {count} Nodes relative to this Node.
64
+ # Defaults to the next Node if no {count} is passed.
65
+ #
66
+ def next(count: 1)
67
+ @graph.newer_sibling_nodes(node: self, count: count)
68
+ end
69
+
70
+ #
71
+ # Answers the previous {count} Nodes relative to this Node.
72
+ # Defaults to the next Node if no {count} is passed.
73
+ #
74
+ def previous(count: 1)
75
+ @graph.older_sibling_nodes(node: self, count: count)
76
+ end
77
+
78
+ def raw_index
79
+ @post_h["index"].delete_prefix('/')
80
+ end
81
+
82
+ def time_sent
83
+ @post_h['time-sent']
84
+ end
85
+
86
+ def to_h
87
+ {
88
+ index: self.index,
89
+ author: self.author,
90
+ contents: self.contents,
91
+ time_sent: self.time_sent,
92
+ is_parent: !self.children.empty?,
93
+ child_count: self.children.count
94
+ }
95
+ end
96
+
97
+ def to_s
98
+ "a Node(#{self.to_h})"
99
+ end
100
+
101
+ private
102
+
103
+ def index_to_atom
104
+ subkeys = self.raw_index.split("/")
105
+ subatoms = []
106
+ subkeys.each do |s|
107
+ subatoms << s.reverse.scan(/.{1,3}/).join('.').reverse
108
+ end
109
+ subatoms.join('/')
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,48 @@
1
+ require 'set'
2
+ require 'urbit/node'
3
+
4
+ module Urbit
5
+ class Parser
6
+ def initialize(for_graph:, with_json:)
7
+ @g = for_graph
8
+ @j = with_json
9
+ end
10
+
11
+ #
12
+ # Parses the embedded json and adds any found nodes to the graph.
13
+ # Answers an array of nodes.
14
+ #
15
+ def add_nodes
16
+ added_nodes = []
17
+ # Make sure we are adding to the correct graph...
18
+ if (@g.resource == self.resource)
19
+ self.nodes_hash.each do |k, v|
20
+ added_nodes << (n = Urbit::Node.new(graph: @g, node_json: v))
21
+ @g.add_node(node: n)
22
+ end
23
+ end
24
+ added_nodes
25
+ end
26
+
27
+ def resource
28
+ "~#{self.resource_hash["ship"]}/#{self.resource_hash["name"]}"
29
+ end
30
+
31
+ def resource_hash
32
+ @j["resource"]
33
+ end
34
+ end
35
+
36
+ class AddGraphParser < Parser
37
+ def nodes_hash
38
+ @j["graph"]
39
+ end
40
+
41
+ end
42
+
43
+ class AddNodesParser < Parser
44
+ def nodes_hash
45
+ @j["nodes"]
46
+ end
47
+ end
48
+ end
@@ -1,7 +1,7 @@
1
1
  module Urbit
2
2
  class PokeMessage < Message
3
- def initialize(channel, app, mark, a_string)
4
- super(channel, 'poke', app, mark, a_string)
3
+ def initialize(channel:, app:, mark:, a_string:)
4
+ super(channel: channel, app: app, mark: mark, contents: a_string)
5
5
  end
6
6
  end
7
7
  end
@@ -1,27 +1,47 @@
1
1
  require 'ld-eventsource'
2
2
 
3
3
  require 'urbit/ack_message'
4
+ require 'urbit/fact'
4
5
 
5
6
  module Urbit
6
7
  class Receiver < SSE::Client
7
8
  attr_accessor :facts
8
9
 
9
- def initialize(channel)
10
+ def initialize(channel:)
10
11
  @facts = []
11
- @headers = {'cookie' => channel.ship.cookie}
12
- super(channel.url, {headers: @headers}) do |rec|
12
+ super(channel.url, {headers: self.headers(channel)}) do |rec|
13
+ # We are now listening on a socket for SSE::Events. This block will be called for each one.
13
14
  rec.on_event do |event|
14
- typ = event.type
15
- dat = JSON.parse(event.data)
16
- self.facts << {typ => dat}
17
- channel.send_message(AckMessage.new(channel, event.id))
15
+ # Wrap the returned event in a Fact.
16
+ @facts << (f = Fact.new(channel: channel, event: event))
17
+
18
+ # We need to acknowlege each message or urbit will eventually disconnect us.
19
+ # We record the ack with the Fact itself.
20
+ f.add_ack(ack: (ack = AckMessage.new(channel: channel, sse_message_id: event.id)))
21
+ channel.send(message: ack)
18
22
  end
19
23
 
20
24
  rec.on_error do |error|
21
25
  self.facts += ["I received an error fact: #{error.class}"]
22
26
  end
23
27
  end
28
+ @is_open = true
29
+ end
30
+
31
+ def open?
32
+ @is_open
24
33
  end
25
34
 
35
+ private
36
+
37
+ def headers(channel)
38
+ {
39
+ "Accept" => "text/event-stream",
40
+ "Cache-Control" => "no-cache",
41
+ 'Cookie' => channel.ship.cookie,
42
+ "Connection" => "keep-alive",
43
+ "User-Agent" => "urbit-ruby"
44
+ }
45
+ end
26
46
  end
27
47
  end
data/lib/urbit/ship.rb CHANGED
@@ -2,6 +2,7 @@ require 'faraday'
2
2
 
3
3
  require 'urbit/channel'
4
4
  require 'urbit/config'
5
+ require 'urbit/graph'
5
6
 
6
7
  module Urbit
7
8
  class Ship
@@ -12,6 +13,7 @@ module Urbit
12
13
  @auth_cookie = nil
13
14
  @channels = []
14
15
  @config = config
16
+ @graphs = []
15
17
  @logged_in = false
16
18
  end
17
19
 
@@ -27,6 +29,37 @@ module Urbit
27
29
  auth_cookie
28
30
  end
29
31
 
32
+ def graph(resource:)
33
+ self.graphs.find {|g| g.resource == resource}
34
+ end
35
+
36
+ #
37
+ # Answers a collection of all the top-level graphs on this ship.
38
+ # This collection is cached and will need to be invalidated to discover new graphs.
39
+ #
40
+ def graphs(flush_cache: false)
41
+ @graphs = [] if flush_cache
42
+ if @graphs.empty?
43
+ if self.logged_in?
44
+ r = self.scry(app: 'graph-store', path: '/keys')
45
+ if r[:body]
46
+ body = JSON.parse r[:body]
47
+ body["graph-update"]["keys"].each do |k|
48
+ @graphs << Graph.new(ship: self, graph_name: k["name"], host_ship_name: k["ship"])
49
+ end
50
+ end
51
+ end
52
+ end
53
+ @graphs
54
+ end
55
+
56
+ #
57
+ # A helper method to just print out the descriptive names of all the ship's graphs.
58
+ #
59
+ def graph_names
60
+ self.graphs.collect {|g| g.resource}
61
+ end
62
+
30
63
  def login
31
64
  return self if logged_in?
32
65
 
@@ -40,6 +73,23 @@ module Urbit
40
73
  config.name
41
74
  end
42
75
 
76
+ def remove_graph(graph:)
77
+ delete_json = %Q({
78
+ "delete": {
79
+ "resource": {
80
+ "ship": "#{self.name}",
81
+ "name": "#{graph.name}"
82
+ }
83
+ }
84
+ })
85
+
86
+ spider = self.spider(mark_in: 'graph-view-action', mark_out: 'json', thread: 'graph-delete', data: delete_json, args: ["NO_RESPONSE"])
87
+ if (retcode = (200 == spider[:status]))
88
+ self.graphs.delete graph
89
+ end
90
+ retcode
91
+ end
92
+
43
93
  def untilded_name
44
94
  name.gsub('~', '')
45
95
  end
@@ -52,9 +102,20 @@ module Urbit
52
102
  @channels.select {|c| c.open?}
53
103
  end
54
104
 
55
- def scry(app, path, mark)
105
+ #
106
+ # Poke an app with a message using a mark.
107
+ #
108
+ # Returns a Channel which has been created and opened and will begin
109
+ # to get back a stream of facts via its Receiver.
110
+ #
111
+ def poke(app:, mark:, message:)
112
+ (self.add_channel).poke(app: app, mark: mark, message: message)
113
+ end
114
+
115
+ def scry(app:, path:, mark: 'json')
56
116
  self.login
57
- scry_url = "#{self.config.api_base_url}/~/scry/#{app}#{path}.#{mark}"
117
+ mark = ".#{mark}" unless mark.empty?
118
+ scry_url = "#{self.config.api_base_url}/~/scry/#{app}#{path}#{mark}"
58
119
 
59
120
  response = Faraday.get(scry_url) do |req|
60
121
  req.headers['Accept'] = 'application/json'
@@ -64,10 +125,29 @@ module Urbit
64
125
  {status: response.status, code: response.reason_phrase, body: response.body}
65
126
  end
66
127
 
67
- def spider(mark_in, mark_out, thread, data)
128
+ def spider(mark_in:, mark_out:, thread:, data:, args: [])
68
129
  self.login
69
130
  url = "#{self.config.api_base_url}/spider/#{mark_in}/#{thread}/#{mark_out}.json"
70
131
 
132
+ # TODO: This is a huge hack due to the fact that certain spider operations are known to
133
+ # not return when they should. Instead I just set the timeout low and catch the
134
+ # error and act like everything is ok.
135
+ if args.include?("NO_RESPONSE")
136
+ conn = Faraday::Connection.new()
137
+ conn.options.timeout = 1
138
+ conn.options.open_timeout = 1
139
+
140
+ begin
141
+ response = conn.post(url) do |req|
142
+ req.headers['Accept'] = 'application/json'
143
+ req.headers['Cookie'] = self.cookie
144
+ req.body = data
145
+ end
146
+ rescue Faraday::TimeoutError
147
+ return {status: 200, code: "ok", body: "null"}
148
+ end
149
+ end
150
+
71
151
  response = Faraday.post(url) do |req|
72
152
  req.headers['Accept'] = 'application/json'
73
153
  req.headers['Cookie'] = self.cookie
@@ -79,21 +159,30 @@ module Urbit
79
159
 
80
160
  #
81
161
  # Subscribe to an app at a path.
82
- # Returns a Receiver which will begin to get back a stream of facts... which is a... Dictionary? Encyclopedia?
83
162
  #
84
- def subscribe(app, path)
85
- self.login
86
- (c = Channel.new self, self.make_channel_name).open("Creating a Subscription Channel.")
87
- self.channels << c
88
- c.subscribe(app, path)
163
+ # Returns a Channel which has been created and opened and will begin
164
+ # to get back a stream of facts via its Receiver.
165
+ #
166
+ def subscribe(app:, path:)
167
+ (self.add_channel).subscribe(app: app, path: path)
168
+ end
169
+
170
+ def to_h
171
+ {name: "#{self.pat_p}", host: "#{self.config.host}", port: "#{self.config.port}"}
89
172
  end
90
173
 
91
174
  def to_s
92
- "a Ship(name: '#{self.pat_p}', host: '#{self.config.host}', port: '#{self.config.port}')"
175
+ "a Ship(#{self.to_h})"
93
176
  end
94
177
 
95
178
  private
96
179
 
180
+ def add_channel
181
+ self.login
182
+ self.channels << (c = Channel.new(ship: self, name: self.make_channel_name))
183
+ c
184
+ end
185
+
97
186
  def make_channel_name
98
187
  "Channel-#{self.open_channels.count}"
99
188
  end