ircinch 2.4.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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +3 -0
  3. data/CHANGELOG.md +298 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE.txt +23 -0
  7. data/README.md +195 -0
  8. data/Rakefile +14 -0
  9. data/docs/bot_options.md +454 -0
  10. data/docs/changes.md +541 -0
  11. data/docs/common_mistakes.md +60 -0
  12. data/docs/common_tasks.md +57 -0
  13. data/docs/encodings.md +69 -0
  14. data/docs/events.md +273 -0
  15. data/docs/getting_started.md +184 -0
  16. data/docs/logging.md +90 -0
  17. data/docs/migrating.md +267 -0
  18. data/docs/plugins.md +4 -0
  19. data/docs/readme.md +20 -0
  20. data/examples/basic/autovoice.rb +32 -0
  21. data/examples/basic/google.rb +35 -0
  22. data/examples/basic/hello.rb +14 -0
  23. data/examples/basic/join_part.rb +35 -0
  24. data/examples/basic/memo.rb +39 -0
  25. data/examples/basic/msg.rb +15 -0
  26. data/examples/basic/seen.rb +37 -0
  27. data/examples/basic/urban_dict.rb +36 -0
  28. data/examples/basic/url_shorten.rb +36 -0
  29. data/examples/plugins/autovoice.rb +37 -0
  30. data/examples/plugins/custom_prefix.rb +22 -0
  31. data/examples/plugins/dice_roll.rb +38 -0
  32. data/examples/plugins/google.rb +36 -0
  33. data/examples/plugins/hello.rb +21 -0
  34. data/examples/plugins/hooks.rb +34 -0
  35. data/examples/plugins/join_part.rb +41 -0
  36. data/examples/plugins/lambdas.rb +35 -0
  37. data/examples/plugins/last_nick.rb +24 -0
  38. data/examples/plugins/msg.rb +21 -0
  39. data/examples/plugins/multiple_matches.rb +32 -0
  40. data/examples/plugins/own_events.rb +37 -0
  41. data/examples/plugins/seen.rb +44 -0
  42. data/examples/plugins/timer.rb +22 -0
  43. data/examples/plugins/url_shorten.rb +34 -0
  44. data/ircinch.gemspec +43 -0
  45. data/lib/cinch/ban.rb +53 -0
  46. data/lib/cinch/bot.rb +476 -0
  47. data/lib/cinch/cached_list.rb +21 -0
  48. data/lib/cinch/callback.rb +22 -0
  49. data/lib/cinch/channel.rb +465 -0
  50. data/lib/cinch/channel_list.rb +31 -0
  51. data/lib/cinch/configuration/bot.rb +50 -0
  52. data/lib/cinch/configuration/dcc.rb +18 -0
  53. data/lib/cinch/configuration/plugins.rb +43 -0
  54. data/lib/cinch/configuration/sasl.rb +21 -0
  55. data/lib/cinch/configuration/ssl.rb +21 -0
  56. data/lib/cinch/configuration/timeouts.rb +16 -0
  57. data/lib/cinch/configuration.rb +75 -0
  58. data/lib/cinch/constants.rb +535 -0
  59. data/lib/cinch/dcc/dccable_object.rb +39 -0
  60. data/lib/cinch/dcc/incoming/send.rb +149 -0
  61. data/lib/cinch/dcc/incoming.rb +3 -0
  62. data/lib/cinch/dcc/outgoing/send.rb +123 -0
  63. data/lib/cinch/dcc/outgoing.rb +3 -0
  64. data/lib/cinch/dcc.rb +14 -0
  65. data/lib/cinch/exceptions.rb +48 -0
  66. data/lib/cinch/formatting.rb +127 -0
  67. data/lib/cinch/handler.rb +120 -0
  68. data/lib/cinch/handler_list.rb +92 -0
  69. data/lib/cinch/helpers.rb +230 -0
  70. data/lib/cinch/i_support.rb +100 -0
  71. data/lib/cinch/irc.rb +924 -0
  72. data/lib/cinch/log_filter.rb +23 -0
  73. data/lib/cinch/logger/formatted_logger.rb +100 -0
  74. data/lib/cinch/logger/zcbot_logger.rb +26 -0
  75. data/lib/cinch/logger.rb +171 -0
  76. data/lib/cinch/logger_list.rb +88 -0
  77. data/lib/cinch/mask.rb +69 -0
  78. data/lib/cinch/message.rb +397 -0
  79. data/lib/cinch/message_queue.rb +104 -0
  80. data/lib/cinch/mode_parser.rb +78 -0
  81. data/lib/cinch/network.rb +106 -0
  82. data/lib/cinch/open_ended_queue.rb +26 -0
  83. data/lib/cinch/pattern.rb +66 -0
  84. data/lib/cinch/plugin.rb +517 -0
  85. data/lib/cinch/plugin_list.rb +40 -0
  86. data/lib/cinch/rubyext/float.rb +5 -0
  87. data/lib/cinch/rubyext/module.rb +28 -0
  88. data/lib/cinch/rubyext/string.rb +35 -0
  89. data/lib/cinch/sasl/dh_blowfish.rb +73 -0
  90. data/lib/cinch/sasl/diffie_hellman.rb +50 -0
  91. data/lib/cinch/sasl/mechanism.rb +8 -0
  92. data/lib/cinch/sasl/plain.rb +29 -0
  93. data/lib/cinch/sasl.rb +36 -0
  94. data/lib/cinch/syncable.rb +83 -0
  95. data/lib/cinch/target.rb +199 -0
  96. data/lib/cinch/timer.rb +147 -0
  97. data/lib/cinch/user.rb +489 -0
  98. data/lib/cinch/user_list.rb +89 -0
  99. data/lib/cinch/utilities/deprecation.rb +18 -0
  100. data/lib/cinch/utilities/encoding.rb +39 -0
  101. data/lib/cinch/utilities/kernel.rb +15 -0
  102. data/lib/cinch/version.rb +6 -0
  103. data/lib/cinch.rb +7 -0
  104. data/lib/ircinch.rb +7 -0
  105. metadata +205 -0
