urbit-api 0.1.0 → 0.3.0

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