grinch 1.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 +180 -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/cinch.rb +5 -0
  41. data/lib/cinch/ban.rb +50 -0
  42. data/lib/cinch/bot.rb +479 -0
  43. data/lib/cinch/cached_list.rb +19 -0
  44. data/lib/cinch/callback.rb +20 -0
  45. data/lib/cinch/channel.rb +463 -0
  46. data/lib/cinch/channel_list.rb +29 -0
  47. data/lib/cinch/configuration.rb +73 -0
  48. data/lib/cinch/configuration/bot.rb +48 -0
  49. data/lib/cinch/configuration/dcc.rb +16 -0
  50. data/lib/cinch/configuration/plugins.rb +41 -0
  51. data/lib/cinch/configuration/sasl.rb +19 -0
  52. data/lib/cinch/configuration/ssl.rb +19 -0
  53. data/lib/cinch/configuration/timeouts.rb +14 -0
  54. data/lib/cinch/constants.rb +533 -0
  55. data/lib/cinch/dcc.rb +12 -0
  56. data/lib/cinch/dcc/dccable_object.rb +37 -0
  57. data/lib/cinch/dcc/incoming.rb +1 -0
  58. data/lib/cinch/dcc/incoming/send.rb +147 -0
  59. data/lib/cinch/dcc/outgoing.rb +1 -0
  60. data/lib/cinch/dcc/outgoing/send.rb +122 -0
  61. data/lib/cinch/exceptions.rb +46 -0
  62. data/lib/cinch/formatting.rb +125 -0
  63. data/lib/cinch/handler.rb +118 -0
  64. data/lib/cinch/handler_list.rb +90 -0
  65. data/lib/cinch/helpers.rb +231 -0
  66. data/lib/cinch/irc.rb +924 -0
  67. data/lib/cinch/isupport.rb +98 -0
  68. data/lib/cinch/log_filter.rb +21 -0
  69. data/lib/cinch/logger.rb +168 -0
  70. data/lib/cinch/logger/formatted_logger.rb +97 -0
  71. data/lib/cinch/logger/zcbot_logger.rb +22 -0
  72. data/lib/cinch/logger_list.rb +85 -0
  73. data/lib/cinch/mask.rb +69 -0
  74. data/lib/cinch/message.rb +392 -0
  75. data/lib/cinch/message_queue.rb +107 -0
  76. data/lib/cinch/mode_parser.rb +76 -0
  77. data/lib/cinch/network.rb +104 -0
  78. data/lib/cinch/open_ended_queue.rb +26 -0
  79. data/lib/cinch/pattern.rb +65 -0
  80. data/lib/cinch/plugin.rb +515 -0
  81. data/lib/cinch/plugin_list.rb +38 -0
  82. data/lib/cinch/rubyext/float.rb +3 -0
  83. data/lib/cinch/rubyext/module.rb +26 -0
  84. data/lib/cinch/rubyext/string.rb +33 -0
  85. data/lib/cinch/sasl.rb +34 -0
  86. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  87. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  88. data/lib/cinch/sasl/mechanism.rb +6 -0
  89. data/lib/cinch/sasl/plain.rb +26 -0
  90. data/lib/cinch/syncable.rb +83 -0
  91. data/lib/cinch/target.rb +199 -0
  92. data/lib/cinch/timer.rb +145 -0
  93. data/lib/cinch/user.rb +488 -0
  94. data/lib/cinch/user_list.rb +87 -0
  95. data/lib/cinch/utilities/deprecation.rb +16 -0
  96. data/lib/cinch/utilities/encoding.rb +37 -0
  97. data/lib/cinch/utilities/kernel.rb +13 -0
  98. data/lib/cinch/version.rb +4 -0
  99. metadata +140 -0
data/lib/cinch/dcc.rb ADDED
@@ -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 [Integer] 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 [Integer] 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 [Integer] 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,147 @@
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
+ #
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 "cinch/dcc/outgoing/send"
@@ -0,0 +1,122 @@
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: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,46 @@
1
+ module Cinch
2
+ # A collection of exceptions.
3
+ module Exceptions
4
+ # Generic error. Superclass for all Cinch-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 Cinch discovers a feature it doesn't support
25
+ # yet.
26
+ class UnsupportedFeature < Generic
27
+ end
28
+
29
+ # Raised when Cinch discovers a user or channel mode, which it
30
+ # doesn't support yet.
31
+ class UnsupportedMode < Generic
32
+ def initialize(mode)
33
+ super "Cinch 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 Cinch
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