nostr_ruby 0.2.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/Gemfile.lock +10 -8
- data/README.md +286 -161
- data/lib/bech32.rb +84 -0
- data/lib/client.rb +184 -0
- data/lib/context.rb +48 -0
- data/lib/event.rb +188 -0
- data/lib/event_wizard.rb +31 -0
- data/lib/filter.rb +57 -0
- data/lib/key.rb +15 -0
- data/lib/kind.rb +10 -0
- data/lib/message_handler.rb +107 -0
- data/lib/nostr_ruby.rb +13 -273
- data/lib/signer.rb +89 -0
- data/lib/version.rb +3 -0
- data/nostr_ruby-0.2.0.gem +0 -0
- data/nostr_ruby.gemspec +4 -4
- metadata +19 -9
- data/lib/custom_addr.rb +0 -59
- data/lib/nostr_ruby/version.rb +0 -3
data/lib/client.rb
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
require_relative 'event_wizard'
|
2
|
+
require 'faye/websocket'
|
3
|
+
|
4
|
+
module Nostr
|
5
|
+
|
6
|
+
class Client
|
7
|
+
include EventWizard
|
8
|
+
|
9
|
+
attr_reader :signer
|
10
|
+
attr_reader :relay
|
11
|
+
attr_reader :subscriptions
|
12
|
+
|
13
|
+
|
14
|
+
def initialize(signer: nil, private_key: nil, relay: nil, context: Context.new(timeout: 5))
|
15
|
+
initialize_event_emitter
|
16
|
+
|
17
|
+
if signer
|
18
|
+
@signer = signer
|
19
|
+
elsif private_key
|
20
|
+
@signer = Nostr::Signer.new(private_key: private_key)
|
21
|
+
end
|
22
|
+
|
23
|
+
@relay = relay
|
24
|
+
@context = context
|
25
|
+
|
26
|
+
@running = false
|
27
|
+
@expected_response_id = nil
|
28
|
+
@response_condition = ConditionVariable.new
|
29
|
+
@response_mutex = Mutex.new
|
30
|
+
@event_to_publish = nil
|
31
|
+
|
32
|
+
@subscriptions = {}
|
33
|
+
@outbound_channel = EventMachine::Channel.new
|
34
|
+
@inbound_channel = EventMachine::Channel.new
|
35
|
+
|
36
|
+
@inbound_channel.subscribe do |msg|
|
37
|
+
case msg[:type]
|
38
|
+
when :open
|
39
|
+
emit :connect, msg[:relay]
|
40
|
+
when :message
|
41
|
+
parsed_data = Nostr::MessageHandler.handle(msg[:data])
|
42
|
+
emit :message, parsed_data
|
43
|
+
emit :event, parsed_data if parsed_data.type == "EVENT"
|
44
|
+
emit :ok, parsed_data if parsed_data.type == "OK"
|
45
|
+
emit :eose, parsed_data if parsed_data.type == "EOSE"
|
46
|
+
emit :closed, parsed_data if parsed_data.type == "CLOSED"
|
47
|
+
emit :notice, parsed_data if parsed_data.type == "NOTICE"
|
48
|
+
when :error
|
49
|
+
emit :error, msg[:message]
|
50
|
+
when :close
|
51
|
+
emit :close, msg[:code], msg[:reason]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def nsec
|
57
|
+
signer.nsec
|
58
|
+
end
|
59
|
+
|
60
|
+
def private_key
|
61
|
+
signer.private_key
|
62
|
+
end
|
63
|
+
|
64
|
+
def npub
|
65
|
+
signer.npub
|
66
|
+
end
|
67
|
+
|
68
|
+
def public_key
|
69
|
+
signer.public_key
|
70
|
+
end
|
71
|
+
|
72
|
+
def sign(event)
|
73
|
+
signer.sign(event)
|
74
|
+
end
|
75
|
+
|
76
|
+
def decrypt(event)
|
77
|
+
signer.decrypt(event)
|
78
|
+
end
|
79
|
+
|
80
|
+
def generate_delegation_tag(to:, conditions:)
|
81
|
+
signer.generate_delegation_tag(to, conditions)
|
82
|
+
end
|
83
|
+
|
84
|
+
def connect(context: @context)
|
85
|
+
@thread = Thread.new do
|
86
|
+
EM.run do
|
87
|
+
@ws_client = Faye::WebSocket::Client.new(@relay)
|
88
|
+
|
89
|
+
@outbound_channel.subscribe { |msg| @ws_client.send(msg) && emit(:send, msg) }
|
90
|
+
|
91
|
+
@ws_client.on :open do
|
92
|
+
@running = true
|
93
|
+
@inbound_channel.push(type: :open, relay: @relay)
|
94
|
+
end
|
95
|
+
|
96
|
+
@ws_client.on :message do |event|
|
97
|
+
@inbound_channel.push(type: :message, data: event.data)
|
98
|
+
end
|
99
|
+
|
100
|
+
@ws_client.on :error do |event|
|
101
|
+
@inbound_channel.push(type: :error, message: event.message)
|
102
|
+
end
|
103
|
+
|
104
|
+
@ws_client.on :close do |event|
|
105
|
+
context.cancel
|
106
|
+
@inbound_channel.push(type: :close, code: event.code, reason: event.reason)
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Wait for the connection to be established or for the context to be canceled
|
113
|
+
if context
|
114
|
+
context.wait { @running }
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
def running?
|
120
|
+
@running
|
121
|
+
end
|
122
|
+
|
123
|
+
def close
|
124
|
+
@running = false
|
125
|
+
EM.next_tick do
|
126
|
+
@ws_client.close if @ws_client
|
127
|
+
EM.add_timer(0.1) do
|
128
|
+
EM.stop if EM.reactor_running?
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def publish(event)
|
134
|
+
return false unless running?
|
135
|
+
@outbound_channel.push(['EVENT', event.to_json].to_json)
|
136
|
+
return true
|
137
|
+
end
|
138
|
+
|
139
|
+
def publish_and_wait(event, context: @context, close_on_finish: false)
|
140
|
+
return false unless running?
|
141
|
+
|
142
|
+
response = nil
|
143
|
+
@outbound_channel.push(['EVENT', event.to_json].to_json)
|
144
|
+
|
145
|
+
response_thread = Thread.new do
|
146
|
+
context.wait do
|
147
|
+
@response_mutex.synchronize do
|
148
|
+
@response_condition.wait(@response_mutex) # Wait for a response
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
@inbound_channel.subscribe do |message|
|
154
|
+
parsed_data = Nostr::MessageHandler.handle(message[:data])
|
155
|
+
if parsed_data.type == "OK" && parsed_data.event_id == event.id
|
156
|
+
response = parsed_data
|
157
|
+
@response_condition.signal
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
response_thread.join
|
162
|
+
close if close_on_finish
|
163
|
+
|
164
|
+
response
|
165
|
+
end
|
166
|
+
|
167
|
+
def subscribe(subscription_id: SecureRandom.hex, filter: Filter.new)
|
168
|
+
@subscriptions[subscription_id] = filter
|
169
|
+
@outbound_channel.push(["REQ", subscription_id, filter.to_h].to_json)
|
170
|
+
@subscriptions[subscription_id]
|
171
|
+
subscription_id
|
172
|
+
end
|
173
|
+
|
174
|
+
def unsubscribe(subscription_id)
|
175
|
+
@subscriptions.delete(subscription_id)
|
176
|
+
@outbound_channel.push(["CLOSE", subscription_id].to_json)
|
177
|
+
end
|
178
|
+
|
179
|
+
def unsubscribe_all
|
180
|
+
@subscriptions.each{|s| unsubscribe(s[0])}
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
end
|
data/lib/context.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
class Context
|
2
|
+
attr_reader :canceled, :timeout
|
3
|
+
|
4
|
+
def initialize(timeout: nil)
|
5
|
+
@timeout = timeout
|
6
|
+
@canceled = false
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@condition = ConditionVariable.new
|
9
|
+
|
10
|
+
# Start a timer if a timeout is specified
|
11
|
+
if @timeout
|
12
|
+
@start_time = Time.now
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def cancel
|
17
|
+
@mutex.synchronize do
|
18
|
+
@canceled = true
|
19
|
+
@condition.broadcast
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def timed_out?
|
24
|
+
return false unless @timeout
|
25
|
+
|
26
|
+
# Check the elapsed time without locking the mutex
|
27
|
+
Time.now - @start_time > @timeout
|
28
|
+
end
|
29
|
+
|
30
|
+
def wait(&block)
|
31
|
+
reset # Reset the context state before waiting
|
32
|
+
loop do
|
33
|
+
break if block.call
|
34
|
+
if timed_out?
|
35
|
+
raise StandardError.new("Operation timed out after #{timeout} seconds")
|
36
|
+
end
|
37
|
+
sleep(0.1) # Sleep briefly to avoid busy-waiting
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def reset
|
42
|
+
@mutex.synchronize do
|
43
|
+
@canceled = false
|
44
|
+
@start_time = Time.now if @timeout
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/lib/event.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
module Nostr
|
2
|
+
class Event
|
3
|
+
include CryptoTools
|
4
|
+
|
5
|
+
ATTRIBUTES = [:kind, :pubkey, :created_at, :tags, :content, :id, :sig, :pow, :delegation, :recipient]
|
6
|
+
|
7
|
+
# Create attr_reader for each attribute name
|
8
|
+
ATTRIBUTES.each do |attribute|
|
9
|
+
attr_reader attribute
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :errors
|
13
|
+
|
14
|
+
class ValidationError < StandardError; end
|
15
|
+
|
16
|
+
def initialize(
|
17
|
+
kind:,
|
18
|
+
pubkey: nil,
|
19
|
+
created_at: nil,
|
20
|
+
tags: [],
|
21
|
+
content: nil,
|
22
|
+
id: nil,
|
23
|
+
sig: nil,
|
24
|
+
pow: nil,
|
25
|
+
delegation: nil,
|
26
|
+
subscription_id: nil
|
27
|
+
)
|
28
|
+
@pubkey = pubkey
|
29
|
+
@created_at = created_at ? created_at : Time.now.utc.to_i
|
30
|
+
@kind = kind
|
31
|
+
@tags = tags
|
32
|
+
@content = content
|
33
|
+
@id = id
|
34
|
+
@sig = sig
|
35
|
+
|
36
|
+
@pow = pow
|
37
|
+
@delegation = delegation
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create setter methods for each attribute name
|
42
|
+
ATTRIBUTES.each do |attribute|
|
43
|
+
define_method("#{attribute}=") do |value|
|
44
|
+
return if instance_variable_get("@#{attribute}") == value
|
45
|
+
instance_variable_set("@#{attribute}", value)
|
46
|
+
reset! unless attribute == :id || attribute == :sig
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def type
|
51
|
+
"EVENT"
|
52
|
+
end
|
53
|
+
|
54
|
+
def content=(content)
|
55
|
+
return if @content == content
|
56
|
+
@content = content
|
57
|
+
reset!
|
58
|
+
end
|
59
|
+
|
60
|
+
def has_tag?(tag)
|
61
|
+
@tags.each_slice(2).any? { |e| e.first == tag }
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_json
|
65
|
+
{
|
66
|
+
'kind': @kind,
|
67
|
+
'pubkey': @pubkey,
|
68
|
+
'created_at': @created_at,
|
69
|
+
'tags': @tags,
|
70
|
+
'content': @content,
|
71
|
+
'id': @id,
|
72
|
+
'sig': @sig,
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def match_pow_difficulty?
|
77
|
+
self.match_pow_difficulty?(@id, pow)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.match_pow_difficulty?(event_id, pow)
|
81
|
+
pow.nil? || pow == [event_id].pack("H*").unpack("B*")[0].index('1')
|
82
|
+
end
|
83
|
+
|
84
|
+
def serialize
|
85
|
+
[
|
86
|
+
0,
|
87
|
+
@pubkey,
|
88
|
+
@created_at,
|
89
|
+
@kind,
|
90
|
+
@tags,
|
91
|
+
@content
|
92
|
+
]
|
93
|
+
end
|
94
|
+
|
95
|
+
def signable?
|
96
|
+
@errors = []
|
97
|
+
|
98
|
+
# Check mandatory fields
|
99
|
+
@errors << "Kind is missing" if @kind.nil?
|
100
|
+
@errors << "Created at is missing" if @created_at.nil?
|
101
|
+
|
102
|
+
# Type validations
|
103
|
+
@errors << "Pubkey must be a string" if @pubkey && !@pubkey.is_a?(String)
|
104
|
+
@errors << "Kind must be an integer" unless @kind.is_a?(Integer)
|
105
|
+
if @created_at
|
106
|
+
# Check if it's a valid Unix timestamp or can be converted to one
|
107
|
+
begin
|
108
|
+
timestamp = if @created_at.is_a?(Time)
|
109
|
+
@created_at.to_i
|
110
|
+
elsif @created_at.is_a?(Integer)
|
111
|
+
@created_at
|
112
|
+
elsif @created_at.respond_to?(:to_time)
|
113
|
+
@created_at.to_time.to_i
|
114
|
+
else
|
115
|
+
raise ArgumentError
|
116
|
+
end
|
117
|
+
|
118
|
+
# Validate timestamp range
|
119
|
+
@errors << "Created at is not a valid timestamp" unless
|
120
|
+
timestamp.is_a?(Integer) &&
|
121
|
+
timestamp >= 0
|
122
|
+
rescue
|
123
|
+
@errors << "Created at must be a valid datetime or Unix timestamp"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
@errors << "Tags must be an array" unless @tags.is_a?(Array)
|
127
|
+
|
128
|
+
@errors << "Content must be a string" if @content && !@content.is_a?(String)
|
129
|
+
@errors << "ID must be a string" if @id && !@id.is_a?(String)
|
130
|
+
@errors << "Signature must be a string" if @sig && !@sig.is_a?(String)
|
131
|
+
@errors << "POW must be an integer" if @pow && !@pow.is_a?(Integer)
|
132
|
+
@errors << "Delegation must be an array" if @delegation && !@delegation.is_a?(Array)
|
133
|
+
|
134
|
+
if @errors.any?
|
135
|
+
raise ValidationError, @errors.join(", ")
|
136
|
+
end
|
137
|
+
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def valid?
|
142
|
+
begin
|
143
|
+
signable?
|
144
|
+
rescue ValidationError => e
|
145
|
+
return false
|
146
|
+
end
|
147
|
+
|
148
|
+
# Additional checks for a valid signed event
|
149
|
+
@errors = []
|
150
|
+
@errors << "ID is missing" if @id.nil?
|
151
|
+
@errors << "Signature is missing" if @sig.nil?
|
152
|
+
@errors << "Pubkey is missing" if @pubkey.nil?
|
153
|
+
|
154
|
+
if @errors.any?
|
155
|
+
raise ValidationError, @errors.join(", ")
|
156
|
+
end
|
157
|
+
|
158
|
+
true
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.from_message(message)
|
162
|
+
subscription_id = message[1]
|
163
|
+
event_data = message[2]
|
164
|
+
|
165
|
+
event = new(
|
166
|
+
subscription_id: subscription_id,
|
167
|
+
kind: event_data["kind"],
|
168
|
+
pubkey: event_data["pubkey"],
|
169
|
+
created_at: event_data["created_at"],
|
170
|
+
tags: event_data["tags"],
|
171
|
+
content: event_data["content"],
|
172
|
+
id: event_data["id"],
|
173
|
+
sig: event_data["sig"],
|
174
|
+
pow: event_data["nonce"]&.last&.to_i
|
175
|
+
)
|
176
|
+
raise ArgumentError, "Event is not valid" unless event.valid?
|
177
|
+
return event
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def reset!
|
183
|
+
@id = nil
|
184
|
+
@sign = nil
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
data/lib/event_wizard.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module EventWizard
|
2
|
+
def initialize_event_emitter
|
3
|
+
@listeners = Hash.new { |hash, key| hash[key] = [] }
|
4
|
+
end
|
5
|
+
|
6
|
+
def on(event, &callback)
|
7
|
+
# Prevent adding the same callback multiple times
|
8
|
+
unless @listeners[event].include?(callback)
|
9
|
+
@listeners[event] << callback
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def emit(event, *args)
|
14
|
+
@listeners[event].each { |callback| callback.call(*args) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def off(event, callback)
|
18
|
+
return unless @listeners[event]
|
19
|
+
@listeners[event].delete(callback)
|
20
|
+
end
|
21
|
+
|
22
|
+
def replace(event, old_callback, new_callback)
|
23
|
+
return unless @listeners[event]
|
24
|
+
index = @listeners[event].index(old_callback)
|
25
|
+
@listeners[event][index] = new_callback if index
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear(event)
|
29
|
+
@listeners.delete(event)
|
30
|
+
end
|
31
|
+
end
|
data/lib/filter.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Nostr
|
2
|
+
class Filter
|
3
|
+
|
4
|
+
attr_reader :id
|
5
|
+
attr_reader :kinds
|
6
|
+
attr_reader :authors
|
7
|
+
attr_reader :tags
|
8
|
+
attr_reader :since
|
9
|
+
attr_reader :until
|
10
|
+
attr_reader :limit
|
11
|
+
attr_reader :search
|
12
|
+
('a'..'z').each { |char| attr_reader char.to_sym }
|
13
|
+
('A'..'Z').each { |char| attr_reader char.to_sym }
|
14
|
+
|
15
|
+
def initialize(ids: nil, kinds: nil, authors: nil, since: nil, limit: nil, search: nil, **params)
|
16
|
+
@id = id
|
17
|
+
@kinds = kinds
|
18
|
+
@authors = authors
|
19
|
+
@tags = tags
|
20
|
+
@since = since.nil? ? nil : since.to_i
|
21
|
+
@limit = limit
|
22
|
+
@search = search
|
23
|
+
@until = params[:until].nil? ? nil : params[:until].to_i # this is an hack to permit the use of the 'until' param, since it is a reserved word
|
24
|
+
|
25
|
+
# Handle additional parameters with a-zA-Z names
|
26
|
+
params.each do |key, value|
|
27
|
+
if key.to_s.match?(/\A[a-zA-Z]\z/)
|
28
|
+
instance_variable_set("@#{key}", value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_h
|
34
|
+
result = {
|
35
|
+
id: @id,
|
36
|
+
authors: @authors,
|
37
|
+
kinds: @kinds,
|
38
|
+
since: @since,
|
39
|
+
until: @until,
|
40
|
+
limit: @limit,
|
41
|
+
search: @search
|
42
|
+
}.compact
|
43
|
+
|
44
|
+
('a'..'z').each do |char|
|
45
|
+
var_value = instance_variable_get("@#{char}")
|
46
|
+
result["##{char}"] = var_value unless var_value.nil?
|
47
|
+
end
|
48
|
+
('A'..'Z').each do |char|
|
49
|
+
var_value = instance_variable_get("@#{char}")
|
50
|
+
result["##{char}"] = var_value unless var_value.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
data/lib/key.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Nostr
|
2
|
+
class Key
|
3
|
+
|
4
|
+
def self.generate_private_key
|
5
|
+
group = ECDSA::Group::Secp256k1
|
6
|
+
(1 + SecureRandom.random_number(group.order - 1)).to_s(16).rjust(64, '0')
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.get_public_key(private_key)
|
10
|
+
group = ECDSA::Group::Secp256k1
|
11
|
+
group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
data/lib/kind.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
module Nostr
|
2
|
+
class MessageHandler
|
3
|
+
def self.handle(message)
|
4
|
+
|
5
|
+
message = JSON.parse(message) rescue ["?", message]
|
6
|
+
type = message[0]
|
7
|
+
strategy_class = case type
|
8
|
+
when 'EVENT' then EventMessageStrategy
|
9
|
+
when 'OK' then OkMessageStrategy
|
10
|
+
when 'EOSE' then EoseMessageStrategy
|
11
|
+
when 'CLOSED' then ClosedMessageStrategy
|
12
|
+
when 'NOTICE' then NoticeMessageStrategy
|
13
|
+
else UnknownMessageStrategy
|
14
|
+
end
|
15
|
+
|
16
|
+
processed_data = strategy_class.new(message).process
|
17
|
+
type == "EVENT" ? processed_data : ParsedData.new(processed_data)
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class BaseMessageStrategy
|
23
|
+
def initialize(message)
|
24
|
+
@message = message
|
25
|
+
end
|
26
|
+
|
27
|
+
def process
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class EventMessageStrategy < BaseMessageStrategy
|
33
|
+
def process
|
34
|
+
Event.from_message(@message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class OkMessageStrategy < BaseMessageStrategy
|
39
|
+
def process
|
40
|
+
{
|
41
|
+
type: 'OK',
|
42
|
+
event_id: @message[1],
|
43
|
+
success: @message[2],
|
44
|
+
message: @message[3]
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class EoseMessageStrategy < BaseMessageStrategy
|
50
|
+
def process
|
51
|
+
{
|
52
|
+
type: 'EOSE',
|
53
|
+
subscription_id: @message[1]
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class ClosedMessageStrategy < BaseMessageStrategy
|
59
|
+
def process
|
60
|
+
{
|
61
|
+
type: 'CLOSED',
|
62
|
+
subscription_id: @message[1],
|
63
|
+
reason: @message[2]
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class NoticeMessageStrategy < BaseMessageStrategy
|
69
|
+
def process
|
70
|
+
{
|
71
|
+
type: 'NOTICE',
|
72
|
+
message: @message[1]
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class UnknownMessageStrategy < BaseMessageStrategy
|
78
|
+
def process
|
79
|
+
{
|
80
|
+
type: 'UNKNOWN',
|
81
|
+
raw_message: @message
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class ParsedData
|
88
|
+
def initialize(data)
|
89
|
+
@data = data
|
90
|
+
end
|
91
|
+
|
92
|
+
def type
|
93
|
+
@data[:type]
|
94
|
+
end
|
95
|
+
|
96
|
+
def method_missing(method_name, *args, &block)
|
97
|
+
if @data.key?(method_name)
|
98
|
+
@data[method_name]
|
99
|
+
else
|
100
|
+
super
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def respond_to_missing?(method_name, include_private = false)
|
105
|
+
@data.key?(method_name) || super
|
106
|
+
end
|
107
|
+
end
|