cinch 1.1.3 → 2.0.0.pre.1

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 (83) hide show
  1. data/LICENSE +1 -0
  2. data/README.md +3 -3
  3. data/docs/bot_options.md +435 -0
  4. data/docs/changes.md +440 -0
  5. data/docs/common_mistakes.md +35 -0
  6. data/docs/common_tasks.md +47 -0
  7. data/docs/encodings.md +67 -0
  8. data/docs/events.md +272 -0
  9. data/docs/logging.md +5 -0
  10. data/docs/migrating.md +267 -0
  11. data/docs/readme.md +18 -0
  12. data/examples/plugins/custom_prefix.rb +1 -1
  13. data/examples/plugins/dice_roll.rb +38 -0
  14. data/examples/plugins/lambdas.rb +1 -1
  15. data/examples/plugins/memo.rb +16 -10
  16. data/examples/plugins/url_shorten.rb +1 -0
  17. data/lib/cinch.rb +5 -60
  18. data/lib/cinch/ban.rb +13 -7
  19. data/lib/cinch/bot.rb +228 -403
  20. data/lib/cinch/{cache_manager.rb → cached_list.rb} +5 -1
  21. data/lib/cinch/callback.rb +3 -0
  22. data/lib/cinch/channel.rb +119 -195
  23. data/lib/cinch/{channel_manager.rb → channel_list.rb} +6 -3
  24. data/lib/cinch/configuration.rb +73 -0
  25. data/lib/cinch/configuration/bot.rb +47 -0
  26. data/lib/cinch/configuration/dcc.rb +16 -0
  27. data/lib/cinch/configuration/plugins.rb +41 -0
  28. data/lib/cinch/configuration/sasl.rb +17 -0
  29. data/lib/cinch/configuration/ssl.rb +19 -0
  30. data/lib/cinch/configuration/storage.rb +37 -0
  31. data/lib/cinch/configuration/timeouts.rb +14 -0
  32. data/lib/cinch/constants.rb +531 -369
  33. data/lib/cinch/dcc.rb +12 -0
  34. data/lib/cinch/dcc/dccable_object.rb +37 -0
  35. data/lib/cinch/dcc/incoming.rb +1 -0
  36. data/lib/cinch/dcc/incoming/send.rb +131 -0
  37. data/lib/cinch/dcc/outgoing.rb +1 -0
  38. data/lib/cinch/dcc/outgoing/send.rb +115 -0
  39. data/lib/cinch/exceptions.rb +8 -1
  40. data/lib/cinch/formatting.rb +106 -0
  41. data/lib/cinch/handler.rb +104 -0
  42. data/lib/cinch/handler_list.rb +86 -0
  43. data/lib/cinch/helpers.rb +167 -10
  44. data/lib/cinch/irc.rb +525 -110
  45. data/lib/cinch/isupport.rb +11 -9
  46. data/lib/cinch/logger.rb +168 -0
  47. data/lib/cinch/logger/formatted_logger.rb +72 -55
  48. data/lib/cinch/logger/zcbot_logger.rb +9 -24
  49. data/lib/cinch/logger_list.rb +62 -0
  50. data/lib/cinch/mask.rb +19 -10
  51. data/lib/cinch/message.rb +94 -28
  52. data/lib/cinch/message_queue.rb +70 -28
  53. data/lib/cinch/mode_parser.rb +6 -1
  54. data/lib/cinch/network.rb +104 -0
  55. data/lib/cinch/{rubyext/queue.rb → open_ended_queue.rb} +8 -1
  56. data/lib/cinch/pattern.rb +24 -4
  57. data/lib/cinch/plugin.rb +352 -177
  58. data/lib/cinch/plugin_list.rb +35 -0
  59. data/lib/cinch/rubyext/float.rb +3 -0
  60. data/lib/cinch/rubyext/module.rb +7 -0
  61. data/lib/cinch/rubyext/string.rb +9 -0
  62. data/lib/cinch/sasl.rb +34 -0
  63. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  64. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  65. data/lib/cinch/sasl/mechanism.rb +6 -0
  66. data/lib/cinch/sasl/plain.rb +26 -0
  67. data/lib/cinch/storage.rb +62 -0
  68. data/lib/cinch/storage/null.rb +12 -0
  69. data/lib/cinch/storage/yaml.rb +96 -0
  70. data/lib/cinch/syncable.rb +13 -1
  71. data/lib/cinch/target.rb +144 -0
  72. data/lib/cinch/timer.rb +145 -0
  73. data/lib/cinch/user.rb +169 -225
  74. data/lib/cinch/{user_manager.rb → user_list.rb} +7 -2
  75. data/lib/cinch/utilities/deprecation.rb +12 -0
  76. data/lib/cinch/utilities/encoding.rb +54 -0
  77. data/lib/cinch/utilities/kernel.rb +13 -0
  78. data/lib/cinch/utilities/string.rb +13 -0
  79. data/lib/cinch/version.rb +4 -0
  80. metadata +88 -47
  81. data/lib/cinch/logger/logger.rb +0 -44
  82. data/lib/cinch/logger/null_logger.rb +0 -18
  83. data/lib/cinch/rubyext/infinity.rb +0 -1
