rita 0.1.0 → 5.0.0.alpha.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +19 -0
- data/.rubocop.yml +51 -0
- data/.ruby-version +1 -0
- data/.travis.yml +16 -0
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +5 -0
- data/{LICENSE.txt → LICENSE} +1 -3
- data/README.md +17 -24
- data/Rakefile +5 -5
- data/bin/lita +7 -0
- data/lib/lita/adapter.rb +147 -0
- data/lib/lita/adapters/shell.rb +126 -0
- data/lib/lita/adapters/test.rb +62 -0
- data/lib/lita/authorization.rb +112 -0
- data/lib/lita/callback.rb +39 -0
- data/lib/lita/cli.rb +218 -0
- data/lib/lita/configurable.rb +47 -0
- data/lib/lita/configuration_builder.rb +247 -0
- data/lib/lita/configuration_validator.rb +95 -0
- data/lib/lita/default_configuration.rb +122 -0
- data/lib/lita/errors.rb +25 -0
- data/lib/lita/handler/chat_router.rb +141 -0
- data/lib/lita/handler/common.rb +208 -0
- data/lib/lita/handler/event_router.rb +84 -0
- data/lib/lita/handler/http_router.rb +31 -0
- data/lib/lita/handler.rb +15 -0
- data/lib/lita/handlers/authorization.rb +129 -0
- data/lib/lita/handlers/help.rb +171 -0
- data/lib/lita/handlers/info.rb +66 -0
- data/lib/lita/handlers/room.rb +36 -0
- data/lib/lita/handlers/users.rb +37 -0
- data/lib/lita/http_callback.rb +46 -0
- data/lib/lita/http_route.rb +83 -0
- data/lib/lita/logger.rb +43 -0
- data/lib/lita/message.rb +124 -0
- data/lib/lita/middleware_registry.rb +39 -0
- data/lib/lita/namespace.rb +29 -0
- data/lib/lita/plugin_builder.rb +43 -0
- data/lib/lita/rack_app.rb +100 -0
- data/lib/lita/registry.rb +164 -0
- data/lib/lita/response.rb +65 -0
- data/lib/lita/robot.rb +273 -0
- data/lib/lita/room.rb +119 -0
- data/lib/lita/route_validator.rb +82 -0
- data/lib/lita/rspec/handler.rb +127 -0
- data/lib/lita/rspec/matchers/chat_route_matcher.rb +53 -0
- data/lib/lita/rspec/matchers/event_route_matcher.rb +29 -0
- data/lib/lita/rspec/matchers/http_route_matcher.rb +34 -0
- data/lib/lita/rspec.rb +48 -0
- data/lib/lita/source.rb +81 -0
- data/lib/lita/store.rb +23 -0
- data/lib/lita/target.rb +3 -0
- data/lib/lita/template.rb +71 -0
- data/lib/lita/template_resolver.rb +52 -0
- data/lib/lita/timer.rb +49 -0
- data/lib/lita/user.rb +157 -0
- data/lib/lita/util.rb +31 -0
- data/lib/lita/version.rb +6 -0
- data/lib/lita.rb +166 -0
- data/lib/rita.rb +2 -7
- data/rita.gemspec +50 -0
- data/spec/lita/adapter_spec.rb +54 -0
- data/spec/lita/adapters/shell_spec.rb +99 -0
- data/spec/lita/authorization_spec.rb +122 -0
- data/spec/lita/configuration_builder_spec.rb +247 -0
- data/spec/lita/configuration_validator_spec.rb +114 -0
- data/spec/lita/default_configuration_spec.rb +242 -0
- data/spec/lita/handler/chat_router_spec.rb +236 -0
- data/spec/lita/handler/common_spec.rb +289 -0
- data/spec/lita/handler/event_router_spec.rb +121 -0
- data/spec/lita/handler/http_router_spec.rb +155 -0
- data/spec/lita/handler_spec.rb +62 -0
- data/spec/lita/handlers/authorization_spec.rb +111 -0
- data/spec/lita/handlers/help_spec.rb +124 -0
- data/spec/lita/handlers/info_spec.rb +67 -0
- data/spec/lita/handlers/room_spec.rb +24 -0
- data/spec/lita/handlers/users_spec.rb +35 -0
- data/spec/lita/logger_spec.rb +28 -0
- data/spec/lita/message_spec.rb +178 -0
- data/spec/lita/plugin_builder_spec.rb +41 -0
- data/spec/lita/response_spec.rb +62 -0
- data/spec/lita/robot_spec.rb +285 -0
- data/spec/lita/room_spec.rb +136 -0
- data/spec/lita/rspec/handler_spec.rb +33 -0
- data/spec/lita/rspec_spec.rb +162 -0
- data/spec/lita/source_spec.rb +68 -0
- data/spec/lita/store_spec.rb +23 -0
- data/spec/lita/template_resolver_spec.rb +42 -0
- data/spec/lita/template_spec.rb +52 -0
- data/spec/lita/timer_spec.rb +32 -0
- data/spec/lita/user_spec.rb +167 -0
- data/spec/lita/util_spec.rb +18 -0
- data/spec/lita_spec.rb +227 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/templates/basic.erb +1 -0
- data/spec/templates/basic.irc.erb +1 -0
- data/spec/templates/helpers.erb +1 -0
- data/spec/templates/interpolated.erb +1 -0
- data/templates/locales/en.yml +137 -0
- data/templates/plugin/Gemfile +5 -0
- data/templates/plugin/README.tt +29 -0
- data/templates/plugin/Rakefile +8 -0
- data/templates/plugin/gemspec.tt +27 -0
- data/templates/plugin/gitignore +18 -0
- data/templates/plugin/lib/lita/plugin_type/plugin.tt +19 -0
- data/templates/plugin/lib/plugin.tt +16 -0
- data/templates/plugin/locales/en.yml.tt +4 -0
- data/templates/plugin/spec/lita/plugin_type/plugin_spec.tt +6 -0
- data/templates/plugin/spec/spec_helper.tt +8 -0
- data/templates/plugin/templates/gitkeep +0 -0
- data/templates/robot/Gemfile +5 -0
- data/templates/robot/lita_config.rb +28 -0
- metadata +386 -20
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -132
- data/lib/rita/version.rb +0 -5
- data/sig/rita.rbs +0 -4
data/lib/lita/robot.rb
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require "i18n"
|
5
|
+
|
6
|
+
require "puma"
|
7
|
+
|
8
|
+
require_relative "authorization"
|
9
|
+
require_relative "rack_app"
|
10
|
+
require_relative "room"
|
11
|
+
require_relative "store"
|
12
|
+
|
13
|
+
module Lita
|
14
|
+
# The main object representing a running instance of Lita. Provides a high
|
15
|
+
# level API for the underlying adapter. Dispatches incoming messages to
|
16
|
+
# registered handlers. Can send outgoing chat messages and set the topic
|
17
|
+
# of chat rooms.
|
18
|
+
class Robot
|
19
|
+
extend Forwardable
|
20
|
+
|
21
|
+
# A +Rack+ application used for the built-in web server.
|
22
|
+
# @return [Rack::Builder] The +Rack+ app.
|
23
|
+
attr_reader :app
|
24
|
+
|
25
|
+
# The {Authorization} object for the currently running robot.
|
26
|
+
# @return [Authorization] The authorization object.
|
27
|
+
# @since 4.0.0
|
28
|
+
attr_reader :auth
|
29
|
+
|
30
|
+
# The name the robot will look for in incoming messages to determine if it's
|
31
|
+
# being addressed.
|
32
|
+
# @return [String] The mention name.
|
33
|
+
attr_accessor :mention_name
|
34
|
+
|
35
|
+
# An alias the robot will look for in incoming messages to determine if it's
|
36
|
+
# being addressed.
|
37
|
+
# @return [String, Nil] The alias, if one was set.
|
38
|
+
attr_accessor :alias
|
39
|
+
|
40
|
+
# The name of the robot as it will appear in the chat.
|
41
|
+
# @return [String] The robot's name.
|
42
|
+
attr_accessor :name
|
43
|
+
|
44
|
+
# The {Registry} for the currently running robot.
|
45
|
+
# @return [Registry] The registry.
|
46
|
+
# @since 4.0.0
|
47
|
+
attr_reader :registry
|
48
|
+
|
49
|
+
# The {Store} for handlers to persist data between instances.
|
50
|
+
# @return [Store] The store.
|
51
|
+
# @since 5.0.0
|
52
|
+
attr_reader :store
|
53
|
+
|
54
|
+
def_delegators :registry, :config, :adapters, :handlers, :hooks, :redis
|
55
|
+
|
56
|
+
# @!method chat_service
|
57
|
+
# @see Adapter#chat_service
|
58
|
+
# @since 4.6.0
|
59
|
+
# @!method mention_format(name)
|
60
|
+
# @see Adapter#mention_format
|
61
|
+
# @since 4.4.0
|
62
|
+
# @!method roster(room)
|
63
|
+
# @see Adapter#roster
|
64
|
+
# @since 4.4.1
|
65
|
+
# @!method run_concurrently
|
66
|
+
# @see Adapter#run_concurrently
|
67
|
+
# @since 5.0.0
|
68
|
+
def_delegators :adapter, :chat_service, :mention_format, :roster, :run_concurrently
|
69
|
+
|
70
|
+
# @param registry [Registry] The registry for the robot's configuration and plugins.
|
71
|
+
def initialize(registry = Lita)
|
72
|
+
@registry = registry
|
73
|
+
@name = config.robot.name
|
74
|
+
@mention_name = config.robot.mention_name || @name
|
75
|
+
@alias = config.robot.alias
|
76
|
+
@store = Store.new(Hash.new { |h, k| h[k] = Store.new })
|
77
|
+
@app = RackApp.build(self)
|
78
|
+
@auth = Authorization.new(self)
|
79
|
+
handlers.each do |handler|
|
80
|
+
handler.after_config_block&.call(config.handlers.public_send(handler.namespace))
|
81
|
+
end
|
82
|
+
trigger(:loaded, room_ids: persisted_rooms)
|
83
|
+
end
|
84
|
+
|
85
|
+
# The global logger. A convenience for +Lita.logger+.
|
86
|
+
# @return [::Logger] The logger.
|
87
|
+
def logger
|
88
|
+
Lita.logger
|
89
|
+
end
|
90
|
+
|
91
|
+
# The primary entry point from the adapter for an incoming message.
|
92
|
+
# Dispatches the message to all registered handlers.
|
93
|
+
# @param message [Message] The incoming message.
|
94
|
+
# @return [void]
|
95
|
+
def receive(message)
|
96
|
+
trigger(:message_received, message: message)
|
97
|
+
|
98
|
+
matched = handlers.map do |handler|
|
99
|
+
next unless handler.respond_to?(:dispatch)
|
100
|
+
|
101
|
+
handler.dispatch(self, message)
|
102
|
+
end.any?
|
103
|
+
|
104
|
+
trigger(:unhandled_message, message: message) unless matched
|
105
|
+
end
|
106
|
+
|
107
|
+
# Starts the robot, booting the web server and delegating to the adapter to
|
108
|
+
# connect to the chat service.
|
109
|
+
# @return [void]
|
110
|
+
def run
|
111
|
+
run_app
|
112
|
+
adapter.run
|
113
|
+
rescue Interrupt
|
114
|
+
shut_down
|
115
|
+
end
|
116
|
+
|
117
|
+
# Makes the robot join a room with the specified ID.
|
118
|
+
# @param room [Room, String] The room to join, as a {Room} object or a string identifier.
|
119
|
+
# @return [void]
|
120
|
+
# @since 3.0.0
|
121
|
+
def join(room)
|
122
|
+
room_object = find_room(room)
|
123
|
+
|
124
|
+
if room_object
|
125
|
+
redis.sadd("persisted_rooms", room_object.id)
|
126
|
+
adapter.join(room_object.id)
|
127
|
+
else
|
128
|
+
adapter.join(room)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Makes the robot part from the room with the specified ID.
|
133
|
+
# @param room [Room, String] The room to leave, as a {Room} object or a string identifier.
|
134
|
+
# @return [void]
|
135
|
+
# @since 3.0.0
|
136
|
+
def part(room)
|
137
|
+
room_object = find_room(room)
|
138
|
+
|
139
|
+
if room_object
|
140
|
+
redis.srem("persisted_rooms", room_object.id)
|
141
|
+
adapter.part(room_object.id)
|
142
|
+
else
|
143
|
+
adapter.part(room)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# A list of room IDs the robot should join on boot.
|
148
|
+
# @return [Array<String>] An array of room IDs.
|
149
|
+
# @since 4.4.2
|
150
|
+
def persisted_rooms
|
151
|
+
redis.smembers("persisted_rooms").sort
|
152
|
+
end
|
153
|
+
|
154
|
+
# Sends one or more messages to a user or room.
|
155
|
+
# @param target [Source] The user or room to send to. If the Source
|
156
|
+
# has a room, it will choose the room. Otherwise, it will send to the
|
157
|
+
# user.
|
158
|
+
# @param strings [String, Array<String>] One or more strings to send.
|
159
|
+
# @return [void]
|
160
|
+
def send_messages(target, *strings)
|
161
|
+
adapter.send_messages(target, strings.flatten)
|
162
|
+
end
|
163
|
+
alias send_message send_messages
|
164
|
+
|
165
|
+
# Sends one or more messages to a user or room. If sending to a room,
|
166
|
+
# prefixes each message with the user's mention name.
|
167
|
+
# @param target [Source] The user or room to send to. If the Source
|
168
|
+
# has a room, it will choose the room. Otherwise, it will send to the
|
169
|
+
# user.
|
170
|
+
# @param strings [String, Array<String>] One or more strings to send.
|
171
|
+
# @return [void]
|
172
|
+
# @since 3.1.0
|
173
|
+
def send_messages_with_mention(target, *strings)
|
174
|
+
return send_messages(target, *strings) if target.private_message?
|
175
|
+
|
176
|
+
mention_name = target.user.mention_name
|
177
|
+
prefixed_strings = strings.map do |s|
|
178
|
+
"#{adapter.mention_format(mention_name).strip} #{s}"
|
179
|
+
end
|
180
|
+
|
181
|
+
send_messages(target, *prefixed_strings)
|
182
|
+
end
|
183
|
+
alias send_message_with_mention send_messages_with_mention
|
184
|
+
|
185
|
+
# Sets the topic for a chat room.
|
186
|
+
# @param target [Source] A source object specifying the room.
|
187
|
+
# @param topic [String] The new topic message to set.
|
188
|
+
# @return [void]
|
189
|
+
def set_topic(target, topic)
|
190
|
+
adapter.set_topic(target, topic)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Gracefully shuts the robot down, stopping the web server and delegating
|
194
|
+
# to the adapter to perform any shut down tasks necessary for the chat
|
195
|
+
# service.
|
196
|
+
# @return [void]
|
197
|
+
def shut_down
|
198
|
+
trigger(:shut_down_started)
|
199
|
+
@server&.stop(true)
|
200
|
+
@server_thread&.join
|
201
|
+
adapter.shut_down
|
202
|
+
trigger(:shut_down_complete)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Triggers an event, instructing all registered handlers to invoke any
|
206
|
+
# methods subscribed to the event, and passing them a payload hash of
|
207
|
+
# arbitrary data.
|
208
|
+
# @param event_name [String, Symbol] The name of the event to trigger.
|
209
|
+
# @param payload [Hash] An optional hash of arbitrary data.
|
210
|
+
# @return [void]
|
211
|
+
def trigger(event_name, payload = {})
|
212
|
+
handlers.each do |handler|
|
213
|
+
next unless handler.respond_to?(:trigger)
|
214
|
+
|
215
|
+
handler.trigger(self, event_name, payload)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
# Loads and caches the adapter on first access.
|
222
|
+
def adapter
|
223
|
+
@adapter ||= load_adapter
|
224
|
+
end
|
225
|
+
|
226
|
+
# Ensure the argument is a Room.
|
227
|
+
def find_room(room_or_identifier)
|
228
|
+
case room_or_identifier
|
229
|
+
when Room
|
230
|
+
room_or_identifier
|
231
|
+
else
|
232
|
+
Room.fuzzy_find(room_or_identifier)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Loads the selected adapter.
|
237
|
+
def load_adapter
|
238
|
+
adapter_name = config.robot.adapter
|
239
|
+
adapter_class = adapters[adapter_name.to_sym]
|
240
|
+
|
241
|
+
unless adapter_class
|
242
|
+
logger.fatal I18n.t("lita.robot.unknown_adapter", adapter: adapter_name)
|
243
|
+
exit(false)
|
244
|
+
end
|
245
|
+
|
246
|
+
adapter_class.new(self)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Starts the web server.
|
250
|
+
def run_app
|
251
|
+
http_config = config.http
|
252
|
+
|
253
|
+
@server_thread = Thread.new do
|
254
|
+
@server = Puma::Server.new(app)
|
255
|
+
begin
|
256
|
+
@server.add_tcp_listener(http_config.host, http_config.port.to_i)
|
257
|
+
rescue Errno::EADDRINUSE, Errno::EACCES => e
|
258
|
+
logger.fatal I18n.t(
|
259
|
+
"lita.http.exception",
|
260
|
+
message: e.message,
|
261
|
+
backtrace: e.backtrace.join("\n")
|
262
|
+
)
|
263
|
+
exit(false)
|
264
|
+
end
|
265
|
+
@server.min_threads = http_config.min_threads
|
266
|
+
@server.max_threads = http_config.max_threads
|
267
|
+
@server.run
|
268
|
+
end
|
269
|
+
|
270
|
+
@server_thread.abort_on_exception = true
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
data/lib/lita/room.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redis-namespace"
|
4
|
+
|
5
|
+
require_relative "util"
|
6
|
+
|
7
|
+
module Lita
|
8
|
+
# A room in the chat service. Persisted in Redis.
|
9
|
+
# @since 4.4.0
|
10
|
+
class Room
|
11
|
+
class << self
|
12
|
+
# Creates a new room with the given ID, or merges and saves supplied
|
13
|
+
# metadata to a room with the given ID.
|
14
|
+
# @param id [Integer, String] A unique identifier for the room.
|
15
|
+
# @param metadata [Hash] An optional hash of metadata about the room.
|
16
|
+
# @option metadata [String] name (id) The display name of the room.
|
17
|
+
# @return [Room] The room.
|
18
|
+
def create_or_update(id, metadata = {})
|
19
|
+
existing_room = find_by_id(id)
|
20
|
+
metadata = Util.stringify_keys(metadata)
|
21
|
+
metadata = existing_room.metadata.merge(metadata) if existing_room
|
22
|
+
room = new(id, metadata)
|
23
|
+
room.save
|
24
|
+
room
|
25
|
+
end
|
26
|
+
|
27
|
+
# Finds a room by ID.
|
28
|
+
# @param id [Integer, String] The room's unique ID.
|
29
|
+
# @return [Room, nil] The room or +nil+ if no such room is known.
|
30
|
+
def find_by_id(id)
|
31
|
+
metadata = redis.hgetall("id:#{id}")
|
32
|
+
new(id, metadata) if metadata.key?("name")
|
33
|
+
end
|
34
|
+
|
35
|
+
# Finds a room by display name.
|
36
|
+
# @param name [String] The room's name.
|
37
|
+
# @return [Room, nil] The room or +nil+ if no such room is known.
|
38
|
+
def find_by_name(name)
|
39
|
+
id = redis.get("name:#{name}")
|
40
|
+
find_by_id(id) if id
|
41
|
+
end
|
42
|
+
|
43
|
+
# Finds a room by ID or name
|
44
|
+
# @param identifier [Integer, String] The room's ID or name.
|
45
|
+
# @return [Room, nil] The room or +nil+ if no room was found.
|
46
|
+
def fuzzy_find(identifier)
|
47
|
+
find_by_id(identifier) || find_by_name(identifier)
|
48
|
+
end
|
49
|
+
|
50
|
+
# The +Redis::Namespace+ for room persistence.
|
51
|
+
# @return [Redis::Namespace] The Redis connection.
|
52
|
+
def redis
|
53
|
+
@redis ||= Redis::Namespace.new("rooms", redis: Lita.redis)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# The room's unique ID.
|
58
|
+
# @return [String] The room's ID.
|
59
|
+
attr_reader :id
|
60
|
+
|
61
|
+
# A hash of arbitrary metadata about the room.
|
62
|
+
# @return [Hash] The room's metadata.
|
63
|
+
attr_reader :metadata
|
64
|
+
|
65
|
+
# The room's name as displayed in a standard user interface.
|
66
|
+
# @return [String] The room's name.
|
67
|
+
attr_reader :name
|
68
|
+
|
69
|
+
# @param id [Integer, String] The room's unique ID.
|
70
|
+
# @param metadata [Hash] Arbitrary room metadata.
|
71
|
+
# @option metadata [String] name (id) The room's display name.
|
72
|
+
def initialize(id, metadata = {})
|
73
|
+
@id = id.to_s
|
74
|
+
@metadata = Util.stringify_keys(metadata)
|
75
|
+
@name = @metadata["name"] || @id
|
76
|
+
end
|
77
|
+
|
78
|
+
# Compares the room against another room object to determine equality. Rooms
|
79
|
+
# are considered equal if they have the same ID.
|
80
|
+
# @param other [Room] The room to compare against.
|
81
|
+
# @return [Boolean] True if rooms are equal, false otherwise.
|
82
|
+
def ==(other)
|
83
|
+
other.respond_to?(:id) && id == other.id
|
84
|
+
end
|
85
|
+
alias eql? ==
|
86
|
+
|
87
|
+
# Generates a +Fixnum+ hash value for this user object. Implemented to support equality.
|
88
|
+
# @return [Fixnum] The hash value.
|
89
|
+
# @see Object#hash
|
90
|
+
def hash
|
91
|
+
id.hash
|
92
|
+
end
|
93
|
+
|
94
|
+
# Saves the room record to Redis, overwriting any previous data for the current ID.
|
95
|
+
# @return [void]
|
96
|
+
def save
|
97
|
+
ensure_name_metadata_set
|
98
|
+
|
99
|
+
redis.pipelined do
|
100
|
+
redis.hmset("id:#{id}", *metadata.to_a.flatten)
|
101
|
+
redis.set("name:#{name}", id)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Ensure the room's metadata contains its name, to ensure their Redis hash contains at least
|
108
|
+
# one value. It's not possible to store an empty hash key in Redis.
|
109
|
+
def ensure_name_metadata_set
|
110
|
+
room_name = metadata.delete("name")
|
111
|
+
metadata["name"] = room_name || id
|
112
|
+
end
|
113
|
+
|
114
|
+
# The Redis connection for room persistence.
|
115
|
+
def redis
|
116
|
+
self.class.redis
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lita
|
4
|
+
# Determines if an incoming message should trigger a route.
|
5
|
+
# @api private
|
6
|
+
class RouteValidator
|
7
|
+
# The handler class the route belongs to.
|
8
|
+
attr_reader :handler
|
9
|
+
|
10
|
+
# The incoming message.
|
11
|
+
attr_reader :message
|
12
|
+
|
13
|
+
# The currently running robot.
|
14
|
+
attr_reader :robot
|
15
|
+
|
16
|
+
# The route being checked.
|
17
|
+
attr_reader :route
|
18
|
+
|
19
|
+
# @param handler [Handler] The handler the route belongs to.
|
20
|
+
# @param route [Handler::ChatRouter::Route] The route being validated.
|
21
|
+
# @param message [Message] The incoming message.
|
22
|
+
# @param robot [Robot] The currently running robot.
|
23
|
+
def initialize(handler, route, message, robot)
|
24
|
+
@handler = handler
|
25
|
+
@route = route
|
26
|
+
@message = message
|
27
|
+
@robot = robot
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a boolean indicating whether or not the route should be triggered.
|
31
|
+
# @return [Boolean] Whether or not the route should be triggered.
|
32
|
+
def call
|
33
|
+
return unless command_satisfied?(route, message)
|
34
|
+
return if from_self?(message, robot)
|
35
|
+
return unless matches_pattern?(route, message)
|
36
|
+
|
37
|
+
unless authorized?(robot, message.user, route.required_groups)
|
38
|
+
robot.trigger(
|
39
|
+
:route_authorization_failed,
|
40
|
+
message: message,
|
41
|
+
robot: robot,
|
42
|
+
route: route,
|
43
|
+
)
|
44
|
+
return
|
45
|
+
end
|
46
|
+
return unless passes_route_hooks?(route, message, robot)
|
47
|
+
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Message must be a command if the route requires a command
|
54
|
+
def command_satisfied?(route, message)
|
55
|
+
!route.command? || message.command?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Messages from self should be ignored to prevent infinite loops
|
59
|
+
def from_self?(message, robot)
|
60
|
+
message.user.name == robot.name
|
61
|
+
end
|
62
|
+
|
63
|
+
# Message must match the pattern
|
64
|
+
def matches_pattern?(route, message)
|
65
|
+
route.pattern.match?(message.body)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Allow custom route hooks to reject the route
|
69
|
+
def passes_route_hooks?(route, message, robot)
|
70
|
+
robot.hooks[:validate_route].all? do |hook|
|
71
|
+
hook.call(handler: handler, route: route, message: message, robot: robot)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# User must be in auth group if route is restricted.
|
76
|
+
def authorized?(robot, user, required_groups)
|
77
|
+
required_groups.nil? || required_groups.any? do |group|
|
78
|
+
robot.auth.user_in_group?(user, group)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require "i18n"
|
6
|
+
require "faraday"
|
7
|
+
|
8
|
+
require_relative "../adapters/test"
|
9
|
+
require_relative "../message"
|
10
|
+
require_relative "../rspec"
|
11
|
+
require_relative "../robot"
|
12
|
+
require_relative "../source"
|
13
|
+
require_relative "../user"
|
14
|
+
require_relative "matchers/chat_route_matcher"
|
15
|
+
require_relative "matchers/http_route_matcher"
|
16
|
+
require_relative "matchers/event_route_matcher"
|
17
|
+
|
18
|
+
module Lita
|
19
|
+
module RSpec
|
20
|
+
# Extras for +RSpec+ to facilitate testing Lita handlers.
|
21
|
+
module Handler
|
22
|
+
include Matchers::ChatRouteMatcher
|
23
|
+
include Matchers::HTTPRouteMatcher
|
24
|
+
include Matchers::EventRouteMatcher
|
25
|
+
|
26
|
+
class << self
|
27
|
+
# Sets up the RSpec environment to easily test Lita handlers.
|
28
|
+
def included(base)
|
29
|
+
base.include(Lita::RSpec)
|
30
|
+
|
31
|
+
prepare_handlers(base)
|
32
|
+
prepare_adapter(base)
|
33
|
+
prepare_let_blocks(base)
|
34
|
+
prepare_subject(base)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Register the test adapter.
|
40
|
+
def prepare_adapter(base)
|
41
|
+
base.class_eval do
|
42
|
+
before do
|
43
|
+
registry.register_adapter(:test, Lita::Adapters::Test)
|
44
|
+
registry.config.robot.adapter = :test
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Register the handler(s) under test.
|
50
|
+
def prepare_handlers(base)
|
51
|
+
base.class_eval do
|
52
|
+
before do
|
53
|
+
handlers = Set.new(
|
54
|
+
[described_class] + Array(base.metadata[:additional_lita_handlers])
|
55
|
+
)
|
56
|
+
|
57
|
+
handlers.each do |handler|
|
58
|
+
registry.register_handler(handler)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Create common test objects.
|
65
|
+
def prepare_let_blocks(base)
|
66
|
+
base.class_eval do
|
67
|
+
let(:robot) { Robot.new(registry) }
|
68
|
+
let(:source) { Source.new(user: user) }
|
69
|
+
let(:user) { User.create("1", name: "Test User") }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Set up a working test subject.
|
74
|
+
def prepare_subject(base)
|
75
|
+
base.class_eval do
|
76
|
+
subject { described_class.new(robot) }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# An array of strings that have been sent by the robot during the course of a test.
|
82
|
+
# @return [Array<String>] The replies.
|
83
|
+
def replies
|
84
|
+
robot.chat_service.sent_messages
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sends a message to the robot.
|
88
|
+
# @param body [String] The message to send.
|
89
|
+
# @param as [User] The user sending the message.
|
90
|
+
# @param from [Room] The room where the message is received from.
|
91
|
+
# @return [void]
|
92
|
+
def send_message(body, as: user, from: nil, privately: false)
|
93
|
+
message = Message.new(
|
94
|
+
robot,
|
95
|
+
body,
|
96
|
+
Source.new(user: as, room: from, private_message: privately)
|
97
|
+
)
|
98
|
+
|
99
|
+
robot.receive(message)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Sends a "command" message to the robot.
|
103
|
+
# @param body [String] The message to send.
|
104
|
+
# @param as [User] The user sending the message.
|
105
|
+
# @param from [Room] The room where the message is received from.
|
106
|
+
# @return [void]
|
107
|
+
def send_command(body, as: user, from: nil, privately: false)
|
108
|
+
send_message("#{robot.mention_name}: #{body}", as: as, from: from, privately: privately)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns a Faraday connection hooked up to the currently running robot's Rack app.
|
112
|
+
# @return [Faraday::Connection] The connection.
|
113
|
+
# @since 4.0.0
|
114
|
+
def http
|
115
|
+
unless Rack.const_defined?(:Test)
|
116
|
+
begin
|
117
|
+
require "rack/test"
|
118
|
+
rescue LoadError
|
119
|
+
raise LoadError, I18n.t("lita.rspec.rack_test_required")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
Faraday::Connection.new { |c| c.adapter(:rack, robot.app) }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../message"
|
4
|
+
require_relative "../../route_validator"
|
5
|
+
|
6
|
+
module Lita
|
7
|
+
module RSpec
|
8
|
+
module Matchers
|
9
|
+
# RSpec matchers for chat routes.
|
10
|
+
# @since 4.0.0
|
11
|
+
module ChatRouteMatcher
|
12
|
+
extend ::RSpec::Matchers::DSL
|
13
|
+
|
14
|
+
matcher :route do |message_body|
|
15
|
+
match do
|
16
|
+
message = Message.new(robot, message_body, source)
|
17
|
+
|
18
|
+
if defined?(@group) && @group.to_s.casecmp("admins").zero?
|
19
|
+
robot.config.robot.admins = Array(robot.config.robot.admins) + [source.user.id]
|
20
|
+
elsif defined?(@group)
|
21
|
+
robot.auth.add_user_to_group!(source.user, @group)
|
22
|
+
end
|
23
|
+
|
24
|
+
matching_routes = described_class.routes.select do |route|
|
25
|
+
RouteValidator.new(described_class, route, message, robot).call
|
26
|
+
end
|
27
|
+
|
28
|
+
if defined?(@method_name)
|
29
|
+
matching_routes.any? { |route| route.callback.method_name == @method_name }
|
30
|
+
else
|
31
|
+
!matching_routes.empty?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
chain :with_authorization_for do |group|
|
36
|
+
@group = group
|
37
|
+
end
|
38
|
+
|
39
|
+
chain :to do |method_name|
|
40
|
+
@method_name = method_name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sets an expectation that the provided message routes to a command.
|
45
|
+
# @param message_body [String] The body of the message.
|
46
|
+
# @return [void]
|
47
|
+
def route_command(message_body)
|
48
|
+
route("#{robot.mention_name} #{message_body}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lita
|
4
|
+
module RSpec
|
5
|
+
module Matchers
|
6
|
+
# RSpec matchers for event routes.
|
7
|
+
# @since 4.0.0
|
8
|
+
module EventRouteMatcher
|
9
|
+
extend ::RSpec::Matchers::DSL
|
10
|
+
|
11
|
+
matcher :route_event do |event_name|
|
12
|
+
match do
|
13
|
+
callbacks = described_class.event_subscriptions_for(event_name)
|
14
|
+
|
15
|
+
if defined?(@method_name)
|
16
|
+
callbacks.any? { |callback| callback.method_name.equal?(@method_name) }
|
17
|
+
else
|
18
|
+
!callbacks.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
chain :to do |method_name|
|
23
|
+
@method_name = method_name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|