grinch 1.0.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 +7 -0
- data/.yardopts +1 -0
- data/LICENSE +22 -0
- data/README.md +180 -0
- data/docs/bot_options.md +454 -0
- data/docs/changes.md +541 -0
- data/docs/common_mistakes.md +60 -0
- data/docs/common_tasks.md +57 -0
- data/docs/encodings.md +69 -0
- data/docs/events.md +273 -0
- data/docs/getting_started.md +184 -0
- data/docs/logging.md +90 -0
- data/docs/migrating.md +267 -0
- data/docs/plugins.md +4 -0
- data/docs/readme.md +20 -0
- data/examples/basic/autovoice.rb +32 -0
- data/examples/basic/google.rb +35 -0
- data/examples/basic/hello.rb +15 -0
- data/examples/basic/join_part.rb +34 -0
- data/examples/basic/memo.rb +39 -0
- data/examples/basic/msg.rb +16 -0
- data/examples/basic/seen.rb +36 -0
- data/examples/basic/urban_dict.rb +35 -0
- data/examples/basic/url_shorten.rb +35 -0
- data/examples/plugins/autovoice.rb +37 -0
- data/examples/plugins/custom_prefix.rb +23 -0
- data/examples/plugins/dice_roll.rb +38 -0
- data/examples/plugins/google.rb +36 -0
- data/examples/plugins/hello.rb +22 -0
- data/examples/plugins/hooks.rb +36 -0
- data/examples/plugins/join_part.rb +42 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/msg.rb +22 -0
- data/examples/plugins/multiple_matches.rb +32 -0
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/seen.rb +45 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +33 -0
- data/lib/cinch.rb +5 -0
- data/lib/cinch/ban.rb +50 -0
- data/lib/cinch/bot.rb +479 -0
- data/lib/cinch/cached_list.rb +19 -0
- data/lib/cinch/callback.rb +20 -0
- data/lib/cinch/channel.rb +463 -0
- data/lib/cinch/channel_list.rb +29 -0
- data/lib/cinch/configuration.rb +73 -0
- data/lib/cinch/configuration/bot.rb +48 -0
- data/lib/cinch/configuration/dcc.rb +16 -0
- data/lib/cinch/configuration/plugins.rb +41 -0
- data/lib/cinch/configuration/sasl.rb +19 -0
- data/lib/cinch/configuration/ssl.rb +19 -0
- data/lib/cinch/configuration/timeouts.rb +14 -0
- data/lib/cinch/constants.rb +533 -0
- data/lib/cinch/dcc.rb +12 -0
- data/lib/cinch/dcc/dccable_object.rb +37 -0
- data/lib/cinch/dcc/incoming.rb +1 -0
- data/lib/cinch/dcc/incoming/send.rb +147 -0
- data/lib/cinch/dcc/outgoing.rb +1 -0
- data/lib/cinch/dcc/outgoing/send.rb +122 -0
- data/lib/cinch/exceptions.rb +46 -0
- data/lib/cinch/formatting.rb +125 -0
- data/lib/cinch/handler.rb +118 -0
- data/lib/cinch/handler_list.rb +90 -0
- data/lib/cinch/helpers.rb +231 -0
- data/lib/cinch/irc.rb +924 -0
- data/lib/cinch/isupport.rb +98 -0
- data/lib/cinch/log_filter.rb +21 -0
- data/lib/cinch/logger.rb +168 -0
- data/lib/cinch/logger/formatted_logger.rb +97 -0
- data/lib/cinch/logger/zcbot_logger.rb +22 -0
- data/lib/cinch/logger_list.rb +85 -0
- data/lib/cinch/mask.rb +69 -0
- data/lib/cinch/message.rb +392 -0
- data/lib/cinch/message_queue.rb +107 -0
- data/lib/cinch/mode_parser.rb +76 -0
- data/lib/cinch/network.rb +104 -0
- data/lib/cinch/open_ended_queue.rb +26 -0
- data/lib/cinch/pattern.rb +65 -0
- data/lib/cinch/plugin.rb +515 -0
- data/lib/cinch/plugin_list.rb +38 -0
- data/lib/cinch/rubyext/float.rb +3 -0
- data/lib/cinch/rubyext/module.rb +26 -0
- data/lib/cinch/rubyext/string.rb +33 -0
- data/lib/cinch/sasl.rb +34 -0
- data/lib/cinch/sasl/dh_blowfish.rb +71 -0
- data/lib/cinch/sasl/diffie_hellman.rb +47 -0
- data/lib/cinch/sasl/mechanism.rb +6 -0
- data/lib/cinch/sasl/plain.rb +26 -0
- data/lib/cinch/syncable.rb +83 -0
- data/lib/cinch/target.rb +199 -0
- data/lib/cinch/timer.rb +145 -0
- data/lib/cinch/user.rb +488 -0
- data/lib/cinch/user_list.rb +87 -0
- data/lib/cinch/utilities/deprecation.rb +16 -0
- data/lib/cinch/utilities/encoding.rb +37 -0
- data/lib/cinch/utilities/kernel.rb +13 -0
- data/lib/cinch/version.rb +4 -0
- metadata +140 -0
data/lib/cinch/mask.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Cinch
|
2
|
+
# This class represents masks, which are primarily used for bans.
|
3
|
+
class Mask
|
4
|
+
# @return [String]
|
5
|
+
attr_reader :nick
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :user
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :host
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :mask
|
12
|
+
|
13
|
+
# @version 1.1.2
|
14
|
+
# @param [String] mask
|
15
|
+
def initialize(mask)
|
16
|
+
@mask = mask
|
17
|
+
@nick, @user, @host = mask.match(/(.+)!(.+)@(.+)/)[1..-1]
|
18
|
+
@regexp = Regexp.new("^" + Regexp.escape(mask).gsub("\\*", ".*").gsub("\\?", ".?") + "$")
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Boolean]
|
22
|
+
# @since 1.1.0
|
23
|
+
def ==(other)
|
24
|
+
other.respond_to?(:mask) && other.mask == @mask
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Boolean]
|
28
|
+
# @since 1.1.0
|
29
|
+
def eql?(other)
|
30
|
+
other.is_a?(self.class) && self == other
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Fixnum]
|
34
|
+
def hash
|
35
|
+
@mask.hash
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param [Mask, String, #mask] target
|
39
|
+
# @return [Boolean]
|
40
|
+
# @version 1.1.2
|
41
|
+
def match(target)
|
42
|
+
return self.class.from(target).mask =~ @regexp
|
43
|
+
|
44
|
+
# TODO support CIDR (freenode)
|
45
|
+
end
|
46
|
+
alias_method :=~, :match
|
47
|
+
|
48
|
+
# @return [String]
|
49
|
+
def to_s
|
50
|
+
@mask.dup
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [String, #mask] target
|
54
|
+
# @return [target] if already a Mask
|
55
|
+
# @return [Mask]
|
56
|
+
# @version 2.0.0
|
57
|
+
def self.from(target)
|
58
|
+
return target if target.is_a?(self)
|
59
|
+
|
60
|
+
if target.respond_to?(:mask)
|
61
|
+
mask = target.mask
|
62
|
+
else
|
63
|
+
mask = Mask.new(target.to_s)
|
64
|
+
end
|
65
|
+
|
66
|
+
return mask
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,392 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "time"
|
3
|
+
require "cinch/formatting"
|
4
|
+
|
5
|
+
module Cinch
|
6
|
+
# This class serves two purposes. For one, it simply
|
7
|
+
# represents incoming messages and allows for querying various
|
8
|
+
# details (who sent the message, what kind of message it is, etc).
|
9
|
+
#
|
10
|
+
# At the same time, it allows **responding** to messages, which
|
11
|
+
# means sending messages to either users or channels.
|
12
|
+
class Message
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :raw
|
15
|
+
|
16
|
+
# @return [String]
|
17
|
+
attr_reader :prefix
|
18
|
+
|
19
|
+
# @return [String]
|
20
|
+
attr_reader :command
|
21
|
+
|
22
|
+
# @return [Array<String>]
|
23
|
+
attr_reader :params
|
24
|
+
|
25
|
+
# @return [Hash]
|
26
|
+
attr_reader :tags
|
27
|
+
|
28
|
+
# @return [Array<Symbol>]
|
29
|
+
attr_reader :events
|
30
|
+
# @api private
|
31
|
+
attr_writer :events
|
32
|
+
|
33
|
+
# @return [Time]
|
34
|
+
# @since 2.0.0
|
35
|
+
attr_reader :time
|
36
|
+
|
37
|
+
# @return [Bot]
|
38
|
+
# @since 1.1.0
|
39
|
+
attr_reader :bot
|
40
|
+
|
41
|
+
# @return [User] The user who sent this message
|
42
|
+
attr_reader :user
|
43
|
+
|
44
|
+
# @return [String, nil]
|
45
|
+
attr_reader :server
|
46
|
+
|
47
|
+
# @return [Integer, nil] the numeric error code, if any
|
48
|
+
attr_reader :error
|
49
|
+
|
50
|
+
# @return [String, nil] the command part of an CTCP message
|
51
|
+
attr_reader :ctcp_command
|
52
|
+
|
53
|
+
# @return [Channel] The channel in which this message was sent
|
54
|
+
attr_reader :channel
|
55
|
+
|
56
|
+
# @return [String, nil] the CTCP message, without \001 control characters
|
57
|
+
attr_reader :ctcp_message
|
58
|
+
|
59
|
+
# @return [Array<String>, nil]
|
60
|
+
attr_reader :ctcp_args
|
61
|
+
|
62
|
+
# @return [String, nil]
|
63
|
+
attr_reader :message
|
64
|
+
|
65
|
+
# @return [String, nil] The action message
|
66
|
+
# @since 2.0.0
|
67
|
+
attr_reader :action_message
|
68
|
+
|
69
|
+
# @return [Target]
|
70
|
+
attr_reader :target
|
71
|
+
|
72
|
+
# The STATUSMSG mode a channel message was sent to.
|
73
|
+
#
|
74
|
+
# Some IRC servers allow sending messages limited to people in a
|
75
|
+
# channel who have a certain mode. For example, by sending a
|
76
|
+
# message to `+#channel`, only people who are voiced, or have a
|
77
|
+
# higher mode (op) will receive the message.
|
78
|
+
#
|
79
|
+
# This attribute contains the mode character the message was sent
|
80
|
+
# to, or nil if it was a normal message. For the previous example,
|
81
|
+
# this attribute would be set to `"v"`, for voiced.
|
82
|
+
#
|
83
|
+
# @return [String, nil]
|
84
|
+
# @since 2.3.0
|
85
|
+
attr_reader :statusmsg_mode
|
86
|
+
|
87
|
+
def initialize(msg, bot)
|
88
|
+
@raw = msg
|
89
|
+
@bot = bot
|
90
|
+
@matches = {:ctcp => {}, :action => {}, :other => {}}
|
91
|
+
@events = []
|
92
|
+
@time = Time.now
|
93
|
+
@statusmsg_mode = nil
|
94
|
+
parse if msg
|
95
|
+
end
|
96
|
+
|
97
|
+
# @api private
|
98
|
+
# @return [void]
|
99
|
+
def parse
|
100
|
+
match = @raw.match(/(?:^@([^:]+))?(?::?(\S+) )?(\S+)(.*)/)
|
101
|
+
tags, @prefix, @command, raw_params = match.captures
|
102
|
+
|
103
|
+
if @bot.irc.network.ngametv?
|
104
|
+
if @prefix != "ngame"
|
105
|
+
@prefix = "%s!%s@%s" % [@prefix, @prefix, @prefix]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
@params = parse_params(raw_params)
|
110
|
+
@tags = parse_tags(tags)
|
111
|
+
|
112
|
+
@user = parse_user
|
113
|
+
@channel, @statusmsg_mode = parse_channel
|
114
|
+
@target = @channel || @user
|
115
|
+
@server = parse_server
|
116
|
+
@error = parse_error
|
117
|
+
@message = parse_message
|
118
|
+
|
119
|
+
@ctcp_message = parse_ctcp_message
|
120
|
+
@ctcp_command = parse_ctcp_command
|
121
|
+
@ctcp_args = parse_ctcp_args
|
122
|
+
|
123
|
+
@action_message = parse_action_message
|
124
|
+
end
|
125
|
+
|
126
|
+
# @group Type checking
|
127
|
+
|
128
|
+
# @return [Boolean] true if the message is an numeric reply (as
|
129
|
+
# opposed to a command)
|
130
|
+
def numeric_reply?
|
131
|
+
!!@command.match(/^\d{3}$/)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [Boolean] true if the message describes an error
|
135
|
+
def error?
|
136
|
+
!@error.nil?
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return [Boolean] true if this message was sent in a channel
|
140
|
+
def channel?
|
141
|
+
!@channel.nil?
|
142
|
+
end
|
143
|
+
|
144
|
+
# @return [Boolean] true if the message is an CTCP message
|
145
|
+
def ctcp?
|
146
|
+
!!(@params.last =~ /\001.+\001/)
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [Boolean] true if the message is an action (/me)
|
150
|
+
# @since 2.0.0
|
151
|
+
def action?
|
152
|
+
@ctcp_command == "ACTION"
|
153
|
+
end
|
154
|
+
|
155
|
+
# @endgroup
|
156
|
+
|
157
|
+
# @api private
|
158
|
+
# @return [MatchData]
|
159
|
+
def match(regexp, type, strip_colors)
|
160
|
+
text = ""
|
161
|
+
case type
|
162
|
+
when :ctcp
|
163
|
+
text = ctcp_message
|
164
|
+
when :action
|
165
|
+
text = action_message
|
166
|
+
else
|
167
|
+
text = message.to_s
|
168
|
+
type = :other
|
169
|
+
end
|
170
|
+
|
171
|
+
if strip_colors
|
172
|
+
text = Cinch::Formatting.unformat(text)
|
173
|
+
end
|
174
|
+
|
175
|
+
@matches[type][regexp] ||= text.match(regexp)
|
176
|
+
end
|
177
|
+
|
178
|
+
# @group Replying
|
179
|
+
|
180
|
+
# Replies to a message, automatically determining if it was a
|
181
|
+
# channel or a private message.
|
182
|
+
#
|
183
|
+
# If the message is a STATUSMSG, i.e. it was send to `+#channel`
|
184
|
+
# or `@#channel` instead of `#channel`, the reply will be sent as
|
185
|
+
# the same kind of STATUSMSG. See {#statusmsg_mode} for more
|
186
|
+
# information on STATUSMSG.
|
187
|
+
#
|
188
|
+
# @param [String] text the message
|
189
|
+
# @param [Boolean] prefix if prefix is true and the message was in
|
190
|
+
# a channel, the reply will be prefixed by the nickname of whoever
|
191
|
+
# send the mesage
|
192
|
+
# @return [void]
|
193
|
+
def reply(text, prefix = false)
|
194
|
+
text = text.to_s
|
195
|
+
if @channel && prefix
|
196
|
+
text = text.split("\n").map {|l| "#{user.nick}: #{l}"}.join("\n")
|
197
|
+
end
|
198
|
+
|
199
|
+
reply_target.send(text)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Like {#reply}, but using {Target#safe_send} instead
|
203
|
+
#
|
204
|
+
# @param (see #reply)
|
205
|
+
# @return (see #reply)
|
206
|
+
def safe_reply(text, prefix = false)
|
207
|
+
text = text.to_s
|
208
|
+
if channel && prefix
|
209
|
+
text = "#{@user.nick}: #{text}"
|
210
|
+
end
|
211
|
+
reply_target.safe_send(text)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Reply to a message with an action.
|
215
|
+
#
|
216
|
+
# For its behaviour with regard to STATUSMSG, see {#reply}.
|
217
|
+
#
|
218
|
+
# @param [String] text the action message
|
219
|
+
# @return [void]
|
220
|
+
def action_reply(text)
|
221
|
+
text = text.to_s
|
222
|
+
reply_target.action(text)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Like {#action_reply}, but using {Target#safe_action} instead
|
226
|
+
#
|
227
|
+
# @param (see #action_reply)
|
228
|
+
# @return (see #action_reply)
|
229
|
+
def safe_action_reply(text)
|
230
|
+
text = text.to_s
|
231
|
+
reply_target.safe_action(text)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Reply to a CTCP message
|
235
|
+
#
|
236
|
+
# @return [void]
|
237
|
+
def ctcp_reply(answer)
|
238
|
+
return unless ctcp?
|
239
|
+
@user.notice "\001#{@ctcp_command} #{answer}\001"
|
240
|
+
end
|
241
|
+
|
242
|
+
# @endgroup
|
243
|
+
|
244
|
+
# @return [String]
|
245
|
+
# @since 1.1.0
|
246
|
+
def to_s
|
247
|
+
"#<Cinch::Message @raw=#{@raw.chomp.inspect} @params=#{@params.inspect} channel=#{@channel.inspect} user=#{@user.inspect}>"
|
248
|
+
end
|
249
|
+
|
250
|
+
private
|
251
|
+
def reply_target
|
252
|
+
if @channel.nil? || @statusmsg_mode.nil?
|
253
|
+
return @target
|
254
|
+
end
|
255
|
+
prefix = @bot.irc.isupport["PREFIX"][@statusmsg_mode]
|
256
|
+
return Target.new(prefix + @channel.name, @bot)
|
257
|
+
end
|
258
|
+
def regular_command?
|
259
|
+
!numeric_reply? # a command can only be numeric or "regular"…
|
260
|
+
end
|
261
|
+
|
262
|
+
def parse_params(raw_params)
|
263
|
+
params = []
|
264
|
+
if match = raw_params.match(/(?:^:| :)(.*)$/)
|
265
|
+
params = match.pre_match.split(" ")
|
266
|
+
params << match[1]
|
267
|
+
else
|
268
|
+
params = raw_params.split(" ")
|
269
|
+
end
|
270
|
+
|
271
|
+
return params
|
272
|
+
end
|
273
|
+
|
274
|
+
def parse_tags(raw_tags)
|
275
|
+
return {} if raw_tags.nil?
|
276
|
+
|
277
|
+
def to_symbol(string)
|
278
|
+
return string.gsub(/-/, "_").downcase.to_sym
|
279
|
+
end
|
280
|
+
|
281
|
+
tags = {}
|
282
|
+
raw_tags.split(";").each do |tag|
|
283
|
+
tag_name, tag_value = tag.split("=")
|
284
|
+
if tag_value =~ /,/
|
285
|
+
tag_value = tag_value.split(',')
|
286
|
+
elsif tag_value.nil?
|
287
|
+
tag_value = tag_name
|
288
|
+
end
|
289
|
+
if tag_name =~ /\//
|
290
|
+
vendor, tag_name = tag_name.split('/')
|
291
|
+
tags[to_symbol(vendor)] = {
|
292
|
+
to_symbol(tag_name) => tag_value
|
293
|
+
}
|
294
|
+
else
|
295
|
+
tags[to_symbol(tag_name)] = tag_value
|
296
|
+
end
|
297
|
+
end
|
298
|
+
return tags
|
299
|
+
end
|
300
|
+
|
301
|
+
def parse_user
|
302
|
+
return unless @prefix
|
303
|
+
nick = @prefix[/^(\S+)!/, 1]
|
304
|
+
user = @prefix[/^\S+!(\S+)@/, 1]
|
305
|
+
host = @prefix[/@(\S+)$/, 1]
|
306
|
+
|
307
|
+
return nil if nick.nil?
|
308
|
+
return @bot.user_list.find_ensured(user, nick, host)
|
309
|
+
end
|
310
|
+
|
311
|
+
def parse_channel
|
312
|
+
# has to be called after parse_params
|
313
|
+
return nil if @params.empty?
|
314
|
+
|
315
|
+
case @command
|
316
|
+
when "INVITE", Constants::RPL_CHANNELMODEIS.to_s, Constants::RPL_BANLIST.to_s
|
317
|
+
@bot.channel_list.find_ensured(@params[1])
|
318
|
+
when Constants::RPL_NAMEREPLY.to_s
|
319
|
+
@bot.channel_list.find_ensured(@params[2])
|
320
|
+
else
|
321
|
+
# Note that this will also find channels for messages that
|
322
|
+
# don't actually include a channel parameter. For example
|
323
|
+
# `QUIT :#sometext` will be interpreted as a channel. The
|
324
|
+
# alternative to the currently used heuristic would be to
|
325
|
+
# hardcode a list of commands that provide a channel argument.
|
326
|
+
ch, status = privmsg_channel_name(@params.first)
|
327
|
+
if ch.nil? && numeric_reply? && @params.size > 1
|
328
|
+
ch, status = privmsg_channel_name(@params[1])
|
329
|
+
end
|
330
|
+
if ch
|
331
|
+
return @bot.channel_list.find_ensured(ch), status
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def privmsg_channel_name(s)
|
337
|
+
chantypes = @bot.irc.isupport["CHANTYPES"]
|
338
|
+
statusmsg = @bot.irc.isupport["STATUSMSG"]
|
339
|
+
if statusmsg.include?(s[0]) && chantypes.include?(s[1])
|
340
|
+
status = @bot.irc.isupport["PREFIX"].invert[s[0]]
|
341
|
+
return s[1..-1], status
|
342
|
+
elsif chantypes.include?(s[0])
|
343
|
+
return s, nil
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def parse_server
|
348
|
+
return unless @prefix
|
349
|
+
return if @prefix.match(/[@!]/)
|
350
|
+
return @prefix[/^(\S+)/, 1]
|
351
|
+
end
|
352
|
+
|
353
|
+
def parse_error
|
354
|
+
return @command.to_i if numeric_reply? && @command[/[45]\d\d/]
|
355
|
+
end
|
356
|
+
|
357
|
+
def parse_message
|
358
|
+
# has to be called after parse_params
|
359
|
+
if error?
|
360
|
+
@error.to_s
|
361
|
+
elsif regular_command?
|
362
|
+
@params.last
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def parse_ctcp_message
|
367
|
+
# has to be called after parse_params
|
368
|
+
return unless ctcp?
|
369
|
+
@params.last =~ /\001(.+)\001/
|
370
|
+
$1
|
371
|
+
end
|
372
|
+
|
373
|
+
def parse_ctcp_command
|
374
|
+
# has to be called after parse_ctcp_message
|
375
|
+
return unless ctcp?
|
376
|
+
@ctcp_message.split(" ").first
|
377
|
+
end
|
378
|
+
|
379
|
+
def parse_ctcp_args
|
380
|
+
# has to be called after parse_ctcp_message
|
381
|
+
return unless ctcp?
|
382
|
+
@ctcp_message.split(" ")[1..-1]
|
383
|
+
end
|
384
|
+
|
385
|
+
def parse_action_message
|
386
|
+
# has to be called after parse_ctcp_message
|
387
|
+
return nil unless action?
|
388
|
+
@ctcp_message.split(" ", 2).last
|
389
|
+
end
|
390
|
+
|
391
|
+
end
|
392
|
+
end
|