@@ -0,0 +1,12 @@
1
+ require "cinch/dcc/outgoing"
2
+ require "cinch/dcc/incoming"
3
+
4
+ module Cinch
5
+ # Cinch 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,37 @@
1
+ module Cinch
2
+ module DCC
3
+ # This module describes the required interface for objects that should
4
+ # be sendable via DCC.
5
+ #
6
+ # @note `File` conforms to this interface.
7
+ # @since 2.0.0
8
+ # @abstract
9
+ module DCCableObject
10
+ # Return the next `number` bytes of the object.
11
+ #
12
+ # @param [Number] number Read `number` bytes at most
13
+ # @return [String] The read data
14
+ # @return [nil] If no more data can be read
15
+ def read(number)
16
+ end
17
+
18
+ # Seek to a specific position.
19
+ #
20
+ # @param [Number] position The position in bytes to seek to
21
+ # @return [void]
22
+ def seek(position)
23
+ end
24
+
25
+ # @return [String] A string representing the object's path or name.
26
+ #
27
+ # @note This is only required if calling {User#dcc_send} with only
28
+ # one argument
29
+ def path
30
+ end
31
+
32
+ # @return [Number] The total size of the data, in bytes.
33
+ def size
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1 @@
1
+ require "cinch/dcc/incoming/send"
@@ -0,0 +1,131 @@
1
+ require "socket"
2
+ require "ipaddr"
3
+
4
+ module Cinch
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
+ class Send
47
+ # @private
48
+ PRIVATE_NETS = [IPAddr.new("fc00::/7"),
49
+ IPAddr.new("10.0.0.0/8"),
50
+ IPAddr.new("172.16.0.0/12"),
51
+ IPAddr.new("192.168.0.0/16")]
52
+
53
+ # @private
54
+ LOCAL_NETS = [IPAddr.new("127.0.0.0/8"),
55
+ IPAddr.new("::1/128")]
56
+
57
+ # @return [User]
58
+ attr_reader :user
59
+
60
+ # @return [String]
61
+ attr_reader :filename
62
+
63
+ # @return [Number]
64
+ attr_reader :size
65
+
66
+ # @return [String]
67
+ attr_reader :ip
68
+
69
+ # @return [Number]
70
+ attr_reader :port
71
+
72
+ # @param [Hash] opts
73
+ # @option opts [User] user
74
+ # @option opts [String] filename
75
+ # @option opts [Number] size
76
+ # @option opts [String] ip
77
+ # @option opts [Number] port
78
+ # @api private
79
+ def initialize(opts)
80
+ @user, @filename, @size, @ip, @port = opts.values_at(:user, :filename, :size, :ip, :port)
81
+ end
82
+
83
+ # @return [String] The basename of the file name, with
84
+ # (back)slashes removed.
85
+ def filename
86
+ File.basename(File.expand_path(@filename)).delete("/\\")
87
+ end
88
+
89
+ # This method is used for accepting a DCC SEND offer. It
90
+ # expects an object to save the result to (usually an instance
91
+ # of IO or String).
92
+ #
93
+ # @param [#<<] io The object to write the data to.
94
+ # @return [void]
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
+ while buf = socket.read(1024)
108
+ total += buf.bytesize
109
+
110
+ socket.write [total].pack("N")
111
+ io << buf
112
+ end
113
+ end
114
+
115
+ # @return [Boolean] True if the DCC originates from a private ip
116
+ # @see #from_localhost?
117
+ def from_private_ip?
118
+ ip = IPAddr.new(@ip)
119
+ PRIVATE_NETS.any? {|n| n.include?(ip)}
120
+ end
121
+
122
+ # @return [Boolean] True if the DCC originates from localhost
123
+ # @see #from_private_ip?
124
+ def from_localhost?
125
+ ip = IPAddr.new(@ip)
126
+ LOCAL_NETS.any? {|n| n.include?(ip)}
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1 @@
1
+ require "cinch/dcc/outgoing/send"
@@ -0,0 +1,115 @@
1
+ require "socket"
2
+ require "ipaddr"
3
+ require "timeout"
4
+
5
+ module Cinch
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
+ # Cinch allows sending files by either using
15
+ # {Cinch::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 Cinch code itself.
19
+ #
20
+ # {Cinch::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: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
+ send_data(fd)
81
+ fd.close
82
+ end
83
+ ensure
84
+ @socket.close
85
+ end
86
+ end
87
+
88
+ # Seek to `pos` in the data.
89
+ #
90
+ # @param [Number] pos
91
+ # @return [void]
92
+ # @api private
93
+ def seek(pos)
94
+ @io.seek(pos)
95
+ end
96
+
97
+ # @return [Number] 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
+ rs, ws = IO.select([fd], [fd])
108
+ rs.first.recv unless rs.empty?
109
+ ws.first.write(chunk) unless ws.empty?
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -1,9 +1,11 @@
1
1
  module Cinch
