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.
- checksums.yaml +4 -4
- data/README.md +123 -63
- data/bin/console +6 -3
- data/lib/urbit/ack_message.rb +10 -2
- data/lib/urbit/api/version.rb +1 -1
- data/lib/urbit/channel.rb +27 -24
- data/lib/urbit/chat_channel.rb +22 -0
- data/lib/urbit/close_message.rb +6 -2
- data/lib/urbit/fact.rb +66 -0
- data/lib/urbit/graph.rb +142 -0
- data/lib/urbit/message.rb +17 -12
- data/lib/urbit/node.rb +112 -0
- data/lib/urbit/parser.rb +48 -0
- data/lib/urbit/poke_message.rb +2 -2
- data/lib/urbit/receiver.rb +27 -7
- data/lib/urbit/ship.rb +99 -10
- data/lib/urbit/subscribe_message.rb +6 -2
- 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
- metadata +14 -2
data/lib/urbit/graph.rb
ADDED
@@ -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 :
|
6
|
+
attr_reader :app, :channel, :contents, :mark
|
8
7
|
|
9
|
-
def initialize(channel
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@id
|
14
|
-
@
|
15
|
-
|
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:
|
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
|
data/lib/urbit/parser.rb
ADDED
@@ -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
|
data/lib/urbit/poke_message.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Urbit
|
2
2
|
class PokeMessage < Message
|
3
|
-
def initialize(channel
|
4
|
-
super(channel
|
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
|
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
8
|
attr_accessor :facts
|
8
9
|
|
9
|
-
def initialize(channel)
|
10
|
+
def initialize(channel:)
|
10
11
|
@facts = []
|
11
|
-
|
12
|
-
|
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.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
|
-
|
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
|
-
|
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
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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(
|
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
|