@@ -0,0 +1,397 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ require_relative "formatting"
6
+
7
+ module Cinch
8
+ # This class serves two purposes. For one, it simply
9
+ # represents incoming messages and allows for querying various
10
+ # details (who sent the message, what kind of message it is, etc).
11
+ #
12
+ # At the same time, it allows **responding** to messages, which
13
+ # means sending messages to either users or channels.
14
+ class Message
15
+ # @return [String]
16
+ attr_reader :raw
17
+
18
+ # @return [String]
19
+ attr_reader :prefix
20
+
21
+ # @return [String]
22
+ attr_reader :command
23
+
24
+ # @return [Array<String>]
25
+ attr_reader :params
26
+
27
+ # @return [Hash]
28
+ attr_reader :tags
29
+
30
+ # @return [Array<Symbol>]
31
+ attr_reader :events
32
+ # @api private
33
+ attr_writer :events
34
+
35
+ # @return [Time]
36
+ # @since 2.0.0
37
+ attr_reader :time
38
+
39
+ # @return [Bot]
40
+ # @since 1.1.0
41
+ attr_reader :bot
42
+
43
+ # @return [User] The user who sent this message
44
+ attr_reader :user
45
+
46
+ # @return [String, nil]
47
+ attr_reader :server
48
+
49
+ # @return [Integer, nil] the numeric error code, if any
50
+ attr_reader :error
51
+
52
+ # @return [String, nil] the command part of an CTCP message
53
+ attr_reader :ctcp_command
54
+
55
+ # @return [Channel] The channel in which this message was sent
56
+ attr_reader :channel
57
+
58
+ # @return [String, nil] the CTCP message, without \001 control characters
59
+ attr_reader :ctcp_message
60
+
61
+ # @return [Array<String>, nil]
62
+ attr_reader :ctcp_args
63
+
64
+ # @return [String, nil]
65
+ attr_reader :message
66
+
67
+ # @return [String, nil] The action message
68
+ # @since 2.0.0
69
+ attr_reader :action_message
70
+
71
+ # @return [Target]
72
+ attr_reader :target
73
+
74
+ # The STATUSMSG mode a channel message was sent to.
75
+ #
76
+ # Some IRC servers allow sending messages limited to people in a
77
+ # channel who have a certain mode. For example, by sending a
78
+ # message to `+#channel`, only people who are voiced, or have a
79
+ # higher mode (op) will receive the message.
80
+ #
81
+ # This attribute contains the mode character the message was sent
82
+ # to, or nil if it was a normal message. For the previous example,
83
+ # this attribute would be set to `"v"`, for voiced.
84
+ #
85
+ # @return [String, nil]
86
+ # @since 2.3.0
87
+ attr_reader :statusmsg_mode
88
+
89
+ def initialize(msg, bot)
90
+ @raw = msg
91
+ @bot = bot
92
+ @matches = {ctcp: {}, action: {}, other: {}}
93
+ @events = []
94
+ @time = Time.now
95
+ @statusmsg_mode = nil
96
+ parse if msg
97
+ end
98
+
99
+ # @api private
100
+ # @return [void]
101
+ def parse
102
+ match = @raw.match(/(?:^@([^:]+))?(?::?(\S+) )?(\S+)(.*)/)
103
+ tags, @prefix, @command, raw_params = match.captures
104
+
105
+ if @bot.irc.network.ngametv?
106
+ if @prefix != "ngame"
107
+ @prefix = "%s!%s@%s" % [@prefix, @prefix, @prefix]
108
+ end
109
+ end
110
+
111
+ @params = parse_params(raw_params)
112
+ @tags = parse_tags(tags)
113
+
114
+ @user = parse_user
115
+ @channel, @statusmsg_mode = parse_channel
116
+ @target = @channel || @user
117
+ @server = parse_server
118
+ @error = parse_error
119
+ @message = parse_message
120
+
121
+ @ctcp_message = parse_ctcp_message
122
+ @ctcp_command = parse_ctcp_command
123
+ @ctcp_args = parse_ctcp_args
124
+
125
+ @action_message = parse_action_message
126
+ end
127
+
128
+ # @group Type checking
129
+
130
+ # @return [Boolean] true if the message is an numeric reply (as
131
+ # opposed to a command)
132
+ def numeric_reply?
133
+ !!@command.match(/^\d{3}$/)
134
+ end
135
+
136
+ # @return [Boolean] true if the message describes an error
137
+ def error?
138
+ !@error.nil?
139
+ end
140
+
141
+ # @return [Boolean] true if this message was sent in a channel
142
+ def channel?
143
+ !@channel.nil?
144
+ end
145
+
146
+ # @return [Boolean] true if the message is an CTCP message
147
+ def ctcp?
148
+ !!(@params.last =~ /\001.+\001/)
149
+ end
150
+
151
+ # @return [Boolean] true if the message is an action (/me)
152
+ # @since 2.0.0
153
+ def action?
154
+ @ctcp_command == "ACTION"
155
+ end
156
+
157
+ # @endgroup
158
+
159
+ # @api private
160
+ # @return [MatchData]
161
+ def match(regexp, type, strip_colors)
162
+ text = ""
163
+ case type
164
+ when :ctcp
165
+ text = ctcp_message
166
+ when :action
167
+ text = action_message
168
+ else
169
+ text = message.to_s
170
+ type = :other
171
+ end
172
+
173
+ if strip_colors
174
+ text = Cinch::Formatting.unformat(text)
175
+ end
176
+
177
+ @matches[type][regexp] ||= text.match(regexp)
178
+ end
179
+
180
+ # @group Replying
181
+
182
+ # Replies to a message, automatically determining if it was a
183
+ # channel or a private message.
184
+ #
185
+ # If the message is a STATUSMSG, i.e. it was send to `+#channel`
186
+ # or `@#channel` instead of `#channel`, the reply will be sent as
187
+ # the same kind of STATUSMSG. See {#statusmsg_mode} for more
188
+ # information on STATUSMSG.
189
+ #
190
+ # @param [String] text the message
191
+ # @param [Boolean] prefix if prefix is true and the message was in
192
+ # a channel, the reply will be prefixed by the nickname of whoever
193
+ # send the mesage
194
+ # @return [void]
195
+ def reply(text, prefix = false)
196
+ text = text.to_s
197
+ if @channel && prefix
198
+ text = text.split("\n").map { |l| "#{user.nick}: #{l}" }.join("\n")
199
+ end
200
+
201
+ reply_target.send(text)
202
+ end
203
+
204
+ # Like {#reply}, but using {Target#safe_send} instead
205
+ #
206
+ # @param (see #reply)
207
+ # @return (see #reply)
208
+ def safe_reply(text, prefix = false)
209
+ text = text.to_s
210
+ if channel && prefix
211
+ text = "#{@user.nick}: #{text}"
212
+ end
213
+ reply_target.safe_send(text)
214
+ end
215
+
216
+ # Reply to a message with an action.
217
+ #
218
+ # For its behaviour with regard to STATUSMSG, see {#reply}.
219
+ #
220
+ # @param [String] text the action message
221
+ # @return [void]
222
+ def action_reply(text)
223
+ text = text.to_s
224
+ reply_target.action(text)
225
+ end
226
+
227
+ # Like {#action_reply}, but using {Target#safe_action} instead
228
+ #
229
+ # @param (see #action_reply)
230
+ # @return (see #action_reply)
231
+ def safe_action_reply(text)
232
+ text = text.to_s
233
+ reply_target.safe_action(text)
234
+ end
235
+
236
+ # Reply to a CTCP message
237
+ #
238
+ # @return [void]
239
+ def ctcp_reply(answer)
240
+ return unless ctcp?
241
+ @user.notice "\001#{@ctcp_command} #{answer}\001"
242
+ end
243
+
244
+ # @endgroup
245
+
246
+ # @return [String]
247
+ # @since 1.1.0
248
+ def to_s
249
+ "#<Cinch::Message @raw=#{@raw.chomp.inspect} @params=#{@params.inspect} channel=#{@channel.inspect} user=#{@user.inspect}>"
250
+ end
251
+
252
+ private
253
+
254
+ def reply_target
255
+ if @channel.nil? || @statusmsg_mode.nil?
256
+ return @target
257
+ end
258
+ prefix = @bot.irc.isupport["PREFIX"][@statusmsg_mode]
259
+ Target.new(prefix + @channel.name, @bot)
260
+ end
261
+
262
+ def regular_command?
263
+ !numeric_reply? # a command can only be numeric or "regular"…
264
+ end
265
+
266
+ def parse_params(raw_params)
267
+ params = []
268
+ if (match = raw_params.match(/(?:^:| :)(.*)$/))
269
+ params = match.pre_match.split(" ")
270
+ params << match[1]
271
+ else
272
+ params = raw_params.split(" ")
273
+ end
274
+
275
+ params
276
+ end
277
+
278
+ def parse_tags(raw_tags)
279
+ return {} if raw_tags.nil?
280
+
281
+ class << self
282
+ def to_symbol(string)
283
+ string.tr(/-/, "_").downcase.to_sym
284
+ end
285
+ end
286
+
287
+ tags = {}
288
+ raw_tags.split(";").each do |tag|
289
+ tag_name, tag_value = tag.split("=")
290
+ if /,/.match?(tag_value)
291
+ tag_value = tag_value.split(",")
292
+ elsif tag_value.nil?
293
+ tag_value = tag_name
294
+ end
295
+ if /\//.match?(tag_name)
296
+ vendor, tag_name = tag_name.split("/")
297
+ tags[to_symbol(vendor)] = {
298
+ to_symbol(tag_name) => tag_value
299
+ }
300
+ else
301
+ tags[to_symbol(tag_name)] = tag_value
302
+ end
303
+ end
304
+ tags
305
+ end
306
+
307
+ def parse_user
308
+ return unless @prefix
309
+ nick = @prefix[/^(\S+)!/, 1]
310
+ user = @prefix[/^\S+!(\S+)@/, 1]
311
+ host = @prefix[/@(\S+)$/, 1]
312
+
313
+ return nil if nick.nil?
314
+ @bot.user_list.find_ensured(user, nick, host)
315
+ end
316
+
317
+ def parse_channel
318
+ # has to be called after parse_params
319
+ return nil if @params.empty?
320
+
321
+ case @command
322
+ when "INVITE", Constants::RPL_CHANNELMODEIS.to_s, Constants::RPL_BANLIST.to_s
323
+ @bot.channel_list.find_ensured(@params[1])
324
+ when Constants::RPL_NAMEREPLY.to_s
325
+ @bot.channel_list.find_ensured(@params[2])
326
+ else
327
+ # Note that this will also find channels for messages that
328
+ # don't actually include a channel parameter. For example
329
+ # `QUIT :#sometext` will be interpreted as a channel. The
330
+ # alternative to the currently used heuristic would be to
331
+ # hardcode a list of commands that provide a channel argument.
332
+ ch, status = privmsg_channel_name(@params.first)
333
+ if ch.nil? && numeric_reply? && @params.size > 1
334
+ ch, status = privmsg_channel_name(@params[1])
335
+ end
336
+ if ch
337
+ [@bot.channel_list.find_ensured(ch), status]
338
+ end
339
+ end
340
+ end
341
+
342
+ def privmsg_channel_name(s)
343
+ chantypes = @bot.irc.isupport["CHANTYPES"]
344
+ statusmsg = @bot.irc.isupport["STATUSMSG"]
345
+ if statusmsg.include?(s[0]) && chantypes.include?(s[1])
346
+ status = @bot.irc.isupport["PREFIX"].invert[s[0]]
347
+ [s[1..], status]
348
+ elsif chantypes.include?(s[0])
349
+ [s, nil]
350
+ end
351
+ end
352
+
353
+ def parse_server
354
+ return unless @prefix
355
+ return if /[@!]/.match?(@prefix)
356
+ @prefix[/^(\S+)/, 1]
357
+ end
358
+
359
+ def parse_error
360
+ return @command.to_i if numeric_reply? && @command[/[45]\d\d/]
361
+ end
362
+
363
+ def parse_message
364
+ # has to be called after parse_params
365
+ if error?
366
+ @error.to_s
367
+ elsif regular_command?
368
+ @params.last
369
+ end
370
+ end
371
+
372
+ def parse_ctcp_message
373
+ # has to be called after parse_params
374
+ return unless ctcp?
375
+ @params.last =~ /\001(.+)\001/
376
+ $1
377
+ end
378
+
379
+ def parse_ctcp_command
380
+ # has to be called after parse_ctcp_message
381
+ return unless ctcp?
382
+ @ctcp_message.split(" ").first
383
+ end
384
+
385
+ def parse_ctcp_args
386
+ # has to be called after parse_ctcp_message
387
+ return unless ctcp?
388
+ @ctcp_message.split(" ")[1..]
389
+ end
390
+
391
+ def parse_action_message
392
+ # has to be called after parse_ctcp_message
393
+ return nil unless action?
394
+ @ctcp_message.split(" ", 2).last
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "open_ended_queue"
4
+
5
+ module Cinch
6
+ # This class manages all outgoing messages, applying rate throttling
7
+ # and fair distribution.
8
+ #
9
+ # @api private
10
+ class MessageQueue
11
+ def initialize(socket, bot)
12
+ @socket = socket
13
+ @bot = bot
14
+ @queues = {generic: OpenEndedQueue.new}
15
+ @queues_to_process = Queue.new
16
+ @queued_queues = Set.new
17
+ @mutex = Mutex.new
18
+ @time_since_last_send = nil
19
+ @log = []
20
+ end
21
+
22
+ # @return [void]
23
+ def queue(message)
24
+ command, target, _ = message.split(" ", 3)
25
+
26
+ queue = nil
27
+ case command
28
+ when "PRIVMSG", "NOTICE"
29
+ @mutex.synchronize do
30
+ # we are assuming that each message has only one target,
31
+ # which will be true as long as the user does not send raw
32
+ # messages.
33
+ #
34
+ # this assumption is also reflected in the computation of
35
+ # passed time and processed messages, since our score does
36
+ # not take weights into account.
37
+ queue = @queues[target] ||= OpenEndedQueue.new
38
+ end
39
+ else
40
+ queue = @queues[:generic]
41
+ end
42
+ queue << message
43
+
44
+ @mutex.synchronize do
45
+ unless @queued_queues.include?(queue)
46
+ @queued_queues << queue
47
+ @queues_to_process << queue
48
+ end
49
+ end
50
+ end
51
+
52
+ # @return [void]
53
+ def process!
54
+ loop do
55
+ wait
56
+ process_one
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def wait
63
+ if @log.size > 1
64
+ mps = @bot.config.messages_per_second || @bot.irc.network.default_messages_per_second
65
+ max_queue_size = @bot.config.server_queue_size || @bot.irc.network.default_server_queue_size
66
+
67
+ time_passed = @log.last - @log.first
68
+
69
+ messages_processed = (time_passed * mps).floor
70
+ effective_size = @log.size - messages_processed
71
+
72
+ if effective_size <= 0
73
+ @log.clear
74
+ elsif effective_size >= max_queue_size
75
+ sleep 1.0 / mps
76
+ end
77
+ end
78
+ end
79
+
80
+ def process_one
81
+ queue = @queues_to_process.pop
82
+ message = queue.pop.to_s.each_line.first.chomp
83
+
84
+ if queue.empty?
85
+ @mutex.synchronize do
86
+ @queued_queues.delete(queue)
87
+ end
88
+ else
89
+ @queues_to_process << queue
90
+ end
91
+
92
+ begin
93
+ to_send = Cinch::Utilities::Encoding.encode_outgoing(message, @bot.config.encoding)
94
+ @socket.write(to_send + "\r\n")
95
+ @log << Time.now
96
+ @bot.loggers.outgoing(message)
97
+
98
+ @time_since_last_send = Time.now
99
+ rescue IOError
100
+ @bot.loggers.error "Could not send message (connectivity problems): #{message}"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "exceptions"
4
+
5
+ module Cinch
6
+ # @api private
7
+ # @since 1.1.0
8
+ module ModeParser
9
+ ERR_EMPTY_STRING = "Empty mode string"
10
+ MalformedError = Struct.new(:modes)
11
+ EmptySequenceError = Struct.new(:modes)
12
+ NotEnoughParametersError = Struct.new(:op)
13
+ TooManyParametersError = Struct.new(:modes, :params)
14
+
15
+ # @param [String] modes The mode string as sent by the server
16
+ # @param [Array<String>] params Parameters belonging to the modes
17
+ # @param [Hash{:add, :remove => Array<String>}] param_modes
18
+ # A mapping describing which modes require parameters
19
+ # @return [(Array<(Symbol<:add, :remove>, String<char>, String<param>), foo)]
20
+ def self.parse_modes(modes, params, param_modes = {})
21
+ if modes.size == 0
22
+ return nil, ERR_EMPTY_STRING
23
+ # raise Exceptions::InvalidModeString, 'Empty mode string'
24
+ end
25
+
26
+ if !/[+-]/.match?(modes[0])
27
+ return nil, MalformedError.new(modes)
28
+ # raise Exceptions::InvalidModeString, "Malformed modes string: %s" % modes
29
+ end
30
+
31
+ changes = []
32
+
33
+ direction = nil
34
+ count = -1
35
+
36
+ modes.each_char do |ch|
37
+ if /[+-]/.match?(ch)
38
+ if count == 0
39
+ return changes, EmptySequenceError.new(modes)
40
+ # raise Exceptions::InvalidModeString, 'Empty mode sequence: %s' % modes
41
+ end
42
+
43
+ direction = case ch
44
+ when "+"
45
+ :add
46
+ when "-"
47
+ :remove
48
+ end
49
+ count = 0
50
+ else
51
+ param = nil
52
+ if param_modes.has_key?(direction) && param_modes[direction].include?(ch)
53
+ if params.size > 0
54
+ param = params.shift
55
+ else
56
+ return changes, NotEnoughParametersError.new(ch)
57
+ # raise Exceptions::InvalidModeString, 'Not enough parameters: %s' % ch.inspect
58
+ end
59
+ end
60
+ changes << [direction, ch, param]
61
+ count += 1
62
+ end
63
+ end
64
+
65
+ if params.size > 0
66
+ return changes, TooManyParametersError.new(modes, params)
67
+ # raise Exceptions::InvalidModeString, 'Too many parameters: %s %s' % [modes, params]
68
+ end
69
+
70
+ if count == 0
71
+ return changes, EmptySequenceError.new(modes)
72
+ # raise Exceptions::InvalidModeString, 'Empty mode sequence: %s' % modes
73
+ end
74
+
75
+ [changes, nil]
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cinch
4
+ # This class allows querying the IRC network for its name and used
5
+ # server software as well as certain non-standard behaviour.
6
+ #
7
+ # @since 2.0.0
8
+ class Network
9
+ # @return [Symbol] The name of the network. `:unknown` if the
10
+ # network couldn't be detected.
11
+ attr_reader :name
12
+
13
+ # @api private
14
+ attr_writer :name
15
+
16
+ # @return [Symbol] The server software used by the network.
17
+ # `:unknown` if the software couldn't be detected.
18
+ attr_reader :ircd
19
+
20
+ # @api private
21
+ attr_writer :ircd
22
+
23
+ # @return [Array<Symbol>] All client capabilities supported by the
24
+ # network.
25
+ attr_reader :capabilities
26
+
27
+ # @api private
28
+ attr_writer :capabilities
29
+
30
+ # @param [Symbol] name
31
+ # @param [Symbol] ircd
32
+ # @api private
33
+ # @note The user should not create instances of this class but use
34
+ # {IRC#network} instead.
35
+ def initialize(name, ircd)
36
+ @name = name
37
+ @ircd = ircd
38
+ @capabilities = []
39
+ end
40
+
41
+ # @return [String, nil] The mode used for getting the list of
42
+ # channel owners, if any
43
+ def owner_list_mode
44
+ return "q" if @ircd == :unreal || @ircd == :inspircd
45
+ end
46
+
47
+ # @return [String, nil] The mode used for getting the list of
48
+ # channel quiets, if any
49
+ def quiet_list_mode
50
+ return "q" if @ircd == :"ircd-seven"
51
+ end
52
+
53
+ # @return [Boolean] Does WHOIS only support one argument?
54
+ def whois_only_one_argument?
55
+ @name == :jtv
56
+ end
57
+
58
+ # @return [Boolean] True if connected to NgameTV
59
+ def ngametv?
60
+ @name == :ngametv
61
+ end
62
+
63
+ # @return [Boolean] True if connected to JTV
64
+ def jtv?
65
+ @name == :jtv
66
+ end
67
+
68
+ # @return [Boolean] True if we do not know which network we are
69
+ # connected to
70
+ def unknown_network?
71
+ @name == :unknown
72
+ end
73
+
74
+ # @return [Boolean] True if we do not know which software the
75
+ # server is running
76
+ def unknown_ircd?
77
+ @ircd == :unknown
78
+ end
79
+
80
+ # Note for the default_* methods: Always make sure to return a
81
+ # value for when no network/ircd was detected so that MessageQueue
82
+ # doesn't break.
83
+
84
+ # @return [Numeric] The `messages per second` value that best suits
85
+ # the current network
86
+ def default_messages_per_second
87
+ case @name
88
+ when :freenode
89
+ 0.7
90
+ else
91
+ 0.5
92
+ end
93
+ end
94
+
95
+ # @return [Integer] The `server queue size` value that best suits
96
+ # the current network
97
+ def default_server_queue_size
98
+ case @name
99
+ when :quakenet
100
+ 40
101
+ else
102
+ 10
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Like Ruby's Queue class, but allowing both pushing and unshifting
4
+ # objects.
5
+ #
6
+ # @api private
7
+ class OpenEndedQueue < Queue
8
+ # @param [Object] obj
9
+ # @return [void]
10
+ def unshift(obj)
11
+ t = nil
12
+ @mutex.synchronize {
13
+ @que.unshift obj
14
+ begin
15
+ t = @waiting.shift
16
+ t&.wakeup
17
+ rescue ThreadError
18
+ retry
19
+ end
20
+ }
21
+ begin
22
+ t&.run
23
+ rescue ThreadError
24
+ end
25
+ end
26
+ end