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.
- checksums.yaml +4 -4
- data/.ruby-version +2 -1
- data/README.md +191 -32
- data/bin/console +6 -3
- data/lib/urbit/ack_message.rb +14 -9
- data/lib/urbit/api/version.rb +1 -1
- data/lib/urbit/channel.rb +33 -22
- data/lib/urbit/chat_channel.rb +22 -0
- data/lib/urbit/close_message.rb +15 -0
- data/lib/urbit/fact.rb +206 -0
- data/lib/urbit/graph.rb +143 -0
- data/lib/urbit/message.rb +18 -24
- data/lib/urbit/node.rb +152 -0
- data/lib/urbit/parser.rb +62 -0
- data/lib/urbit/poke_message.rb +7 -0
- data/lib/urbit/receiver.rb +30 -10
- data/lib/urbit/setting.rb +30 -0
- data/lib/urbit/ship.rb +170 -15
- data/lib/urbit/subscribe_message.rb +8 -9
- data/misc/graph-store_graph +51 -0
- data/misc/graph-store_keys +15 -0
- data/misc/graph-store_node +34 -0
- data/misc/graph-store_update +76 -0
- data/misc/graph-update_add-graph +20 -0
- data/misc/graph-update_add-nodes +75 -0
- data/misc/post +12 -0
- data/misc/settings-store.json +52 -0
- data/urbit-api.gemspec +1 -3
- metadata +20 -4
data/lib/urbit/graph.rb
ADDED
@@ -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 '
|
1
|
+
require 'faraday'
|
2
2
|
|
3
3
|
module Urbit
|
4
4
|
class Message
|
5
5
|
attr_accessor :id
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :app, :channel, :contents, :mark
|
7
7
|
|
8
|
-
def initialize(channel
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@id
|
13
|
-
@
|
14
|
-
|
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:
|
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
|
data/lib/urbit/parser.rb
ADDED
@@ -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
|
data/lib/urbit/receiver.rb
CHANGED
@@ -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 :
|
8
|
+
attr_accessor :facts
|
8
9
|
|
9
|
-
def initialize(channel)
|
10
|
-
@
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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.
|
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
|