chatx 0.0.0.pre.pre3
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 +7 -0
- data/.gitignore +12 -0
- data/.gitlab-ci.yml +27 -0
- data/.rubocop.yml +38 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +3 -0
- data/Guardfile +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/chatx.gemspec +52 -0
- data/lib/chatx.rb +271 -0
- data/lib/chatx/auth.rb +38 -0
- data/lib/chatx/events.rb +195 -0
- data/lib/chatx/hooks.rb +111 -0
- data/lib/chatx/models/helpers.rb +36 -0
- data/lib/chatx/models/message.rb +68 -0
- data/lib/chatx/models/room.rb +56 -0
- data/lib/chatx/models/user.rb +51 -0
- data/lib/chatx/polling.rb +46 -0
- data/lib/chatx/version.rb +3 -0
- data/lib/chatx/websocket.rb +76 -0
- metadata +252 -0
data/lib/chatx.rb
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
require "mechanize"
|
2
|
+
require "nokogiri"
|
3
|
+
require "websocket/driver"
|
4
|
+
require "json"
|
5
|
+
require "permessage_deflate"
|
6
|
+
require "open-uri"
|
7
|
+
require_relative "chatx/events"
|
8
|
+
require_relative "chatx/auth"
|
9
|
+
require_relative "chatx/websocket"
|
10
|
+
require_relative "chatx/hooks"
|
11
|
+
require_relative "chatx/polling"
|
12
|
+
|
13
|
+
require "thwait"
|
14
|
+
|
15
|
+
Thread.abort_on_exception = true
|
16
|
+
|
17
|
+
class ChatBot
|
18
|
+
# @!attribute [r] rooms
|
19
|
+
# @return [Hash<Hash<Array>>] a hash of the rooms. Each key is a room ID, and
|
20
|
+
# each value is futher hash with an :events key which contains an array
|
21
|
+
# of {Event}s from that room.
|
22
|
+
# @!attribute [r] websocket
|
23
|
+
# @return [Hash<room_id, Thread>] a hash of websockets. Each key is a room ID and
|
24
|
+
# each value is a websocket thread. Each websocket gets it's own thead because
|
25
|
+
# EventMachine blocks the main thread when I run it there.
|
26
|
+
# @!attribute [rw] default_server
|
27
|
+
# @return [String] The default server to connect to. It's good to
|
28
|
+
# pass this to the default_server keyword argument of the {initialize} method
|
29
|
+
# if you're only sticking to one server. Otherwise, with every {#say}
|
30
|
+
# you'll have to specify the server. This defaults to stackexchange. The options for
|
31
|
+
# this are "meta.stackexchange", "stackexchange", and "stackoverflow". The rest of
|
32
|
+
# the URL is managed internally.
|
33
|
+
# @!attribute [rw] hooks
|
34
|
+
# @return [Array] An array of the {Hook}s for the bot.
|
35
|
+
attr_reader :rooms, :websockets, :logger
|
36
|
+
attr_accessor :default_server, :hooks
|
37
|
+
|
38
|
+
# Creates a bot.
|
39
|
+
#
|
40
|
+
# These must be stack exchange openid credentials, and the user
|
41
|
+
# must have spoken in a room on that server first.
|
42
|
+
#
|
43
|
+
# For further details on authentication, see #authenticate
|
44
|
+
#
|
45
|
+
# @param [String] email The Stack Exchange OpenID email
|
46
|
+
# @param [String] password The Stack Exchange OpenID password
|
47
|
+
# @return [ChatBot] A new bot instance. Not sure what happens if
|
48
|
+
# you try and run more than one at a time...
|
49
|
+
def initialize(email, password, **opts)
|
50
|
+
opts[:default_server] ||= 'stackexchange'
|
51
|
+
opts[:log_location] ||= STDOUT
|
52
|
+
opts[:log_level] ||= Logger::DEBUG
|
53
|
+
|
54
|
+
@logger = Logger.new opts[:log_location]
|
55
|
+
@logger.level = opts[:log_level]
|
56
|
+
|
57
|
+
@ws_json_logger = Logger.new 'websockets_json.log'
|
58
|
+
|
59
|
+
@email = email
|
60
|
+
@password = password
|
61
|
+
@agent = Mechanize.new
|
62
|
+
@rooms = {} # room_id => {events}
|
63
|
+
@default_server = opts[:default_server]
|
64
|
+
@hooks = {}
|
65
|
+
@websockets = {}
|
66
|
+
at_exit { rejoin }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Logs the bot into the three SE chat servers.
|
70
|
+
#
|
71
|
+
# @return [Boolean] A bool indicating the result of authentication: true if all three
|
72
|
+
# servers were authenticated successfully, false otherwise.
|
73
|
+
def login(servers = @default_server)
|
74
|
+
servers = [servers] unless servers.is_a? Array
|
75
|
+
authenticate(servers)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Attempts to join a room, and for every room joined opens a websocket.
|
79
|
+
# Websockets seem to be the way to show your presence in a room. It's weird
|
80
|
+
# that way.
|
81
|
+
#
|
82
|
+
# Each websocket is added to the @websockets instance variable which can be
|
83
|
+
# read but not written to.
|
84
|
+
#
|
85
|
+
# @param room_id [#to_i] A valid room ID on the server designated by the
|
86
|
+
# server param.
|
87
|
+
# @keyword server [String] A string referring to the relevant server. The
|
88
|
+
# default value is set by the @default_server instance variable.
|
89
|
+
# @return [Hash] The hash of currently active websockets.
|
90
|
+
def join_room(room_id, server: @default_server)
|
91
|
+
@logger.info "Joining #{room_id}"
|
92
|
+
|
93
|
+
fkey = get_fkey(server, "rooms/#{room_id}")
|
94
|
+
|
95
|
+
@agent.get("https://chat.#{server}.com/rooms/#{room_id}", fkey: fkey)
|
96
|
+
|
97
|
+
events_json = @agent.post("https://chat.#{server}.com/chats/#{room_id}/events",
|
98
|
+
fkey: fkey,
|
99
|
+
since: 0,
|
100
|
+
mode: "Messages",
|
101
|
+
msgCount: 100).body
|
102
|
+
|
103
|
+
events = JSON.parse(events_json)["events"]
|
104
|
+
|
105
|
+
@logger.info "Retrieved events (length #{events.length})"
|
106
|
+
|
107
|
+
ws_auth_data = @agent.post("https://chat.#{server}.com/ws-auth",
|
108
|
+
roomid: room_id,
|
109
|
+
fkey: fkey)
|
110
|
+
|
111
|
+
@logger.info "Began room auth for room id: #{room_id}"
|
112
|
+
|
113
|
+
@rooms[room_id.to_i] = {}
|
114
|
+
@rooms[room_id.to_i][:events] = events.map do |e|
|
115
|
+
begin
|
116
|
+
ChatX::Event.new e, server, self
|
117
|
+
rescue ChatX::InitializationDataException => e
|
118
|
+
@logger.warn "Caught InitializationDataException during events population (#{e}); skipping event"
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
end.compact
|
122
|
+
|
123
|
+
@logger.info "Rooms: #{@rooms.keys}"
|
124
|
+
|
125
|
+
unless @websockets[server].nil?
|
126
|
+
@websockets[server].driver.close
|
127
|
+
@logger.info "Websocket #{@websockets[server]} already open; clearing."
|
128
|
+
@websockets[server] = nil
|
129
|
+
end
|
130
|
+
ws_uri = JSON.parse(ws_auth_data.body)["url"]
|
131
|
+
last_event_time = events.max_by { |event| event['time_stamp'] }['time_stamp']
|
132
|
+
cookies = (@agent.cookies.map { |cookie| "#{cookie.name}=#{cookie.value}" if cookie.domain == "chat.#{server}.com" || cookie.domain == "#{server}.com" } - [nil]).join("; ")
|
133
|
+
@websockets[server] = WSClient.new("#{ws_uri}?l=#{last_event_time}", cookies, self, server)
|
134
|
+
@logger.info "New websocket open (#{@websockets[server]}"
|
135
|
+
end
|
136
|
+
|
137
|
+
# Leaves the room. Not much else to say...
|
138
|
+
# @param room_id [#to_i] The ID of the room to leave
|
139
|
+
# @keyword server [String] The chat server that room is on.
|
140
|
+
# @return A meaningless value
|
141
|
+
def leave_room(room_id, server: @default_server)
|
142
|
+
fkey = get_fkey("stackexchange", "/rooms/#{room_id}")
|
143
|
+
@rooms.delete(room_id) unless @rooms[room_id].nil?
|
144
|
+
@agent.post("https://chat.#{server}.com/chats/leave/#{room_id}", fkey: fkey, quiet: "true")
|
145
|
+
end
|
146
|
+
|
147
|
+
# Speaks in a room! Not much to say here, but much to say in the room
|
148
|
+
# that is passed!
|
149
|
+
#
|
150
|
+
# If you're trying to reply to a message, please use the {Message#reply}
|
151
|
+
# method.
|
152
|
+
# @param room_id [#to_i] The ID of the room to be spoken in
|
153
|
+
# @param content [String] The text of message to send
|
154
|
+
# @keyword server [String] The server to send the messon on.
|
155
|
+
# @return A meaningless value
|
156
|
+
def say(content, room_id, server: @default_server)
|
157
|
+
fkey = get_fkey(server, "/rooms/#{room_id}")
|
158
|
+
@logger.warn "Message too long, truncating each line to 500 chars: #{content}" if content.to_s.split("\n").any? { |line| line.length > 500 }
|
159
|
+
if content.to_s.empty?
|
160
|
+
@logger.warn "Message is empty, not posting: '#{content}'"
|
161
|
+
return
|
162
|
+
end
|
163
|
+
begin
|
164
|
+
resp = @agent.post("https://chat.#{server}.com/chats/#{room_id}/messages/new", fkey: fkey, text: content.to_s.split("\n").map { |line| line[0...500] }.join("\n"))
|
165
|
+
rescue Mechanize::ResponseCodeError
|
166
|
+
@logger.error "Posting message to room #{room_id} failed. Retrying... #{content.to_s.split("\n").map { |line| line[0...500] }.join("\n")}"
|
167
|
+
sleep 0.3 # A magic number I just chose for no reason
|
168
|
+
retry
|
169
|
+
end
|
170
|
+
return JSON.parse(resp.content)["id"].to_i
|
171
|
+
end
|
172
|
+
|
173
|
+
def toggle_star(message_id, server: @default_server)
|
174
|
+
fkey = get_fkey("stackexchange")
|
175
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}/star", fkey: fkey)
|
176
|
+
end
|
177
|
+
|
178
|
+
def star_count(message_id, server: @default_server)
|
179
|
+
page = @agent.get("https://chat.#{server}.com/transcript/message/#{message_id}")
|
180
|
+
page.css("#message-#{message_id} .flash .star .times")[0].content.to_i
|
181
|
+
end
|
182
|
+
|
183
|
+
def star(message_id, server: @default_server)
|
184
|
+
fkey = get_fkey("stackexchange")
|
185
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}/star", fkey: fkey) unless starred?(message_id)
|
186
|
+
end
|
187
|
+
|
188
|
+
def unstar(message_id, server: @default_server)
|
189
|
+
fkey = get_fkey("stackexchange")
|
190
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}/star", fkey: fkey) if starred?(message_id)
|
191
|
+
end
|
192
|
+
|
193
|
+
def starred?(message_id, server: @default_server)
|
194
|
+
page = @agent.get("https://chat.#{server}.com/transcript/message/#{message_id}")
|
195
|
+
return false if page.css("#message-#{message_id} .flash .stars")[0].nil?
|
196
|
+
page.css("#message-#{message_id} .flash .stars")[0].attr("class").include?("user-star")
|
197
|
+
end
|
198
|
+
|
199
|
+
def cancel_stars(message_id, server: @default_server)
|
200
|
+
fkey = get_fkey("stackexchange")
|
201
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}/unstar", fkey: fkey)
|
202
|
+
end
|
203
|
+
|
204
|
+
def delete(message_id, server: @default_server)
|
205
|
+
fkey = get_fkey("stackexchange")
|
206
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}/delete", fkey: fkey)
|
207
|
+
end
|
208
|
+
|
209
|
+
def edit(message_id, new_message, server: @default_server)
|
210
|
+
fkey = get_fkey("stackexchange")
|
211
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}", fkey: fkey, text: new_message)
|
212
|
+
end
|
213
|
+
|
214
|
+
def toggle_pin(message_id, server: @default_server)
|
215
|
+
fkey = get_fkey("stackexchange")
|
216
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}/owner-star", fkey: fkey)
|
217
|
+
end
|
218
|
+
|
219
|
+
def pin(message_id, server: @default_server)
|
220
|
+
fkey = get_fkey("stackexchange")
|
221
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}/owner-star", fkey: fkey)
|
222
|
+
end
|
223
|
+
|
224
|
+
def unpin(message_id, server: @default_server)
|
225
|
+
fkey = get_fkey("stackexchange")
|
226
|
+
@agent.post("https://chat.#{server}.com/messages/#{message_id}/owner-star", fkey: fkey)
|
227
|
+
end
|
228
|
+
|
229
|
+
def pinned?(message_id, server: @default_server)
|
230
|
+
page = @agent.get("https://chat.#{server}.com/transcript/message/#{message_id}")
|
231
|
+
return false if page.css("#message-#{message_id} .flash .stars")[0].nil?
|
232
|
+
page.css("#message-#{message_id} .flash .stars")[0].attr("class").include?("owner-star")
|
233
|
+
end
|
234
|
+
|
235
|
+
# Kills all active websockets for the bot.
|
236
|
+
def kill
|
237
|
+
@websockets.values.each(&:close)
|
238
|
+
end
|
239
|
+
|
240
|
+
def rejoin
|
241
|
+
ThreadsWait.all_waits @websockets.values.map(&:thread)
|
242
|
+
leave_all_rooms
|
243
|
+
end
|
244
|
+
|
245
|
+
def stop
|
246
|
+
leave_all_rooms
|
247
|
+
@websockets.values.map(&:thread).each(&:kill)
|
248
|
+
end
|
249
|
+
|
250
|
+
def leave_all_rooms
|
251
|
+
@rooms.each_key do |room_id|
|
252
|
+
leave_room(room_id)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def current_rooms
|
257
|
+
@websockets.map do |server, ws|
|
258
|
+
[server, ws.in_rooms[:rooms]]
|
259
|
+
end.to_h
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
def get_fkey(server, uri = "")
|
265
|
+
@agent.get("https://chat.#{server}.com/#{uri}").search("//input[@name='fkey']").attribute("value")
|
266
|
+
rescue Mechanize::ResponseCodeError
|
267
|
+
@logger.error "Getting fkey failed for uri https://chat.#{server}.com/#{uri}. Retrying..."
|
268
|
+
sleep 1 # A magic number I just chose for no reason
|
269
|
+
retry
|
270
|
+
end
|
271
|
+
end
|
data/lib/chatx/auth.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
class ChatBot
|
2
|
+
def authenticate(sites = ["stackexchange"])
|
3
|
+
sites = [sites] unless sites.is_a?(Array)
|
4
|
+
|
5
|
+
openid = @agent.get "https://openid.stackexchange.com/account/login"
|
6
|
+
fkey_input = openid.search "//input[@name='fkey']"
|
7
|
+
fkey = fkey_input.empty? ? "" : fkey_input.attribute("value")
|
8
|
+
|
9
|
+
@agent.post("https://openid.stackexchange.com/account/login/submit",
|
10
|
+
fkey: fkey,
|
11
|
+
email: @email,
|
12
|
+
password: @password)
|
13
|
+
|
14
|
+
auth_results = sites.map { |s| site_auth(s) }
|
15
|
+
failed = auth_results.any?(&:!)
|
16
|
+
!failed
|
17
|
+
end
|
18
|
+
|
19
|
+
def site_auth(site)
|
20
|
+
# Get fkey
|
21
|
+
login_page = @agent.get "https://#{site}.com/users/login"
|
22
|
+
fkey_input = login_page.search "//input[@name='fkey']"
|
23
|
+
fkey = fkey_input.attribute('value')
|
24
|
+
|
25
|
+
@agent.post("https://#{site}.com/users/authenticate",
|
26
|
+
fkey: fkey,
|
27
|
+
openid_identifier: 'https://openid.stackexchange.com')
|
28
|
+
|
29
|
+
home = @agent.get "https://chat.#{site}.com"
|
30
|
+
if home.search(".topbar-links span.topbar-menu-links a").first.text.casecmp('log in').zero?
|
31
|
+
@logger.warn "Login to #{site} failed :("
|
32
|
+
false
|
33
|
+
else
|
34
|
+
@logger.info "Login to #{site} successful!"
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/chatx/events.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
require_relative 'models/message'
|
2
|
+
require_relative 'models/room'
|
3
|
+
require_relative 'models/user'
|
4
|
+
|
5
|
+
EVENT = %w[
|
6
|
+
Message Posted
|
7
|
+
Message Edited
|
8
|
+
User Entered
|
9
|
+
User Left
|
10
|
+
Room Name Changed
|
11
|
+
Message Starred
|
12
|
+
Debug Message
|
13
|
+
User Mentioned
|
14
|
+
Message Flagged
|
15
|
+
Message Deleted
|
16
|
+
File Added
|
17
|
+
Moderator Flag
|
18
|
+
User Settings Changed
|
19
|
+
Global Notification
|
20
|
+
Access Level Changed
|
21
|
+
User Notification
|
22
|
+
Invitation
|
23
|
+
Message Reply
|
24
|
+
Message Moved Out
|
25
|
+
Message Moved In
|
26
|
+
Time Break
|
27
|
+
Feed Ticker
|
28
|
+
User Suspended
|
29
|
+
User Merged
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
EVENT_SHORTHAND = %w[
|
33
|
+
message
|
34
|
+
edit
|
35
|
+
entrance
|
36
|
+
exit
|
37
|
+
rename
|
38
|
+
star
|
39
|
+
debug
|
40
|
+
mention
|
41
|
+
flag
|
42
|
+
delete
|
43
|
+
file
|
44
|
+
mod-flag
|
45
|
+
settings
|
46
|
+
gnotif
|
47
|
+
level
|
48
|
+
lnotif
|
49
|
+
invite
|
50
|
+
reply
|
51
|
+
move-out
|
52
|
+
move-in
|
53
|
+
time
|
54
|
+
feed
|
55
|
+
suspended
|
56
|
+
merge
|
57
|
+
].freeze
|
58
|
+
|
59
|
+
module ChatX
|
60
|
+
class Event
|
61
|
+
EVENT_CLASSES = {
|
62
|
+
1 => {
|
63
|
+
ChatX::Message => {
|
64
|
+
time_stamp: 'time_stamp',
|
65
|
+
content: 'content',
|
66
|
+
room_id: 'room_id',
|
67
|
+
user_id: 'user_id',
|
68
|
+
message_id: 'message_id'
|
69
|
+
}
|
70
|
+
},
|
71
|
+
2 => {
|
72
|
+
ChatX::Message => {
|
73
|
+
time_stamp: 'time_stamp',
|
74
|
+
content: 'content',
|
75
|
+
room_id: 'room_id',
|
76
|
+
user_id: 'user_id',
|
77
|
+
message_id: 'message_id'
|
78
|
+
}
|
79
|
+
},
|
80
|
+
3 => {
|
81
|
+
ChatX::User => {
|
82
|
+
user_id: 'target_user_id'
|
83
|
+
},
|
84
|
+
ChatX::Room => {
|
85
|
+
room_id: 'room_id'
|
86
|
+
}
|
87
|
+
},
|
88
|
+
4 => {
|
89
|
+
ChatX::User => {
|
90
|
+
user_id: 'target_user_id'
|
91
|
+
},
|
92
|
+
ChatX::Room => {
|
93
|
+
room_id: 'room_id'
|
94
|
+
}
|
95
|
+
},
|
96
|
+
5 => {
|
97
|
+
ChatX::Room => {
|
98
|
+
room_id: 'room_id'
|
99
|
+
}
|
100
|
+
},
|
101
|
+
# Note: A star/unstar/pin/unpin event DOES NOT have a user_id, which throws an error in ChatX::Message intialization.
|
102
|
+
# 6 => {
|
103
|
+
# ChatX::Message => {
|
104
|
+
# time_stamp: 'time_stamp',
|
105
|
+
# content: 'content',
|
106
|
+
# room_id: 'room_id',
|
107
|
+
# user_id: 'user_id',
|
108
|
+
# message_id: 'message_id'
|
109
|
+
# }
|
110
|
+
# },
|
111
|
+
8 => {
|
112
|
+
ChatX::Message => {
|
113
|
+
time_stamp: 'time_stamp',
|
114
|
+
content: 'content',
|
115
|
+
room_id: 'room_id',
|
116
|
+
user_id: 'user_id',
|
117
|
+
message_id: 'message_id'
|
118
|
+
},
|
119
|
+
[ChatX::User, 'source_user'] => {
|
120
|
+
user_id: 'user_id'
|
121
|
+
},
|
122
|
+
[ChatX::User, 'target_user'] => {
|
123
|
+
user_id: 'target_user_id'
|
124
|
+
}
|
125
|
+
},
|
126
|
+
18 => {
|
127
|
+
ChatX::Message => {
|
128
|
+
time_stamp: 'time_stamp',
|
129
|
+
content: 'content',
|
130
|
+
room_id: 'room_id',
|
131
|
+
user_id: 'user_id',
|
132
|
+
message_id: 'message_id'
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}.freeze
|
136
|
+
|
137
|
+
attr_reader :type, :type_long, :type_short, :hash
|
138
|
+
|
139
|
+
def initialize(event_hash, server, bot)
|
140
|
+
@type = event_hash['event_type'].to_i
|
141
|
+
@type_short = EVENT_SHORTHAND[@type - 1]
|
142
|
+
@type_long = EVENT[@type - 1]
|
143
|
+
@bot = bot
|
144
|
+
|
145
|
+
@etypes = []
|
146
|
+
|
147
|
+
@hash = event_hash # .delete("event_type")
|
148
|
+
|
149
|
+
# rubocop:disable Style/GuardClause
|
150
|
+
if EVENT_CLASSES.include? @type
|
151
|
+
classes = EVENT_CLASSES[@type]
|
152
|
+
classes.each do |c, i|
|
153
|
+
if c.class == Array
|
154
|
+
clazz = c[0]
|
155
|
+
method_name = c[1]
|
156
|
+
else
|
157
|
+
clazz = c
|
158
|
+
method_name = clazz.to_s.split('::').last.downcase
|
159
|
+
end
|
160
|
+
init_properties = i.map { |k, v| [k, event_hash[v]] }.to_h
|
161
|
+
# This bit is fun. It creates a ChatX::<event type> and uses the method_missing
|
162
|
+
# to make its methods all aliased to this ChatX::Event, and also defines an
|
163
|
+
# <event type> method on this ChatX::Event which returns the ChatX::<event type>
|
164
|
+
# object.
|
165
|
+
event_type_obj = clazz.new(server, **init_properties)
|
166
|
+
@etypes.push event_type_obj
|
167
|
+
instance_variable_set "@#{method_name}", event_type_obj
|
168
|
+
self.class.send(:define_method, method_name) do
|
169
|
+
instance_variable_get "@#{method_name}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
# rubocop:enable Style/GuardClause
|
174
|
+
|
175
|
+
def method_missing(m, *args, **opts, &block)
|
176
|
+
etypes = @etypes.select { |e| e.respond_to? m }
|
177
|
+
super if etypes.empty?
|
178
|
+
etype = etypes.first
|
179
|
+
if etype.respond_to? m
|
180
|
+
meth = etype.method(m)
|
181
|
+
# Because it treats **opts as 1 argument
|
182
|
+
if opts.empty?
|
183
|
+
meth.call(*args, &block)
|
184
|
+
else
|
185
|
+
meth.call(*args, **opts, &block)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def respond_to_missing?(m, *)
|
191
|
+
!@etypes.select { |e| e.respond_to? m }.empty? || super
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|