2
+ # A collection of exceptions.
2
3
  module Exceptions
3
4
  # Generic error. Superclass for all Cinch-specific errors.
4
5
  class Generic < ::StandardError
5
6
  end
6
7
 
8
+ # Generic error when an argument is too long.
7
9
  class ArgumentTooLong < Generic
8
10
  end
9
11
 
@@ -19,15 +21,20 @@ module Cinch
19
21
  class KickReasonTooLong < ArgumentTooLong
20
22
  end
21
23
 
24
+ # Raised whenever Cinch discovers a feature it doesn't support
25
+ # yet.
22
26
  class UnsupportedFeature < Generic
23
27
  end
24
28
 
29
+ # Raised when Cinch discovers a user or channel mode, which it
30
+ # doesn't support yet.
25
31
  class UnsupportedMode < Generic
26
32
  def initialize(mode)
27
- super "Cinch does not support the mode #{mode} yet."
33
+ super "Cinch does not support the mode '#{mode}' yet."
28
34
  end
29
35
  end
30
36
 
37
+ # Error stating that an invalid mode string was encountered.
31
38
  class InvalidModeString < Generic
32
39
  end
33
40
  end
@@ -0,0 +1,106 @@
1
+ module Cinch
2
+ # @since 2.0.0
3
+ #
4
+ # List of valid colors
5
+ # =========================
6
+ # - aqua
7
+ # - black
8
+ # - blue
9
+ # - brown
10
+ # - green
11
+ # - grey
12
+ # - lime
13
+ # - orange
14
+ # - pink
15
+ # - purple
16
+ # - red
17
+ # - royal
18
+ # - silver
19
+ # - teal
20
+ # - white
21
+ # - yellow
22
+ #
23
+ # List of valid attributes
24
+ # ========================
25
+ # - bold
26
+ # - italic
27
+ # - reverse/reversed
28
+ # - underline/underlined
29
+ #
30
+ # Other
31
+ # =====
32
+ # - reset (Resets all formatting to the client's defaults)
33
+ module Formatting
34
+ # @private
35
+ Colors = {
36
+ :white => "00",
37
+ :black => "01",
38
+ :blue => "02",
39
+ :green => "03",
40
+ :red => "04",
41
+ :brown => "05",
42
+ :purple => "06",
43
+ :orange => "07",
44
+ :yellow => "08",
45
+ :lime => "09",
46
+ :teal => "10",
47
+ :aqua => "11",
48
+ :royal => "12",
49
+ :pink => "13",
50
+ :grey => "14",
51
+ :silver => "15",
52
+ }
53
+
54
+ # @private
55
+ Attributes = {
56
+ :bold => 2.chr,
57
+ :underlined => 31.chr,
58
+ :underline => 31.chr,
59
+ :reversed => 22.chr,
60
+ :reverse => 22.chr,
61
+ :italic => 22.chr,
62
+ :reset => 15.chr,
63
+ }
64
+
65
+ # @param [Array<Symbol>] *settings The colors and attributes to apply.
66
+ # When supplying two colors, the first will be used for the
67
+ # foreground and the second for the background.
68
+ # @param [String] string The string to format.
69
+ # @return [String] The formatted string
70
+ # @since 2.0.0
71
+ # @raise [ArgumentError] When passing more than two colors as arguments.
72
+ # @see Helpers#Format Helpers#Format for easier access to this method.
73
+ #
74
+ # @example Nested formatting, combining text styles and colors
75
+ # reply = Format(:underline, "Hello %s! Is your favourite color %s?" % [Format(:bold, "stranger"), Format(:red, "red")])
76
+ def self.format(*settings, string)
77
+ string = string.dup
78
+
79
+ attributes = settings.select {|k| Attributes.has_key?(k)}.map {|k| Attributes[k]}
80
+ colors = settings.select {|k| Colors.has_key?(k)}.map {|k| Colors[k]}
81
+ if colors.size > 2
82
+ raise ArgumentError, "At most two colors (foreground and background) might be specified"
83
+ end
84
+
85
+ attribute_string = attributes.join
86
+ color_string = if colors.empty?
87
+ ""
88
+ else
89
+ "\x03#{colors.join(",")}"
90
+ end
91
+
92
+ prepend = attribute_string + color_string
93
+ append = Attributes[:reset]
94
+
95
+ # attributes act as toggles, so e.g. underline+underline = no
96
+ # underline. We thus have to delete all duplicate attributes
97
+ # from nested strings.
98
+ string.delete!(attribute_string)
99
+
100
+ # Replace the reset code of nested strings to continue the
101
+ # formattings of the outer string.
102
+ string.gsub!(/#{Attributes[:reset]}/, Attributes[:reset] + prepend)
103
+ return prepend + string + append
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,104 @@
1
+ module Cinch
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 [ThreadGroup]
23
+ # @api private
24
+ attr_reader :thread_group
25
+
26
+ # @param [Bot] bot
27
+ # @param [Symbol] event
28
+ # @param [Pattern] pattern
29
+ # @param [Hash] options
30
+ # @option options [Symbol] :group (nil) Match group the h belongs
31
+ # to.
32
+ # @option options [Boolean] :execute_in_callback (false) Whether
33
+ # to execute the handler in an instance of {Callback}
34
+ # @option options [Array] :args ([]) Additional arguments to pass
35
+ # to the block
36
+ def initialize(bot, event, pattern, options = {}, &block)
37
+ options = {:group => nil, :execute_in_callback => false, :args => []}.merge(options)
38
+ @bot = bot
39
+ @event = event
40
+ @pattern = pattern
41
+ @group = options[:group]
42
+ @execute_in_callback = options[:execute_in_callback]
43
+ @args = options[:args]
44
+ @block = block
45
+
46
+ @thread_group = ThreadGroup.new
47
+ end
48
+
49
+ # Unregisters the handler.
50
+ #
51
+ # @return [void]
52
+ def unregister
53
+ @bot.handlers.unregister(self)
54
+ end
55
+
56
+ # Stops execution of the handler. This means stopping and killing
57
+ # all associated threads.
58
+ #
59
+ # @return [void]
60
+ def stop
61
+ @bot.loggers.debug "[Stopping handler] Stopping all threads of handler #{self}: #{@thread_group.list.size} threads..."
62
+ @thread_group.list.each do |thread|
63
+ Thread.new do
64
+ @bot.loggers.debug "[Ending thread] Waiting 10 seconds for #{thread} to finish..."
65
+ thread.join(10)
66
+ @bot.loggers.debug "[Killing thread] Killing #{thread}"
67
+ thread.kill
68
+ end
69
+ end
70
+ end
71
+
72
+ # Executes the handler.
73
+ #
74
+ # @param [Message] message Message that caused the invocation
75
+ # @param [Array] captures Capture groups of the pattern that are
76
+ # being passed as arguments
77
+ # @return [void]
78
+ def call(message, captures, arguments)
79
+ bargs = captures + arguments
80
+
81
+ @thread_group.add Thread.new {
82
+ @bot.loggers.debug "[New thread] For #{self}: #{Thread.current} -- #{@thread_group.list.size} in total."
83
+
84
+ begin
85
+ if @execute_in_callback
86
+ @bot.callback.instance_exec(message, *@args, *bargs, &@block)
87
+ else
88
+ @block.call(message, *@args, *bargs)
89
+ end
90
+ rescue => e
91
+ @bot.loggers.exception(e)
92
+ ensure
93
+ @bot.loggers.debug "[Thread done] For #{self}: #{Thread.current} -- #{@thread_group.list.size - 1} remaining."
94
+ end
95
+ }
96
+ end
97
+
98
+ # @return [String]
99
+ def to_s
100
+ # TODO maybe add the number of running threads to the output?
101
+ "#<Cinch::Handler @event=#{@event.inspect} pattern=#{@pattern.inspect}>"
102
+ end
103
+ end
104
+ end