meshchat 0.8.0 → 0.10.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 +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
|