urbit-api 0.1.0 → 0.3.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,143 @@
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 unless node.deleted?
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
+ last_node = self.nodes.count - 1
60
+ self.nodes[(last_node - count)..last_node]
61
+ end
62
+
63
+ def oldest_nodes(count: 10)
64
+ count = 1 if count < 1
65
+ return self.fetch_oldest_nodes(count) if @nodes.empty? || @nodes.count < count
66
+ self.nodes[0..(count - 1)]
67
+ end
68
+
69
+ def resource
70
+ "#{self.host_ship}/#{self.name}"
71
+ end
72
+
73
+ #
74
+ # Answers the {count} newer sibling nodes relative to the passed {node}.
75
+ #
76
+ def newer_sibling_nodes(node:, count:)
77
+ self.fetch_sibling_nodes(node, :newer, count)[0..(count - 1)]
78
+ end
79
+
80
+ #
81
+ # Answers the {count} older sibling nodes relative to the passed {node}.
82
+ #
83
+ def older_sibling_nodes(node:, count:)
84
+ self.fetch_sibling_nodes(node, :older, count)[0..(count - 1)]
85
+ end
86
+
87
+ #
88
+ # the canonical printed representation of a Graph
89
+ def to_s
90
+ "a Graph(#{self.resource})"
91
+ end
92
+
93
+ private
94
+
95
+ def fetch_all_nodes
96
+ self.fetch_nodes("#{self.graph_resource}/",
97
+ AddGraphParser,
98
+ "add-graph")
99
+ end
100
+
101
+ def fetch_newest_nodes(count)
102
+ self.fetch_nodes("#{self.graph_resource}/node/siblings/newest/kith/#{count}/",
103
+ AddNodesParser,
104
+ "add-nodes")
105
+ end
106
+
107
+ def fetch_node(index_atom)
108
+ self.fetch_nodes("#{self.graph_resource}/node/index/kith/#{index_atom}/",
109
+ AddNodesParser,
110
+ "add-nodes")
111
+ end
112
+
113
+ def fetch_oldest_nodes(count)
114
+ self.fetch_nodes("#{self.graph_resource}/node/siblings/oldest/kith/#{count}/",
115
+ AddNodesParser,
116
+ "add-nodes")
117
+ end
118
+
119
+ def fetch_sibling_nodes(node, direction, count)
120
+ self.fetch_nodes("#{self.graph_resource}/node/siblings/#{direction}/kith/#{count}/#{node.index}/",
121
+ AddNodesParser,
122
+ "add-nodes")
123
+ end
124
+
125
+ #
126
+ # Answers an array of Nodes that were fetched or an empty array if nothing found.
127
+ #
128
+ def fetch_nodes(endpoint, parser, node)
129
+ r = self.ship.scry(app: 'graph-store', path: endpoint)
130
+ if (200 == r[:status])
131
+ body = JSON.parse(r[:body])
132
+ if (p = parser.new(for_graph: self, with_json: body["graph-update"][node]))
133
+ return p.add_nodes
134
+ end
135
+ end
136
+ []
137
+ end
138
+
139
+ def graph_resource
140
+ "/graph/#{self.resource}"
141
+ end
142
+ end
143
+ end
data/lib/urbit/message.rb CHANGED
@@ -1,17 +1,24 @@
1
- require 'json'
1
+ require 'faraday'
2
2
 
3
3
  module Urbit
4
4
  class Message
5
5
  attr_accessor :id
6
- attr_reader :action, :app, :channel, :json, :mark
6
+ attr_reader :app, :channel, :contents, :mark
7
7
 
8
- def initialize(channel, action, app, mark, json)
9
- @action = action
10
- @app = app
11
- @channel = channel
12
- @id = 0
13
- @json = json
14
- @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"
15
22
  end
16
23
 
17
24
  def channel_url
@@ -32,10 +39,10 @@ module Urbit
32
39
 
33
40
  def to_h
34
41
  {
35
- action: action,
42
+ action: self.action,
36
43
  app: app,
37
44
  id: id,
38
- json: json,
45
+ json: contents,
39
46
  mark: mark,
40
47
  ship: ship.untilded_name
41
48
  }
@@ -50,23 +57,10 @@ module Urbit
50
57
  req.headers['Cookie'] = self.ship.cookie
51
58
  req.headers['Content-Type'] = 'application/json'
52
59
  req.body = request_body
53
- # puts req.body.to_s
54
60
  end
55
61
 
56
62
  # TODO: handle_error if response.status != 204
57
63
  response
58
64
  end
59
65
  end
60
-
61
- class CloseMessage < Message
62
- def initialize(channel)
63
- @action = 'delete'
64
- @channel = channel
65
- @id = 0
66
- end
67
-
68
- def to_h
69
- {id: id, action: action}
70
- end
71
- end
72
66
  end
