meshchat 0.8.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +39 -11
- data/lib/meshchat.rb +48 -42
- data/lib/meshchat/configuration.rb +14 -0
- data/lib/meshchat/configuration/app_config.rb +63 -0
- data/lib/meshchat/configuration/database.rb +41 -0
- data/lib/meshchat/{config → configuration}/hash_file.rb +7 -6
- data/lib/meshchat/configuration/identity.rb +79 -0
- data/lib/meshchat/{config → configuration}/settings.rb +22 -26
- data/lib/meshchat/debug.rb +69 -0
- data/lib/meshchat/encryption.rb +7 -2
- data/lib/meshchat/encryption/aes_rsa.rb +2 -1
- data/lib/meshchat/encryption/passthrough.rb +5 -3
- data/lib/meshchat/locale/en.yml +14 -0
- data/lib/meshchat/models/node.rb +140 -0
- data/lib/meshchat/network.rb +19 -0
- data/lib/meshchat/network/dispatcher.rb +83 -0
- data/lib/meshchat/network/errors.rb +11 -0
- data/lib/meshchat/network/incoming.rb +13 -0
- data/lib/meshchat/network/incoming/message_decryptor.rb +51 -0
- data/lib/meshchat/network/incoming/message_processor.rb +75 -0
- data/lib/meshchat/network/incoming/request_processor.rb +30 -0
- data/lib/meshchat/network/local.rb +12 -0
- data/lib/meshchat/network/local/connection.rb +58 -0
- data/lib/meshchat/network/local/server.rb +69 -0
- data/lib/meshchat/network/message.rb +34 -0
- data/lib/meshchat/network/message/base.rb +139 -0
- data/lib/meshchat/network/message/chat.rb +9 -0
- data/lib/meshchat/network/message/disconnect.rb +21 -0
- data/lib/meshchat/network/message/emote.rb +9 -0
- data/lib/meshchat/network/message/factory.rb +80 -0
- data/lib/meshchat/network/message/node_list.rb +75 -0
- data/lib/meshchat/network/message/node_list_diff.rb +18 -0
- data/lib/meshchat/network/message/node_list_hash.rb +32 -0
- data/lib/meshchat/network/message/ping.rb +31 -0
- data/lib/meshchat/network/message/ping_reply.rb +12 -0
- data/lib/meshchat/network/message/whisper.rb +28 -0
- data/lib/meshchat/network/remote.rb +13 -0
- data/lib/meshchat/network/remote/connection.rb +28 -0
- data/lib/meshchat/network/remote/relay.rb +109 -0
- data/lib/meshchat/network/remote/relay_pool.rb +52 -0
- data/lib/meshchat/ui.rb +13 -0
- data/lib/meshchat/ui/cli.rb +48 -0
- data/lib/meshchat/ui/cli/base.rb +39 -0
- data/lib/meshchat/ui/cli/input_factory.rb +50 -0
- data/lib/meshchat/ui/cli/keyboard_line_input.rb +14 -0
- data/lib/meshchat/ui/command.rb +51 -0
- data/lib/meshchat/ui/command/base.rb +77 -0
- data/lib/meshchat/ui/command/bind.rb +47 -0
- data/lib/meshchat/ui/command/chat.rb +31 -0
- data/lib/meshchat/ui/command/config.rb +37 -0
- data/lib/meshchat/ui/command/emote.rb +23 -0
- data/lib/meshchat/ui/command/exit.rb +16 -0
- data/lib/meshchat/ui/command/help.rb +20 -0
- data/lib/meshchat/ui/command/identity.rb +16 -0
- data/lib/meshchat/ui/command/import.rb +42 -0
- data/lib/meshchat/ui/command/irb.rb +22 -0
- data/lib/meshchat/ui/command/offline.rb +23 -0
- data/lib/meshchat/ui/command/online.rb +18 -0
- data/lib/meshchat/ui/command/ping.rb +65 -0
- data/lib/meshchat/ui/command/ping_all.rb +19 -0
- data/lib/meshchat/ui/command/send_disconnect.rb +20 -0
- data/lib/meshchat/ui/command/server.rb +22 -0
- data/lib/meshchat/ui/command/share.rb +16 -0
- data/lib/meshchat/ui/command/whisper.rb +40 -0
- data/lib/meshchat/ui/display.rb +78 -0
- data/lib/meshchat/ui/display/base.rb +58 -0
- data/lib/meshchat/ui/display/manager.rb +59 -0
- data/lib/meshchat/ui/notifier.rb +9 -0
- data/lib/meshchat/ui/notifier/base.rb +33 -0
- data/lib/meshchat/version.rb +3 -2
- metadata +150 -80
- data/lib/meshchat/cli.rb +0 -188
- data/lib/meshchat/cli/base.rb +0 -13
- data/lib/meshchat/cli/input.rb +0 -37
- data/lib/meshchat/command/base.rb +0 -80
- data/lib/meshchat/command/bind.rb +0 -44
- data/lib/meshchat/command/chat.rb +0 -30
- data/lib/meshchat/command/config.rb +0 -34
- data/lib/meshchat/command/emote.rb +0 -20
- data/lib/meshchat/command/exit.rb +0 -13
- data/lib/meshchat/command/help.rb +0 -17
- data/lib/meshchat/command/identity.rb +0 -13
- data/lib/meshchat/command/import.rb +0 -41
- data/lib/meshchat/command/init.rb +0 -34
- data/lib/meshchat/command/irb.rb +0 -23
- data/lib/meshchat/command/listen.rb +0 -13
- data/lib/meshchat/command/offline.rb +0 -20
- data/lib/meshchat/command/online.rb +0 -15
- data/lib/meshchat/command/ping.rb +0 -65
- data/lib/meshchat/command/ping_all.rb +0 -15
- data/lib/meshchat/command/send_disconnect.rb +0 -15
- data/lib/meshchat/command/server.rb +0 -20
- data/lib/meshchat/command/share.rb +0 -13
- data/lib/meshchat/command/stop_listening.rb +0 -13
- data/lib/meshchat/command/whisper.rb +0 -38
- data/lib/meshchat/database.rb +0 -30
- data/lib/meshchat/display.rb +0 -33
- data/lib/meshchat/display/base.rb +0 -60
- data/lib/meshchat/display/manager.rb +0 -55
- data/lib/meshchat/instance.rb +0 -40
- data/lib/meshchat/message.rb +0 -41
- data/lib/meshchat/message/base.rb +0 -97
- data/lib/meshchat/message/chat.rb +0 -19
- data/lib/meshchat/message/disconnect.rb +0 -13
- data/lib/meshchat/message/emote.rb +0 -9
- data/lib/meshchat/message/node_list.rb +0 -63
- data/lib/meshchat/message/node_list_diff.rb +0 -15
- data/lib/meshchat/message/node_list_hash.rb +0 -33
- data/lib/meshchat/message/ping.rb +0 -32
- data/lib/meshchat/message/ping_reply.rb +0 -9
- data/lib/meshchat/message/relay.rb +0 -43
- data/lib/meshchat/message/whisper.rb +0 -36
- data/lib/meshchat/models/entry.rb +0 -104
- data/lib/meshchat/net/client.rb +0 -83
- data/lib/meshchat/net/listener/errors.rb +0 -11
- data/lib/meshchat/net/listener/request.rb +0 -48
- data/lib/meshchat/net/listener/request_processor.rb +0 -50
- data/lib/meshchat/net/listener/server.rb +0 -114
- data/lib/meshchat/net/request.rb +0 -29
- data/lib/meshchat/notifier/base.rb +0 -31
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Meshchat
|
3
|
+
module Network
|
4
|
+
module Incoming
|
5
|
+
# decodes an encrypted message and handles it.
|
6
|
+
# also update's the info of the sender
|
7
|
+
class MessageProcessor
|
8
|
+
attr_reader :_network, :_location
|
9
|
+
attr_reader :_message_factory, :_message_dispatcher
|
10
|
+
|
11
|
+
def initialize(network: NETWORK_LOCAL, message_dispatcher: nil, location: nil)
|
12
|
+
@_network = network
|
13
|
+
@_message_dispatcher = message_dispatcher
|
14
|
+
@_message_factory = message_dispatcher._message_factory
|
15
|
+
@_location = location
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [String] encoded_message - the encrypted message as a string
|
19
|
+
def process(encoded_message)
|
20
|
+
request = MessageDecryptor.new(encoded_message, _message_factory)
|
21
|
+
message = request.message
|
22
|
+
|
23
|
+
Debug.receiving_message(message)
|
24
|
+
|
25
|
+
# show the message to the user, and update the information
|
26
|
+
# we have on the sender, so that we may reply to the
|
27
|
+
# correct location
|
28
|
+
update_sender_info(request._json)
|
29
|
+
Display.present_message message
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param [String] encoded_message - the encrypted message as a string
|
33
|
+
def update_sender_info(json)
|
34
|
+
sender = json['sender']
|
35
|
+
# Note that sender['location'] should always reference
|
36
|
+
# the sender's local network address
|
37
|
+
network_location = sender['location']
|
38
|
+
|
39
|
+
# if the sender isn't currently marked as active,
|
40
|
+
# perform the server list exchange
|
41
|
+
node = Node.find_by_uid(sender['uid'])
|
42
|
+
raise Errors::Forbidden, 'node not found' if node.nil?
|
43
|
+
|
44
|
+
# if we are receiving a message from a node we had previously
|
45
|
+
# known to be offline, we need to do the node list hash dance
|
46
|
+
# with them to see if they know of any new members to the network
|
47
|
+
unless node.online?
|
48
|
+
node.update(on_local_network: true) if is_processing_for_local?
|
49
|
+
node.update(on_relay: true) if is_processing_for_relay?
|
50
|
+
|
51
|
+
nlh = _message_factory.create(Message::NODE_LIST_HASH)
|
52
|
+
_message_dispatcher.send_message(node: node, message: nlh)
|
53
|
+
end
|
54
|
+
|
55
|
+
# update the node's location/alias
|
56
|
+
# as they can change this info willy nilly
|
57
|
+
attributes = {
|
58
|
+
location_on_network: network_location,
|
59
|
+
alias_name: sender['alias']
|
60
|
+
}
|
61
|
+
attributes[:location_of_relay] = _location if is_processing_for_relay?
|
62
|
+
node.update(attributes)
|
63
|
+
end
|
64
|
+
|
65
|
+
def is_processing_for_relay?
|
66
|
+
_network == NETWORK_RELAY
|
67
|
+
end
|
68
|
+
|
69
|
+
def is_processing_for_local?
|
70
|
+
_network != NETWORK_RELAY
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Meshchat
|
3
|
+
module Network
|
4
|
+
module Incoming
|
5
|
+
# all this does is pull the encrypted message out of
|
6
|
+
# the received request
|
7
|
+
class RequestProcessor
|
8
|
+
attr_reader :_message_processor
|
9
|
+
|
10
|
+
def initialize(network: NETWORK_LOCAL, message_dispatcher: nil, location: nil)
|
11
|
+
@_message_processor = MessageProcessor.new(
|
12
|
+
network: network,
|
13
|
+
message_dispatcher: message_dispatcher,
|
14
|
+
location: location)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [String] request_body - the encrypted message as a json string
|
18
|
+
def process(request_body)
|
19
|
+
encoded_message = parse_content(request_body)
|
20
|
+
_message_processor.process(encoded_message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_content(content)
|
24
|
+
content = JSON.parse(content) if content.is_a?(String)
|
25
|
+
content['message']
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'em-http-request'
|
3
|
+
|
4
|
+
module Meshchat
|
5
|
+
module Network
|
6
|
+
module Local
|
7
|
+
class Connection
|
8
|
+
attr_reader :_message_factory, :_message_dispatcher
|
9
|
+
|
10
|
+
def initialize(dispatcher, message_factory)
|
11
|
+
@_message_factory = message_factory
|
12
|
+
@_message_dispatcher = dispatcher
|
13
|
+
|
14
|
+
# async, won't prevent us from sending
|
15
|
+
start_server
|
16
|
+
end
|
17
|
+
|
18
|
+
def start_server
|
19
|
+
port = APP_CONFIG.user['port']
|
20
|
+
Display.info "listening on port #{port}"
|
21
|
+
EM.start_server('0.0.0.0', port,
|
22
|
+
Network::Local::Server, _message_dispatcher)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [Node] node - the node describing the person you're sending a message to
|
26
|
+
# @param [JSON] encrypted_message - the message intended for the person at the location
|
27
|
+
# @param [Block] error_callback - what to do in case of failure
|
28
|
+
def send_message(node, encrypted_message, &error_callback)
|
29
|
+
payload = payload_for(encrypted_message)
|
30
|
+
create_http_request(node.location_on_network, payload, &error_callback)
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_http_request(location, payload, &error_callback)
|
34
|
+
# TODO: what about https?
|
35
|
+
# maybe do the regex match: /https?:\/\//
|
36
|
+
location = 'http://' + location unless location.include?('http://')
|
37
|
+
http = EventMachine::HttpRequest.new(location).post(
|
38
|
+
body: payload,
|
39
|
+
head: {
|
40
|
+
'Accept' => 'application/json',
|
41
|
+
'Content-Type' => 'application/json'
|
42
|
+
})
|
43
|
+
|
44
|
+
http.errback &error_callback
|
45
|
+
# example things available in the callback
|
46
|
+
# p http.response_header.status
|
47
|
+
# p http.response_header
|
48
|
+
# p http.response
|
49
|
+
http.callback {}
|
50
|
+
end
|
51
|
+
|
52
|
+
def payload_for(encrypted_message)
|
53
|
+
{ message: encrypted_message }.to_json
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'evma_httpserver'
|
3
|
+
|
4
|
+
module Meshchat
|
5
|
+
module Network
|
6
|
+
module Local
|
7
|
+
# This is created every request
|
8
|
+
class Server < EM::Connection
|
9
|
+
include EM::HttpServer
|
10
|
+
attr_reader :_message_dispatcher, :_request_processor
|
11
|
+
|
12
|
+
OK = 200
|
13
|
+
BAD_REQUEST = 400
|
14
|
+
NOT_AUTHORIZED = 401
|
15
|
+
FORBIDDEN = 403
|
16
|
+
SERVER_ERROR = 500
|
17
|
+
|
18
|
+
def initialize(message_dispatcher)
|
19
|
+
@_message_dispatcher = message_dispatcher
|
20
|
+
@_request_processor = Incoming::RequestProcessor.new(
|
21
|
+
network: NETWORK_LOCAL,
|
22
|
+
message_dispatcher: message_dispatcher)
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_http_request
|
26
|
+
# the http request details are available via the following instance variables:
|
27
|
+
# @http_protocol
|
28
|
+
# @http_request_method
|
29
|
+
# @http_cookie
|
30
|
+
# @http_if_none_match
|
31
|
+
# @http_content_type
|
32
|
+
# @http_path_info
|
33
|
+
# @http_request_uri
|
34
|
+
# @http_query_string
|
35
|
+
# @http_post_content
|
36
|
+
# @http_headers
|
37
|
+
# view what instance variables are available thorugh the
|
38
|
+
# instance_variables method
|
39
|
+
|
40
|
+
process(@http_post_content)
|
41
|
+
build_response
|
42
|
+
end
|
43
|
+
|
44
|
+
def process(raw)
|
45
|
+
# decode, etc
|
46
|
+
_request_processor.process(raw)
|
47
|
+
rescue Errors::NotAuthorized
|
48
|
+
build_response NOT_AUTHORIZED
|
49
|
+
rescue Errors::Forbidden
|
50
|
+
build_response FORBIDDEN
|
51
|
+
rescue Errors::BadRequest
|
52
|
+
build_response BAD_REQUEST
|
53
|
+
rescue => e
|
54
|
+
Display.error e.message
|
55
|
+
Display.error e.backtrace.join("\n")
|
56
|
+
build_response SERVER_ERROR, e.message
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_response(s = OK, message = '')
|
60
|
+
response = EM::DelegatedHttpResponse.new(self)
|
61
|
+
response.status = s
|
62
|
+
response.content_type 'text/json'
|
63
|
+
response.content = message
|
64
|
+
response.send_response
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Meshchat
|
3
|
+
module Network
|
4
|
+
module Message
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
# @see https://github.com/neuravion/mesh-chat/blob/master/message-types.md
|
8
|
+
CHAT = 'chat'
|
9
|
+
EMOTE = 'emote'
|
10
|
+
PING = 'ping'
|
11
|
+
PING_REPLY = 'pingreply'
|
12
|
+
WHISPER = 'whisper'
|
13
|
+
DISCONNECT = 'disconnect'
|
14
|
+
|
15
|
+
NODE_LIST = 'nodelist'
|
16
|
+
NODE_LIST_HASH = 'nodelisthash'
|
17
|
+
NODE_LIST_DIFF = 'nodelistdiff'
|
18
|
+
|
19
|
+
eager_autoload do
|
20
|
+
autoload :Base
|
21
|
+
autoload :Chat
|
22
|
+
autoload :Emote
|
23
|
+
autoload :Ping
|
24
|
+
autoload :PingReply
|
25
|
+
autoload :Disconnect
|
26
|
+
autoload :Whisper
|
27
|
+
autoload :NodeList
|
28
|
+
autoload :NodeListDiff
|
29
|
+
autoload :NodeListHash
|
30
|
+
autoload :Factory
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Meshchat
|
3
|
+
module Network
|
4
|
+
module Message
|
5
|
+
# NOTE:
|
6
|
+
# #display: shows the message
|
7
|
+
# should be used locally, before *sending* a message
|
8
|
+
# #handle: processing logic for the message
|
9
|
+
# should be used when receiving a message, and there
|
10
|
+
# needs to be a response right away
|
11
|
+
# #respond: where the actual logic for the response goes
|
12
|
+
class Base
|
13
|
+
attr_accessor :payload,
|
14
|
+
:_message, :_sender_name, :_sender_location, :_sender_uid,
|
15
|
+
:_time_received,
|
16
|
+
:_message_dispatcher, :_message_factory
|
17
|
+
|
18
|
+
# @param [String] message
|
19
|
+
# @param [Hash] sender
|
20
|
+
# @param [Hash] payload all paramaters for a received message
|
21
|
+
# @param [MeshChat::Network::Dispatcher] message_dispatcher optionally overrides the default payload
|
22
|
+
# @param [MeshChat::Message::Factory] message_factory the message factory
|
23
|
+
def initialize(
|
24
|
+
message: '',
|
25
|
+
sender: {},
|
26
|
+
payload: {},
|
27
|
+
message_dispatcher: nil,
|
28
|
+
message_factory: nil)
|
29
|
+
|
30
|
+
if payload.present?
|
31
|
+
@payload = payload.deep_stringify_keys
|
32
|
+
else
|
33
|
+
@_message = message
|
34
|
+
@_sender_name = sender['alias']
|
35
|
+
@_sender_location = sender['location']
|
36
|
+
@_sender_uid = sender['uid']
|
37
|
+
@_time_received = Time.now.iso8601
|
38
|
+
end
|
39
|
+
|
40
|
+
@_message_dispatcher = message_dispatcher
|
41
|
+
@_message_factory = message_factory
|
42
|
+
end
|
43
|
+
|
44
|
+
def payload
|
45
|
+
@payload ||= {
|
46
|
+
'type' => type,
|
47
|
+
'message' => _message,
|
48
|
+
'client' => client,
|
49
|
+
'client_version' => client_version,
|
50
|
+
'time_sent' => _time_received,
|
51
|
+
'sender' => {
|
52
|
+
'alias' => _sender_name,
|
53
|
+
'location' => _sender_location,
|
54
|
+
'uid' => _sender_uid
|
55
|
+
}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def type
|
60
|
+
@type ||= Factory::TYPES.invert[self.class]
|
61
|
+
end
|
62
|
+
|
63
|
+
def message
|
64
|
+
_message || payload['message']
|
65
|
+
end
|
66
|
+
|
67
|
+
def sender
|
68
|
+
payload['sender']
|
69
|
+
end
|
70
|
+
|
71
|
+
def sender_name
|
72
|
+
_sender_name || sender['alias']
|
73
|
+
end
|
74
|
+
|
75
|
+
def sender_location
|
76
|
+
_sender_location || sender['location']
|
77
|
+
end
|
78
|
+
|
79
|
+
def sender_uid
|
80
|
+
_sender_uid || sender['uid']
|
81
|
+
end
|
82
|
+
|
83
|
+
def time_received
|
84
|
+
_time_received || payload['time_sent']
|
85
|
+
end
|
86
|
+
|
87
|
+
def time_received_as_date
|
88
|
+
DateTime.parse(time_received) if time_received
|
89
|
+
end
|
90
|
+
|
91
|
+
def client
|
92
|
+
APP_CONFIG[:client_name]
|
93
|
+
end
|
94
|
+
|
95
|
+
def client_version
|
96
|
+
APP_CONFIG[:client_version]
|
97
|
+
end
|
98
|
+
|
99
|
+
# shows the message
|
100
|
+
# should be used locally, before *sending* a message
|
101
|
+
def display
|
102
|
+
{
|
103
|
+
time: time_received_as_date,
|
104
|
+
from: sender_name,
|
105
|
+
message: message
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
# processing logic for the message
|
110
|
+
# should be used when receiving a message, and there
|
111
|
+
# needs to be a response right away.
|
112
|
+
# this may call display, if the response is always to be displayed
|
113
|
+
def handle
|
114
|
+
display
|
115
|
+
end
|
116
|
+
|
117
|
+
# Most message types aren't going to need to have an
|
118
|
+
# immediate response.
|
119
|
+
def respond
|
120
|
+
end
|
121
|
+
|
122
|
+
# this message should be called immediately
|
123
|
+
# before sending to the whomever
|
124
|
+
def render
|
125
|
+
payload.to_json
|
126
|
+
end
|
127
|
+
|
128
|
+
alias_method :jsonized_payload, :render
|
129
|
+
|
130
|
+
def encrypt_for(node)
|
131
|
+
result = jsonized_payload
|
132
|
+
public_key = node.public_key
|
133
|
+
result = Encryption.encrypt(result, public_key) if node.public_key
|
134
|
+
Base64.strict_encode64(result)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Meshchat
|
3
|
+
module Network
|
4
|
+
module Message
|
5
|
+
class Disconnect < Base
|
6
|
+
def display
|
7
|
+
location = payload['sender']['location']
|
8
|
+
uid = payload['sender']['uid']
|
9
|
+
name = payload['sender']['alias']
|
10
|
+
node = Node.find_by_uid(uid)
|
11
|
+
if node
|
12
|
+
node.update(on_local_network: false)
|
13
|
+
node.update(on_relay: false)
|
14
|
+
end
|
15
|
+
|
16
|
+
"#{name}@#{location} has disconnected"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|