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