data/lib/urbit/node.rb ADDED
@@ -0,0 +1,152 @@
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
+ #
15
+ # Given a bigint representing an urbit date, returns a unix timestamp.
16
+ #
17
+ def self.da_to_unix(da)
18
+ # ported from urbit lib.ts which in turn was ported from +time:enjs:format in hoon.hoon
19
+ da_second = 18446744073709551616
20
+ da_unix_epoch = 170141184475152167957503069145530368000
21
+ offset = da_second / 2000
22
+ epoch_adjusted = offset + (da - da_unix_epoch)
23
+ return (epoch_adjusted * 1000) / da_second
24
+ end
25
+
26
+ #
27
+ # Given a unix timestamp, returns a bigint representing an urbit date
28
+ #
29
+ def self.unix_to_da(unix)
30
+ da_second = 18446744073709551616
31
+ da_unix_epoch = 170141184475152167957503069145530368000
32
+ time_since_epoch = (unix * da_second) / 1000
33
+ return da_unix_epoch + time_since_epoch;
34
+ end
35
+
36
+ def ==(another_node)
37
+ another_node.index == self.index
38
+ end
39
+
40
+ def <=>(another_node)
41
+ self.time_sent <=> another_node.time_sent
42
+ end
43
+
44
+ def eql?(another_node)
45
+ another_node.index == self.index
46
+ end
47
+
48
+ def deleted?
49
+ # This is a "deleted" node. Not sure what to do yet, but for now don't create a Node.
50
+ @post_h["index"].nil?
51
+ end
52
+
53
+ def hash
54
+ self.index.hash
55
+ end
56
+
57
+ def author
58
+ @post_h["author"]
59
+ end
60
+
61
+ def children
62
+ @children = SortedSet.new
63
+ if @children_h
64
+ @children_h.each do |k, v|
65
+ @children << (n = Urbit::Node.new(graph: @graph, node_json: v))
66
+ # Recursively fetch all the children's children until we reach the bottom...
67
+ n.children.each {|c| @children << c} if !n.children.empty?
68
+ end
69
+ end
70
+ @children
71
+ end
72
+
73
+ def contents
74
+ @post_h['contents']
75
+ end
76
+
77
+ def datetime_sent
78
+ Time.at(self.time_sent / 1000).to_datetime
79
+ end
80
+
81
+ def persistent?
82
+ @persistent
83
+ end
84
+
85
+ #
86
+ # Answers the memoized @index or calculates it from the raw_index.
87
+ #
88
+ def index
89
+ return @index if @index
90
+ @index = self.index_to_atom
91
+ end
92
+
93
+ #
94
+ # Answers the next {count} Nodes relative to this Node.
95
+ # Defaults to the next Node if no {count} is passed.
96
+ #
97
+ def next(count: 1)
98
+ @graph.newer_sibling_nodes(node: self, count: count)
99
+ end
100
+
101
+ #
102
+ # Answers the previous {count} Nodes relative to this Node.
103
+ # Defaults to the next Node if no {count} is passed.
104
+ #
105
+ def previous(count: 1)
106
+ @graph.older_sibling_nodes(node: self, count: count)
107
+ end
108
+
109
+ def raw_index
110
+ return @post_h["index"].delete_prefix('/') unless self.deleted?
111
+ (Node.unix_to_da(Time.now.to_i)).to_s
112
+ end
113
+
114
+ #
115
+ # This is the time sent as recorded by urbit in unix extended format.
116
+ #
117
+ def time_sent
118
+ @post_h['time-sent']
119
+ end
120
+
121
+ def to_h
122
+ {
123
+ index: self.index,
124
+ author: self.author,
125
+ sent: self.datetime_sent,
126
+ contents: self.contents,
127
+ is_parent: !self.children.empty?,
128
+ child_count: self.children.count
129
+ }
130
+ end
131
+
132
+ def to_pretty_array
133
+ self.to_h.each.map {|k, v| "#{k}#{(' ' * (12 - k.length))}#{v}"}
134
+ end
135
+
136
+ def to_s
137
+ "a Node(#{self.to_h})"
138
+ end
139
+
140
+ private
141
+
142
+ def index_to_atom
143
+ subkeys = self.raw_index.split("/")
144
+ subatoms = []
145
+ subkeys.each do |s|
146
+ subatoms << s.reverse.scan(/.{1,3}/).join('.').reverse
147
+ end
148
+ subatoms.join('/')
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,62 @@
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
+ def resource_hash
42
+ @j["resource"]
43
+ end
44
+ end
45
+
46
+ class AddNodesParser < Parser
47
+ def nodes_hash
48
+ @j["nodes"]
49
+ end
50
+ end
51
+
52
+ class RemoveGraphParser < Parser
53
+ def nodes_hash
54
+ nil
55
+ end
56
+
57
+ def resource_hash
58
+ @j["resource"]
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,7 @@
1
+ module Urbit
2
+ class PokeMessage < Message
3
+ def initialize(channel:, app:, mark:, a_string:)
4
+ super(channel: channel, app: app, mark: mark, contents: a_string)
5
+ end
6
+ end
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
- attr_accessor :events
8
+ attr_accessor :facts
8
9
 
9
- def initialize(channel)
10
- @events = []
11
- @headers = {'cookie' => channel.ship.cookie}
12
- super(channel.url, {headers: @headers}) do |rec|
10
+ def initialize(channel:)
11
+ @facts = []
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.events << {typ => dat}
17
- channel.send_message(AckMessage.new(channel, event.id))
15
+ # Wrap the returned event in a Fact.
16
+ @facts << (f = Fact.collect(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
- self.events += ["I received an error: #{error.class}"]
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
@@ -0,0 +1,30 @@
1
+
2
+ module Urbit
3
+ class Setting
4
+ attr_reader :bucket
5
+
6
+ def initialize(ship:, desk:, setting:)
7
+ @ship = ship
8
+ @desk = desk
9
+ @bucket = setting.first
10
+ @entries = setting.last
11
+ end
12
+
13
+ def entries(bucket: nil)
14
+ return @entries if (bucket.nil? || @bucket == bucket)
15
+ {}
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ desk: @desk,
21
+ bucket: self.bucket,
22
+ entries: self.entries
23
+ }
24
+ end
25
+
26
+ def to_s
27
+ "a Setting(#{self.to_h})"
28
+ end
29
+ end
30
+ end