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
data/lib/cinch/bot.rb ADDED
@@ -0,0 +1,476 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+ require "socket"
5
+
6
+ require_relative "rubyext/module"
7
+ require_relative "rubyext/string"
8
+ require_relative "rubyext/float"
9
+
10
+ require_relative "exceptions"
11
+
12
+ require_relative "handler"
13
+ require_relative "helpers"
14
+
15
+ require_relative "logger_list"
16
+ require_relative "logger"
17
+
18
+ require_relative "logger/formatted_logger"
19
+ require_relative "syncable"
20
+ require_relative "message"
21
+ require_relative "message_queue"
22
+ require_relative "irc"
23
+ require_relative "target"
24
+ require_relative "channel"
25
+ require_relative "user"
26
+ require_relative "constants"
27
+ require_relative "callback"
28
+ require_relative "ban"
29
+ require_relative "mask"
30
+ require_relative "i_support"
31
+ require_relative "plugin"
32
+ require_relative "pattern"
33
+ require_relative "mode_parser"
34
+ require_relative "dcc"
35
+ require_relative "sasl"
36
+
37
+ require_relative "handler_list"
38
+ require_relative "cached_list"
39
+ require_relative "channel_list"
40
+ require_relative "user_list"
41
+ require_relative "plugin_list"
42
+
43
+ require_relative "timer"
44
+ require_relative "formatting"
45
+
46
+ require_relative "configuration"
47
+ require_relative "configuration/bot"
48
+ require_relative "configuration/plugins"
49
+ require_relative "configuration/ssl"
50
+ require_relative "configuration/timeouts"
51
+ require_relative "configuration/dcc"
52
+ require_relative "configuration/sasl"
53
+
54
+ module Cinch
55
+ # @attr nick
56
+ # @version 2.0.0
57
+ class Bot < User
58
+ include Helpers
59
+
60
+ # @return [Configuration::Bot]
61
+ # @version 2.0.0
62
+ attr_reader :config
63
+
64
+ # The underlying IRC connection
65
+ #
66
+ # @return [IRC]
67
+ attr_reader :irc
68
+
69
+ # The logger list containing all loggers
70
+ #
71
+ # @return [LoggerList]
72
+ # @since 2.0.0
73
+ attr_accessor :loggers
74
+
75
+ # @return [Array<Channel>] All channels the bot currently is in
76
+ attr_reader :channels
77
+
78
+ # @return [PluginList] The {PluginList} giving access to
79
+ # (un)loading plugins
80
+ # @version 2.0.0
81
+ attr_reader :plugins
82
+
83
+ # @return [Boolean] whether the bot is in the process of disconnecting
84
+ attr_reader :quitting
85
+
86
+ # @return [UserList] All {User users} the bot knows about.
87
+ # @see UserList
88
+ # @since 1.1.0
89
+ attr_reader :user_list
90
+
91
+ # @return [ChannelList] All {Channel channels} the bot knows about.
92
+ # @see ChannelList
93
+ # @since 1.1.0
94
+ attr_reader :channel_list
95
+
96
+ # @return [Boolean]
97
+ # @api private
98
+ attr_accessor :last_connection_was_successful
99
+
100
+ # @return [Callback]
101
+ # @api private
102
+ attr_reader :callback
103
+
104
+ # The {HandlerList}, providing access to all registered plugins
105
+ # and plugin manipulation as well as {HandlerList#dispatch calling handlers}.
106
+ #
107
+ # @return [HandlerList]
108
+ # @see HandlerList
109
+ # @since 2.0.0
110
+ attr_reader :handlers
111
+
112
+ # The bot's modes.
113
+ #
114
+ # @return [Array<String>]
115
+ # @since 2.0.0
116
+ attr_reader :modes
117
+
118
+ # @group Helper methods
119
+
120
+ # Define helper methods in the context of the bot.
121
+ #
122
+ # @yield Expects a block containing method definitions
123
+ # @return [void]
124
+ def helpers(&b)
125
+ @callback.instance_eval(&b)
126
+ end
127
+
128
+ # Since Cinch uses threads, all handlers can be run
129
+ # simultaneously, even the same handler multiple times. This also
130
+ # means, that your code has to be thread-safe. Most of the time,
131
+ # this is not a problem, but if you are accessing stored data, you
132
+ # will most likely have to synchronize access to it. Instead of
133
+ # managing all mutexes yourself, Cinch provides a synchronize
134
+ # method, which takes a name and block.
135
+ #
136
+ # Synchronize blocks with the same name share the same mutex,
137
+ # which means that only one of them will be executed at a time.
138
+ #
139
+ # @param [String, Symbol] name a name for the synchronize block.
140
+ # @return [void]
141
+ # @yield
142
+ #
143
+ # @example
144
+ # configure do |c|
145
+ # …
146
+ # @i = 0
147
+ # end
148
+ #
149
+ # on :channel, /^start counting!/ do
150
+ # synchronize(:my_counter) do
151
+ # 10.times do
152
+ # val = @i
153
+ # # at this point, another thread might've incremented :i already.
154
+ # # this thread wouldn't know about it, though.
155
+ # @i = val + 1
156
+ # end
157
+ # end
158
+ # end
159
+ def synchronize(name, &block)
160
+ # Must run the default block +/ fetch in a thread safe way in order to
161
+ # ensure we always get the same mutex for a given name.
162
+ semaphore = @semaphores_mutex.synchronize { @semaphores[name] }
163
+ semaphore.synchronize(&block)
164
+ end
165
+
166
+ # @endgroup
167
+
168
+ # @group Events &amp; Plugins
169
+
170
+ # Registers a handler.
171
+ #
172
+ # @param [String, Symbol, Integer] event the event to match. For a
173
+ # list of available events, check the {file:docs/events.md Events
174
+ # documentation}.
175
+ #
176
+ # @param [Regexp, Pattern, String] regexp every message of the
177
+ # right event will be checked against this argument and the event
178
+ # will only be called if it matches
179
+ #
180
+ # @param [Array<Object>] args Arguments that should be passed to
181
+ # the block, additionally to capture groups of the regexp.
182
+ #
183
+ # @yieldparam [Array<String>] args each capture group of the regex will
184
+ # be one argument to the block.
185
+ #
186
+ # @return [Handler] The handlers that have been registered
187
+ def on(event, regexp = //, *args, &block)
188
+ event = event.to_s.to_sym
189
+
190
+ pattern = case regexp
191
+ when Pattern
192
+ regexp
193
+ when Regexp
194
+ Pattern.new(nil, regexp, nil)
195
+ else
196
+ if event == :ctcp
197
+ Pattern.generate(:ctcp, regexp)
198
+ else
199
+ Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
200
+ end
201
+ end
202
+
203
+ handler = Handler.new(self, event, pattern, {args: args, execute_in_callback: true}, &block)
204
+ @handlers.register(handler)
205
+
206
+ handler
207
+ end
208
+
209
+ # @endgroup
210
+ # @group Bot Control
211
+
212
+ # This method is used to set a bot's options. It indeed does
213
+ # nothing else but yielding {Bot#config}, but it makes for a nice DSL.
214
+ #
215
+ # @yieldparam [Struct] config the bot's config
216
+ # @return [void]
217
+ def configure
218
+ yield @config
219
+ end
220
+
221
+ # Disconnects from the server.
222
+ #
223
+ # @param [String] message The quit message to send while quitting
224
+ # @return [void]
225
+ def quit(message = nil)
226
+ @quitting = true
227
+ command = message ? "QUIT :#{message}" : "QUIT"
228
+
229
+ @irc.send command
230
+ end
231
+
232
+ # Connects the bot to a server.
233
+ #
234
+ # @param [Boolean] plugins Automatically register plugins from
235
+ # `@config.plugins.plugins`?
236
+ # @return [void]
237
+ def start(plugins = true)
238
+ @reconnects = 0
239
+ @plugins.register_plugins(@config.plugins.plugins) if plugins
240
+
241
+ loop do
242
+ @user_list.each do |user|
243
+ user.in_whois = false
244
+ user.unsync_all
245
+ end # reset state of all users
246
+
247
+ @channel_list.each do |channel|
248
+ channel.unsync_all
249
+ end # reset state of all channels
250
+
251
+ @channels = [] # reset list of channels the bot is in
252
+
253
+ @join_handler&.unregister
254
+ @join_timer&.stop
255
+
256
+ join_lambda = lambda { @config.channels.each { |channel| Channel(channel).join } }
257
+
258
+ if @config.delay_joins.is_a?(Symbol)
259
+ @join_handler = join_handler = on(@config.delay_joins) {
260
+ join_handler.unregister
261
+ join_lambda.call
262
+ }
263
+ else
264
+ @join_timer = Timer.new(self, interval: @config.delay_joins, shots: 1) {
265
+ join_lambda.call
266
+ }
267
+ end
268
+
269
+ @modes = []
270
+
271
+ @loggers.info "Connecting to #{@config.server}:#{@config.port}"
272
+ @irc = IRC.new(self)
273
+ @irc.start
274
+
275
+ if @config.reconnect && !@quitting
276
+ # double the delay for each unsuccesful reconnection attempt
277
+ if @last_connection_was_successful
278
+ @reconnects = 0
279
+ @last_connection_was_successful = false
280
+ else
281
+ @reconnects += 1
282
+ end
283
+
284
+ # Throttle reconnect attempts
285
+ wait = 2**@reconnects
286
+ wait = @config.max_reconnect_delay if wait > @config.max_reconnect_delay
287
+ @loggers.info "Waiting #{wait} seconds before reconnecting"
288
+ start_time = Time.now
289
+ while !@quitting && (Time.now - start_time) < wait
290
+ sleep 1
291
+ end
292
+ end
293
+ break unless @config.reconnect && !@quitting
294
+ end
295
+ end
296
+
297
+ # @endgroup
298
+ # @group Channel Control
299
+
300
+ # Join a channel.
301
+ #
302
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
303
+ # @param [String] key optionally the key of the channel
304
+ # @return [Channel] The joined channel
305
+ # @see Channel#join
306
+ def join(channel, key = nil)
307
+ channel = Channel(channel)
308
+ channel.join(key)
309
+
310
+ channel
311
+ end
312
+
313
+ # Part a channel.
314
+ #
315
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
316
+ # @param [String] reason an optional reason/part message
317
+ # @return [Channel] The channel that was left
318
+ # @see Channel#part
319
+ def part(channel, reason = nil)
320
+ channel = Channel(channel)
321
+ channel.part(reason)
322
+
323
+ channel
324
+ end
325
+
326
+ # @endgroup
327
+
328
+ # @return [Boolean] True if the bot reports ISUPPORT violations as
329
+ # exceptions.
330
+ def strict?
331
+ @config.strictness == :strict
332
+ end
333
+
334
+ # @yield
335
+ def initialize(&b)
336
+ @config = Configuration::Bot.new
337
+
338
+ @loggers = LoggerList.new
339
+ @loggers << Logger::FormattedLogger.new($stderr, level: @config.default_logger_level)
340
+ @handlers = HandlerList.new
341
+ @semaphores_mutex = Mutex.new
342
+ @semaphores = Hash.new { |h, k| h[k] = Mutex.new }
343
+ @callback = Callback.new(self)
344
+ @channels = []
345
+ @quitting = false
346
+ @modes = []
347
+
348
+ @user_list = UserList.new(self)
349
+ @channel_list = ChannelList.new(self)
350
+ @plugins = PluginList.new(self)
351
+
352
+ @join_handler = nil
353
+ @join_timer = nil
354
+
355
+ super(nil, self)
356
+ instance_eval(&b) if b
357
+ end
358
+
359
+ # @since 2.0.0
360
+ # @return [self]
361
+ # @api private
362
+ def bot
363
+ # This method is needed for the Helpers interface
364
+ self
365
+ end
366
+
367
+ # Sets a mode on the bot.
368
+ #
369
+ # @param [String] mode
370
+ # @return [void]
371
+ # @since 2.0.0
372
+ # @see Bot#modes
373
+ # @see Bot#unset_mode
374
+ def set_mode(mode)
375
+ @modes << mode unless @modes.include?(mode)
376
+ @irc.send "MODE #{nick} +#{mode}"
377
+ end
378
+
379
+ # Unsets a mode on the bot.
380
+ #
381
+ # @param [String] mode
382
+ # @return [void]
383
+ # @since 2.0.0
384
+ def unset_mode(mode)
385
+ @modes.delete(mode)
386
+ @irc.send "MODE #{nick} -#{mode}"
387
+ end
388
+
389
+ # @since 2.0.0
390
+ def modes=(modes)
391
+ (@modes - modes).each do |mode|
392
+ unset_mode(mode)
393
+ end
394
+
395
+ (modes - @modes).each do |mode|
396
+ set_mode(mode)
397
+ end
398
+ end
399
+
400
+ # Used for updating the bot's nick from within the IRC parser.
401
+ #
402
+ # @param [String] nick
403
+ # @api private
404
+ # @return [String]
405
+ def set_nick(nick)
406
+ @name = nick
407
+ end
408
+
409
+ # The bot's nickname.
410
+ # @overload nick=(new_nick)
411
+ # @raise [Exceptions::NickTooLong] Raised if the bot is
412
+ # operating in {#strict? strict mode} and the new nickname is
413
+ # too long
414
+ # @return [String]
415
+ # @overload nick
416
+ # @return [String]
417
+ # @return [String]
418
+ def nick
419
+ @name
420
+ end
421
+
422
+ def nick=(new_nick)
423
+ if new_nick.size > @irc.isupport["NICKLEN"] && strict?
424
+ raise Exceptions::NickTooLong, new_nick
425
+ end
426
+ @config.nick = new_nick
427
+ @irc.send "NICK #{new_nick}"
428
+ end
429
+
430
+ # Gain oper privileges.
431
+ #
432
+ # @param [String] password
433
+ # @param [String] user The username to use. Defaults to the bot's
434
+ # nickname
435
+ # @since 2.1.0
436
+ # @return [void]
437
+ def oper(password, user = nil)
438
+ user ||= nick
439
+ @irc.send "OPER #{user} #{password}"
440
+ end
441
+
442
+ # Try to create a free nick, first by cycling through all
443
+ # available alternatives and then by appending underscores.
444
+ #
445
+ # @param [String] base The base nick to start trying from
446
+ # @api private
447
+ # @return [String]
448
+ # @since 2.0.0
449
+ def generate_next_nick!(base = nil)
450
+ nicks = @config.nicks || []
451
+
452
+ if base
453
+ # if `base` is not in our list of nicks to try, assume that it's
454
+ # custom and just append an underscore
455
+ if !nicks.include?(base)
456
+ new_nick = base + "_"
457
+ else
458
+ # if we have a base, try the next nick or append an
459
+ # underscore if no more nicks are left
460
+ new_index = nicks.index(base) + 1
461
+ new_nick = nicks[new_index] || base + "_"
462
+ end
463
+ else
464
+ # if we have no base, try the first possible nick
465
+ new_nick = @config.nicks ? @config.nicks.first : @config.nick
466
+ end
467
+
468
+ @config.nick = new_nick
469
+ end
470
+
471
+ # @return [String]
472
+ def inspect
473
+ "#<Bot nick=#{@name.inspect}>"
474
+ end
475
+ end
476
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cinch
4
+ # @api private
5
+ # @since 2.0.0
6
+ # @version 1.1.0
7
+ # @note In prior versions, this class was called CacheManager
8
+ class CachedList
9
+ include Enumerable
10
+
11
+ def initialize(bot)
12
+ @bot = bot
13
+ @cache = {}
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ def each(&block)
18
+ @cache.each_value(&block)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cinch
4
+ # Class used for encapsulating handlers to prevent them from
5
+ # overwriting instance variables in {Bot}
6
+ #
7
+ # @api private
8
+ class Callback
9
+ include Helpers
10
+
11
+ # @return [Bot]
12
+ attr_reader :bot
13
+ def initialize(bot)
14
+ @bot = bot
15
+ end
16
+
17
+ # (see Bot#synchronize)
18
+ def synchronize(name, &block)
19
+ @bot.synchronize(name, &block)
20
+ end
21
+ end
22
+ end