urbit-api 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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