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,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require "socket"
5
+
6
+ module Cinch
7
+ module DCC
8
+ module Incoming
9
+ # DCC SEND is a protocol for transferring files, usually found
10
+ # in IRC. While the handshake, i.e. the details of the file
11
+ # transfer, are transferred over IRC, the actual file transfer
12
+ # happens directly between two clients. As such it doesn't put
13
+ # stress on the IRC server.
14
+ #
15
+ # When someone tries to send a file to the bot, the `:dcc_send`
16
+ # event will be triggered, in which the DCC request can be
17
+ # inspected and optionally accepted.
18
+ #
19
+ # The event handler receives the plain message object as well as
20
+ # an instance of this class. That instance contains information
21
+ # about {#filename the suggested file name} (in a sanitized way)
22
+ # and allows for checking the origin.
23
+ #
24
+ # It is advised to reject transfers that seem to originate from
25
+ # a {#from_private_ip? private IP} or {#from_localhost? the
26
+ # local IP itself} unless that is expected. Otherwise, specially
27
+ # crafted requests could cause the bot to connect to internal
28
+ # services.
29
+ #
30
+ # Finally, the file transfer can be {#accept accepted} and
31
+ # written to any object that implements a `#<<` method, which
32
+ # includes File objects as well as plain strings.
33
+ #
34
+ # @example Saving a transfer to a temporary file
35
+ # require "tempfile"
36
+ #
37
+ # listen_to :dcc_send, method: :incoming_dcc
38
+ # def incoming_dcc(m, dcc)
39
+ # if dcc.from_private_ip? || dcc.from_localhost?
40
+ # @bot.loggers.debug "Not accepting potentially dangerous file transfer"
41
+ # return
42
+ # end
43
+ #
44
+ # t = Tempfile.new(dcc.filename)
45
+ # dcc.accept(t)
46
+ # t.close
47
+ # end
48
+ #
49
+ # @attr_reader filename
50
+ class Send
51
+ # @private
52
+ PRIVATE_NETS = [IPAddr.new("fc00::/7"),
53
+ IPAddr.new("10.0.0.0/8"),
54
+ IPAddr.new("172.16.0.0/12"),
55
+ IPAddr.new("192.168.0.0/16")]
56
+
57
+ # @private
58
+ LOCAL_NETS = [IPAddr.new("127.0.0.0/8"),
59
+ IPAddr.new("::1/128")]
60
+
61
+ # @return [User]
62
+ attr_reader :user
63
+
64
+ # @return [Integer]
65
+ attr_reader :size
66
+
67
+ # @return [String]
68
+ attr_reader :ip
69
+
70
+ # @return [Fixnum]
71
+ attr_reader :port
72
+
73
+ # @param [Hash] opts
74
+ # @option opts [User] user
75
+ # @option opts [String] filename
76
+ # @option opts [Integer] size
77
+ # @option opts [String] ip
78
+ # @option opts [Fixnum] port
79
+ # @api private
80
+ def initialize(opts)
81
+ @user, @filename, @size, @ip, @port = opts.values_at(:user, :filename, :size, :ip, :port)
82
+ end
83
+
84
+ # @return [String] The basename of the file name, with
85
+ # (back)slashes removed.
86
+ def filename
87
+ File.basename(File.expand_path(@filename)).delete("/\\")
88
+ end
89
+
90
+ # This method is used for accepting a DCC SEND offer. It
91
+ # expects an object to save the result to (usually an instance
92
+ # of IO or String).
93
+ #
94
+ # @param [#<<] io The object to write the data to.
95
+ # @return [Boolean] True if the transfer finished
96
+ # successfully, false otherwise.
97
+ # @note This method blocks.
98
+ # @example Saving to a file
99
+ # f = File.open("/tmp/foo", "w")
100
+ # dcc.accept(f)
101
+ # f.close
102
+ #
103
+ # @example Saving to a string
104
+ # s = ""
105
+ # dcc.accept(s)
106
+ def accept(io)
107
+ socket = TCPSocket.new(@ip, @port)
108
+ total = 0
109
+
110
+ while (buf = socket.readpartial(8192))
111
+ total += buf.bytesize
112
+
113
+ begin
114
+ socket.write_nonblock [total].pack("N")
115
+ rescue Errno::EWOULDBLOCK, Errno::AGAIN
116
+ # Nobody cares about ACKs, really. And if the sender
117
+ # couldn't receive it at this point, they probably don't
118
+ # care, either.
119
+ end
120
+ io << buf
121
+
122
+ # Break here in case the sender doesn't close the
123
+ # connection on the final ACK.
124
+ break if total == @size
125
+ end
126
+
127
+ socket.close
128
+ true
129
+ rescue EOFError
130
+ false
131
+ end
132
+
133
+ # @return [Boolean] True if the DCC originates from a private ip
134
+ # @see #from_localhost?
135
+ def from_private_ip?
136
+ ip = IPAddr.new(@ip)
137
+ PRIVATE_NETS.any? { |n| n.include?(ip) }
138
+ end
139
+
140
+ # @return [Boolean] True if the DCC originates from localhost
141
+ # @see #from_private_ip?
142
+ def from_localhost?
143
+ ip = IPAddr.new(@ip)
144
+ LOCAL_NETS.any? { |n| n.include?(ip) }
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "incoming/send"
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require "socket"
5
+ require "timeout"
6
+
7
+ module Cinch
8
+ module DCC
9
+ module Outgoing
10
+ # DCC SEND is a protocol for transferring files, usually found
11
+ # in IRC. While the handshake, i.e. the details of the file
12
+ # transfer, are transferred over IRC, the actual file transfer
13
+ # happens directly between two clients. As such it doesn't put
14
+ # stress on the IRC server.
15
+ #
16
+ # Cinch allows sending files by either using
17
+ # {Cinch::User#dcc_send}, which takes care of all parameters as
18
+ # well as setting up resume support, or by creating instances of
19
+ # this class directly. The latter will only be useful to people
20
+ # working on the Cinch code itself.
21
+ #
22
+ # {Cinch::User#dcc_send} expects an object to send as well as
23
+ # optionaly a file name, which is sent to the receiver as a
24
+ # suggestion where to save the file. If no file name is
25
+ # provided, the method will use the object's `#path` method to
26
+ # determine it.
27
+ #
28
+ # Any object that implements {DCC::DCCableObject} can be sent,
29
+ # but sending files will probably be the most common case.
30
+ #
31
+ # If you're behind a NAT it is necessary to explicitly set the
32
+ # external IP using the {file:docs/bot_options.md#dccownip dcc.own_ip
33
+ # option}.
34
+ #
35
+ # @example Sending a file to a user
36
+ # match "send me something"
37
+ # def execute(m)
38
+ # m.user.dcc_send(open("/tmp/cookies"))
39
+ # end
40
+ class Send
41
+ # @param [Hash] opts
42
+ # @option opts [User] receiver
43
+ # @option opts [String] filename
44
+ # @option opts [File] io
45
+ # @option opts [String] own_ip
46
+ def initialize(opts = {})
47
+ @receiver, @filename, @io, @own_ip = opts.values_at(:receiver, :filename, :io, :own_ip)
48
+ end
49
+
50
+ # Start the server
51
+ #
52
+ # @return [void]
53
+ def start_server
54
+ @socket = TCPServer.new(0)
55
+ @socket.listen(1)
56
+ end
57
+
58
+ # Send the handshake to the user.
59
+ #
60
+ # @return [void]
61
+ def send_handshake
62
+ handshake = "\001DCC SEND %s %d %d %d\001" % [@filename, IPAddr.new(@own_ip).to_i, port, @io.size]
63
+ @receiver.send(handshake)
64
+ end
65
+
66
+ # Listen for an incoming connection.
67
+ #
68
+ # This starts listening for an incoming connection to the server
69
+ # started by {#start_server}. After a client successfully
70
+ # connected, the server socket will be closed and the file
71
+ # transferred to the client.
72
+ #
73
+ # @raise [Timeout::Error] Raised if the receiver did not connect
74
+ # within 30 seconds
75
+ # @return [void]
76
+ # @note This method blocks.
77
+ def listen
78
+ fd = nil
79
+ Timeout.timeout(30) do
80
+ fd, _ = @socket.accept
81
+ end
82
+ send_data(fd)
83
+ fd.close
84
+ ensure
85
+ @socket.close
86
+ end
87
+
88
+ # Seek to `pos` in the data.
89
+ #
90
+ # @param [Integer] pos
91
+ # @return [void]
92
+ # @api private
93
+ def seek(pos)
94
+ @io.seek(pos)
95
+ end
96
+
97
+ # @return [Fixnum] The port used for the socket
98
+ def port
99
+ @port ||= @socket.addr[1]
100
+ end
101
+
102
+ private
103
+
104
+ def send_data(fd)
105
+ @io.advise(:sequential)
106
+
107
+ while (chunk = @io.read(8096))
108
+ loop do
109
+ rs, ws = IO.select([fd], [fd])
110
+ if !rs.empty?
111
+ rs.first.recv(8096)
112
+ end
113
+ if !ws.empty?
114
+ ws.first.write(chunk)
115
+ break
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "outgoing/send"
data/lib/cinch/dcc.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dcc/incoming"
4
+ require_relative "dcc/outgoing"
5
+
6
+ module Cinch
7
+ # Cinch supports the following DCC commands:
8
+ #
9
+ # - SEND (both {DCC::Incoming::Send incoming} and
10
+ # {DCC::Outgoing::Send outgoing})
11
+ # @since 2.0.0
12
+ module DCC
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cinch
4
+ # A collection of exceptions.
5
+ module Exceptions
6
+ # Generic error. Superclass for all Cinch-specific errors.
7
+ class Generic < ::StandardError
8
+ end
9
+
10
+ # Generic error when an argument is too long.
11
+ class ArgumentTooLong < Generic
12
+ end
13
+
14
+ # Error that is raised when a topic is too long to be set.
15
+ class TopicTooLong < ArgumentTooLong
16
+ end
17
+
18
+ # Error that is raised when a nick is too long to be used.
19
+ class NickTooLong < ArgumentTooLong
20
+ end
21
+
22
+ # Error that is raised when a kick reason is too long.
23
+ class KickReasonTooLong < ArgumentTooLong
24
+ end
25
+
26
+ # Raised whenever Cinch discovers a feature it doesn't support
27
+ # yet.
28
+ class UnsupportedFeature < Generic
29
+ end
30
+
31
+ # Raised when Cinch discovers a user or channel mode, which it
32
+ # doesn't support yet.
33
+ class UnsupportedMode < Generic
34
+ def initialize(mode)
35
+ super "Cinch does not support the mode '#{mode}' yet."
36
+ end
37
+ end
38
+
39
+ # Error stating that an invalid mode string was encountered.
40
+ class InvalidModeString < Generic
41
+ end
42
+
43
+ # Raised when a synced attribute hasn't been available for too
44
+ # long.
45
+ class SyncedAttributeNotAvailable < Generic
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cinch
4
+ # This module can be used for adding and removing colors and
5
+ # formatting to/from messages.
6
+ #
7
+ # The format codes used are those defined by mIRC, which are also
8
+ # the ones supported by most clients.
9
+ #
10
+ # For usage instructions and examples, see {.format}.
11
+ #
12
+ # List of valid colors
13
+ # =========================
14
+ # - aqua
15
+ # - black
16
+ # - blue
17
+ # - brown
18
+ # - green
19
+ # - grey
20
+ # - lime
21
+ # - orange
22
+ # - pink
23
+ # - purple
24
+ # - red
25
+ # - royal
26
+ # - silver
27
+ # - teal
28
+ # - white
29
+ # - yellow
30
+ #
31
+ # List of valid attributes
32
+ # ========================
33
+ # - bold
34
+ # - italic
35
+ # - reverse/reversed
36
+ # - underline/underlined
37
+ #
38
+ # Other
39
+ # =====
40
+ # - reset (Resets all formatting to the client's defaults)
41
+ #
42
+ # @since 2.0.0
43
+ module Formatting
44
+ # @private
45
+ COLORS = {
46
+ white: "00",
47
+ black: "01",
48
+ blue: "02",
49
+ green: "03",
50
+ red: "04",
51
+ brown: "05",
52
+ purple: "06",
53
+ orange: "07",
54
+ yellow: "08",
55
+ lime: "09",
56
+ teal: "10",
57
+ aqua: "11",
58
+ royal: "12",
59
+ pink: "13",
60
+ grey: "14",
61
+ silver: "15"
62
+ }
63
+
64
+ # @private
65
+ ATTRIBUTES = {
66
+ bold: 2.chr,
67
+ underlined: 31.chr,
68
+ underline: 31.chr,
69
+ reversed: 22.chr,
70
+ reverse: 22.chr,
71
+ italic: 29.chr,
72
+ reset: 15.chr
73
+ }
74
+
75
+ # @param [Array<Symbol>] settings The colors and attributes to apply.
76
+ # When supplying two colors, the first will be used for the
77
+ # foreground and the second for the background.
78
+ # @param [String] string The string to format.
79
+ # @return [String] the formatted string
80
+ # @since 2.0.0
81
+ # @raise [ArgumentError] When passing more than two colors as arguments.
82
+ # @see Helpers#Format Helpers#Format for easier access to this method.
83
+ #
84
+ # @example Nested formatting, combining text styles and colors
85
+ # reply = Format(:underline, "Hello %s! Is your favourite color %s?" % [Format(:bold, "stranger"), Format(:red, "red")])
86
+ def self.format(*settings, string)
87
+ string = string.dup
88
+
89
+ attributes = settings.select { |k| ATTRIBUTES.has_key?(k) }.map { |k| ATTRIBUTES[k] }
90
+ colors = settings.select { |k| COLORS.has_key?(k) }.map { |k| COLORS[k] }
91
+ if colors.size > 2
92
+ raise ArgumentError, "At most two colors (foreground and background) might be specified"
93
+ end
94
+
95
+ attribute_string = attributes.join
96
+ color_string = if colors.empty?
97
+ ""
98
+ else
99
+ "\x03#{colors.join(",")}"
100
+ end
101
+
102
+ prepend = attribute_string + color_string
103
+ append = ATTRIBUTES[:reset]
104
+
105
+ # Attributes act as toggles, so e.g. underline+underline = no
106
+ # underline. We thus have to delete all duplicate attributes
107
+ # from nested strings.
108
+ string.delete!(attribute_string)
109
+
110
+ # Replace the reset code of nested strings to continue the
111
+ # formattings of the outer string.
112
+ string.gsub!(/#{ATTRIBUTES[:reset]}/, ATTRIBUTES[:reset] + prepend)
113
+ prepend + string + append
114
+ end
115
+
116
+ # Deletes all mIRC formatting codes from the string. This strips
117
+ # formatting for bold, underline and so on, as well as color
118
+ # codes. This does include removing the numeric arguments.
119
+ #
120
+ # @param [String] string The string to filter
121
+ # @return [String] The filtered string
122
+ # @since 2.2.0
123
+ def self.unformat(string)
124
+ string.gsub(/[\x02\x0f\x16\x1f\x12]|\x03(\d{1,2}(,\d{1,2})?)?/, "")
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cinch
4
+ # @since 2.0.0
5
+ class Handler
6
+ # @return [Bot]
7
+ attr_reader :bot
8
+
9
+ # @return [Symbol]
10
+ attr_reader :event
11
+
12
+ # @return [Pattern]
13
+ attr_reader :pattern
14
+
15
+ # @return [Array]
16
+ attr_reader :args
17
+
18
+ # @return [Proc]
19
+ attr_reader :block
20
+
21
+ # @return [Symbol]
22
+ attr_reader :group
23
+
24
+ # @return [Boolean]
25
+ attr_reader :strip_colors
26
+
27
+ # @return [ThreadGroup]
28
+ # @api private
29
+ attr_reader :thread_group
30
+
31
+ # @param [Bot] bot
32
+ # @param [Symbol] event
33
+ # @param [Pattern] pattern
34
+ # @param [Hash] options
35
+ # @option options [Symbol] :group (nil) Match group the h belongs
36
+ # to.
37
+ # @option options [Boolean] :execute_in_callback (false) Whether
38
+ # to execute the handler in an instance of {Callback}
39
+ # @option options [Boolean] :strip_colors (false) Strip colors
40
+ # from message before attemping match
41
+ # @option options [Array] :args ([]) Additional arguments to pass
42
+ # to the block
43
+ def initialize(bot, event, pattern, options = {}, &block)
44
+ options = {
45
+ group: nil,
46
+ execute_in_callback: false,
47
+ strip_colors: false,
48
+ args: []
49
+ }.merge(options)
50
+ @bot = bot
51
+ @event = event
52
+ @pattern = pattern
53
+ @group = options[:group]
54
+ @execute_in_callback = options[:execute_in_callback]
55
+ @strip_colors = options[:strip_colors]
56
+ @args = options[:args]
57
+ @block = block
58
+
59
+ @thread_group = ThreadGroup.new
60
+ end
61
+
62
+ # Unregisters the handler.
63
+ #
64
+ # @return [void]
65
+ def unregister
66
+ @bot.handlers.unregister(self)
67
+ end
68
+
69
+ # Stops execution of the handler. This means stopping and killing
70
+ # all associated threads.
71
+ #
72
+ # @return [void]
73
+ def stop
74
+ @bot.loggers.debug "[Stopping handler] Stopping all threads of handler #{self}: #{@thread_group.list.size} threads..."
75
+ @thread_group.list.each do |thread|
76
+ Thread.new do
77
+ @bot.loggers.debug "[Ending thread] Waiting 10 seconds for #{thread} to finish..."
78
+ thread.join(10)
79
+ @bot.loggers.debug "[Killing thread] Killing #{thread}"
80
+ thread.kill
81
+ end
82
+ end
83
+ end
84
+
85
+ # Executes the handler.
86
+ #
87
+ # @param [Message] message Message that caused the invocation
88
+ # @param [Array] captures Capture groups of the pattern that are
89
+ # being passed as arguments
90
+ # @return [Thread]
91
+ def call(message, captures, arguments)
92
+ bargs = captures + arguments
93
+
94
+ thread = Thread.new {
95
+ @bot.loggers.debug "[New thread] For #{self}: #{Thread.current} -- #{@thread_group.list.size} in total."
96
+
97
+ begin
98
+ if @execute_in_callback
99
+ @bot.callback.instance_exec(message, *@args, *bargs, &@block)
100
+ else
101
+ @block.call(message, *@args, *bargs)
102
+ end
103
+ rescue => e
104
+ @bot.loggers.exception(e)
105
+ ensure
106
+ @bot.loggers.debug "[Thread done] For #{self}: #{Thread.current} -- #{@thread_group.list.size - 1} remaining."
107
+ end
108
+ }
109
+
110
+ @thread_group.add(thread)
111
+ thread
112
+ end
113
+
114
+ # @return [String]
115
+ def to_s
116
+ # TODO maybe add the number of running threads to the output?
117
+ "#<Cinch::Handler @event=#{@event.inspect} pattern=#{@pattern.inspect}>"
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ require_relative "cached_list"
6
+
7
+ module Cinch
8
+ # @since 2.0.0
9
+ class HandlerList
10
+ include Enumerable
11
+
12
+ def initialize
13
+ @handlers = Hash.new { |h, k| h[k] = [] }
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ def register(handler)
18
+ @mutex.synchronize do
19
+ handler.bot.loggers.debug "[on handler] Registering handler with pattern `#{handler.pattern.inspect}`, reacting on `#{handler.event}`"
20
+ @handlers[handler.event] << handler
21
+ end
22
+ end
23
+
24
+ # @param [Handler, Array<Handler>] handlers The handlers to unregister
25
+ # @return [void]
26
+ # @see Handler#unregister
27
+ def unregister(*handlers)
28
+ @mutex.synchronize do
29
+ handlers.each do |handler|
30
+ @handlers[handler.event].delete(handler)
31
+ end
32
+ end
33
+ end
34
+
35
+ # @api private
36
+ # @return [Array<Handler>]
37
+ def find(type, msg = nil)
38
+ if (handlers = @handlers[type])
39
+ if msg.nil?
40
+ return handlers
41
+ end
42
+
43
+ handlers = handlers.select { |handler|
44
+ msg.match(handler.pattern.to_r(msg), type, handler.strip_colors)
45
+ }.group_by { |handler| handler.group }
46
+
47
+ handlers.values_at(*(handlers.keys - [nil])).map(&:first) + (handlers[nil] || [])
48
+ end
49
+ end
50
+
51
+ # @param [Symbol] event The event type
52
+ # @param [Message, nil] msg The message which is responsible for
53
+ # and attached to the event, or nil.
54
+ # @param [Array] arguments A list of additional arguments to pass
55
+ # to event handlers
56
+ # @return [Array<Thread>]
57
+ def dispatch(event, msg = nil, *arguments)
58
+ threads = []
59
+
60
+ if (handlers = find(event, msg))
61
+ already_run = Set.new
62
+ handlers.each do |handler|
63
+ next if already_run.include?(handler.block)
64
+ already_run << handler.block
65
+ # calling Message#match multiple times is not a problem
66
+ # because we cache the result
67
+ captures = if msg
68
+ msg.match(handler.pattern.to_r(msg), event, handler.strip_colors).captures
69
+ else
70
+ []
71
+ end
72
+
73
+ threads << handler.call(msg, captures, arguments)
74
+ end
75
+ end
76
+
77
+ threads
78
+ end
79
+
80
+ # @yield [handler] Yields all registered handlers
81
+ # @yieldparam [Handler] handler
82
+ # @return [void]
83
+ def each(&block)
84
+ @handlers.values.flatten.each(&block)
85
+ end
86
+
87
+ # @api private
88
+ def stop_all
89
+ each { |h| h.stop }
90
+ end
91
+ end
92
+ end