cinch 1.1.3 → 2.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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