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,18 @@
1
+ # @title README
2
+
3
+
4
+ - {file:changes.md Changelog}
5
+ - {file:migrating.md Migration guides}
6
+
7
+ # Important documents
8
+
9
+ The following is a list of important documents to read.
10
+
11
+ - {file:bot_options.md Bot options}
12
+ - {file:common_tasks.md Common Tasks}
13
+ - {file:common_mistakes.md Common mistakes}
14
+ - {Cinch::DCC DCC}
15
+ - {file:encodings.md Encodings}
16
+ - {file:logging.md Logging}
17
+ - {Cinch::SASL SASL}
18
+ - {file:events.d Events}
@@ -3,7 +3,7 @@ require 'cinch'
3
3
  class SomeCommand
4
4
  include Cinch::Plugin
5
5
 
6
- prefix "~"
6
+ prefix /^~/
7
7
  match "somecommand"
8
8
 
9
9
  def execute(m)
@@ -0,0 +1,38 @@
1
+ require 'cinch'
2
+
3
+ class DiceRoll
4
+ include Cinch::Plugin
5
+
6
+ # [[<repeats>#]<rolls>]d<sides>[<+/-><offset>]
7
+ match(/roll (?:(?:(\d+)#)?(\d+))?d(\d+)(?:([+-])(\d+))?/)
8
+ def execute(m, repeats, rolls, sides, offset_op, offset)
9
+ repeats = repeats.to_i
10
+ repeats = 1 if repeats < 1
11
+ rolls = rolls.to_i
12
+ rolls = 1 if rolls < 1
13
+
14
+ total = 0
15
+
16
+ repeats.times do
17
+ rolls.times do
18
+ score = rand(sides.to_i) + 1
19
+ if offset_op
20
+ score = score.send(offset_op, offset.to_i)
21
+ end
22
+ total += score
23
+ end
24
+ end
25
+
26
+ m.reply "Your dice roll was: #{total}", true
27
+ end
28
+ end
29
+
30
+ bot = Cinch::Bot.new do
31
+ configure do |c|
32
+ c.server = 'irc.freenode.org'
33
+ c.channels = ['#cinch-bots']
34
+ c.plugins.plugins = [DiceRoll]
35
+ end
36
+ end
37
+
38
+ bot.start
@@ -9,7 +9,7 @@ class DirectAddressing
9
9
  #
10
10
  # The reason we are using a lambda is that the bot's nick can change
11
11
  # and the prefix has to be up to date.
12
- prefix lambda{ |m| m.bot.nick + ": " }
12
+ prefix lambda{ |m| Regexp.new("^" + Regexp.escape(m.bot.nick + ": " ))}
13
13
 
14
14
  match "hello", method: :greet
15
15
  def greet(m)
@@ -1,39 +1,42 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'cinch'
4
+ require 'cinch/storage/yaml'
4
5
 
5
6
  class Memo
6
- class MemoStruct < Struct.new(:nick, :channel, :text, :time)
7
+ class MemoStruct < Struct.new(:user, :channel, :text, :time)
7
8
  def to_s
8
- "[#{time.asctime}] <#{channel}/#{nick}> #{text}"
9
+ "[#{time.asctime}] <#{channel}/#{user}> #{text}"
9
10
  end
10
11
  end
11
12
 
12
13
  include Cinch::Plugin
13
14
 
14
- listen_to :message
15
- match /memo (.+?) (.+)/
16
-
17
15
  def initialize(*args)
18
16
  super
19
- @memos = {}
17
+ storage[:memos] ||= {}
20
18
  end
21
19
 
20
+ listen_to :message
21
+ match /memo (.+?) (.+)/
22
+
22
23
  def listen(m)
23
- if @memos.has_key?(m.user.nick)
24
- m.user.send @memos.delete(m.user.nick).to_s
24
+ if storage[:memos].has_key?(m.user.nick)
25
+ m.user.send storage[:memos].delete(m.user.nick).to_s
26
+ storage.save
25
27
  end
26
28
  end
27
29
 
28
30
  def execute(m, nick, message)
29
- if @memos.key?(nick)
31
+ if storage[:memos].key?(nick)
30
32
  m.reply "There's already a memo for #{nick}. You can only store one right now"
31
33
  elsif nick == m.user.nick
32
34
  m.reply "You can't leave memos for yourself.."
33
35
  elsif nick == bot.nick
34
36
  m.reply "You can't leave memos for me.."
35
37
  else
36
- @memos[nick] = MemoStruct.new(m.user.nick, m.channel, message, Time.now)
38
+ storage[:memos][nick] = MemoStruct.new(m.user.name, m.channel.name, message, Time.now)
39
+ storage.save
37
40
  m.reply "Added memo for #{nick}"
38
41
  end
39
42
  end
@@ -44,6 +47,9 @@ bot = Cinch::Bot.new do
44
47
  c.server = "irc.freenode.org"
45
48
  c.channels = ["#cinch-bots"]
46
49
  c.plugins.plugins = [Memo]
50
+ c.storage.backend = Cinch::Storage::YAML
51
+ c.storage.basedir = "./yaml/"
52
+ c.storage.autosave = true
47
53
  end
48
54
  end
49
55
 
@@ -26,6 +26,7 @@ bot = Cinch::Bot.new do
26
26
  configure do |c|
27
27
  c.server = "irc.freenode.org"
28
28
  c.channels = ["#cinch-bots"]
29
+ c.plugins.plugins = [TinyURL]
29
30
  end
30
31
  end
31
32
 
@@ -1,61 +1,6 @@
1
+ require 'cinch/version'
2
+ require 'cinch/utilities/kernel'
3
+ require 'cinch/utilities/string'
4
+ require 'cinch/utilities/deprecation'
5
+ require 'cinch/utilities/encoding'
1
6
  require 'cinch/bot'
2
-
3
- module Cinch
4
- VERSION = '1.1.3'
5
-
6
- # @return [String]
7
- # @todo Handle mIRC color codes more gracefully.
8
- # @api private
9
- def self.filter_string(string)
10
- string.gsub(/[\x00-\x1f]/, '')
11
- end
12
-
13
- # @api private
14
- def self.encode_incoming(string, encoding)
15
- string = string.dup
16
- if encoding == :irc
17
- # If incoming text is valid UTF-8, it will be interpreted as
18
- # such. If it fails validation, a CP1252 -&gt; UTF-8 conversion
19
- # is performed. This allows you to see non-ASCII from mIRC
20
- # users (non-UTF-8) and other users sending you UTF-8.
21
- #
22
- # (from http://xchat.org/encoding/#hybrid)
23
- string.force_encoding("UTF-8")
24
- if !string.valid_encoding?
25
- string.force_encoding("CP1252").encode!("UTF-8", {:invalid => :replace, :undef => :replace})
26
- end
27
- else
28
- string.force_encoding(encoding).encode!({:invalid => :replace, :undef => :replace})
29
- string = string.chars.select { |c| c.valid_encoding? }.join
30
- end
31
-
32
- return string
33
- end
34
-
35
- # @api private
36
- def self.encode_outgoing(string, encoding)
37
- string = string.dup
38
- if encoding == :irc
39
- # If your text contains only characters that fit inside the CP1252
40
- # code page (aka Windows Latin-1), the entire line will be sent
41
- # that way. mIRC users should see it correctly. XChat users who
42
- # are using UTF-8 will also see it correctly, because it will fail
43
- # UTF-8 validation and will be assumed to be CP1252, even by older
44
- # XChat versions.
45
- #
46
- # If the text doesn't fit inside the CP1252 code page, (for eaxmple if you
47
- # type Eastern European characters, or Russian) it will be sent as UTF-8. Only
48
- # UTF-8 capable clients will be able to see these characters correctly
49
- #
50
- # (from http://xchat.org/encoding/#hybrid)
51
- begin
52
- string.encode!("CP1252")
53
- rescue Encoding::UndefinedConversionError
54
- end
55
- else
56
- string.encode!(encoding, {:invalid => :replace, :undef => :replace})
57
- end
58
-
59
- return string
60
- end
61
- end
@@ -1,10 +1,16 @@
1
1
  require "cinch/mask"
2
2
  module Cinch
3
+ # This class represents channel bans.
3
4
  class Ban
4
- # @return [Mask, String]
5
+ # @return [Mask] A {Mask} object for non-extended bans
6
+ # @return [String] A String object for extended bans (see {#extended})
5
7
  attr_reader :mask
6
8
 
7
- # @return [User]
9
+ # The user who created the ban. Might be nil on networks that do
10
+ # not strictly follow the RFCs, for example IRCnet in some(?)
11
+ # cases.
12
+ #
13
+ # @return [User, nil] The user who created the ban
8
14
  attr_reader :by
9
15
 
10
16
  # @return [Time]
@@ -13,17 +19,17 @@ module Cinch
13
19
  # @return [Boolean] whether this is an extended ban (as used by for example Freenode)
14
20
  attr_reader :extended
15
21
 
16
- # @param [String] mask The mask
17
- # @param [User] by The user who created the ban
22
+ # @param [String, Mask] mask The mask
23
+ # @param [User, nil] by The user who created the ban.
18
24
  # @param [Time] at The time at which the ban was created
19
25
  def initialize(mask, by, at)
20
26
  @by, @created_at = by, at
21
- if mask =~ /^\$/
27
+ if mask =~ /^[\$~]/
22
28
  @extended = true
23
29
  @mask = mask
24
30
  else
25
31
  @extended = false
26
- @mask = Mask.new(mask)
32
+ @mask = Mask.from(mask)
27
33
  end
28
34
  end
29
35
 
@@ -31,7 +37,7 @@ module Cinch
31
37
  # @raise [Exceptions::UnsupportedFeature] Cinch does not support
32
38
  # Freenode's extended bans
33
39
  def match(user)
34
- raise UnsupportedFeature, "extended bans (freenode) are not supported yet" if @extended
40
+ raise UnsupportedFeature, "extended bans are not supported yet" if @extended
35
41
  @mask =~ user
36
42
  end
37
43
  alias_method :=~, :match
@@ -3,20 +3,23 @@ require 'socket'
3
3
  require "thread"
4
4
  require "ostruct"
5
5
  require "cinch/rubyext/module"
6
- require "cinch/rubyext/queue"
7
6
  require "cinch/rubyext/string"
8
- require "cinch/rubyext/infinity"
7
+ require "cinch/rubyext/float"
9
8
 
10
9
  require "cinch/exceptions"
11
10
 
11
+ require "cinch/handler"
12
12
  require "cinch/helpers"
13
- require "cinch/logger/logger"
14
- require "cinch/logger/null_logger"
13
+
14
+ require "cinch/logger_list"
15
+ require "cinch/logger"
16
+
15
17
  require "cinch/logger/formatted_logger"
16
18
  require "cinch/syncable"
17
19
  require "cinch/message"
18
20
  require "cinch/message_queue"
19
21
  require "cinch/irc"
22
+ require "cinch/target"
20
23
  require "cinch/channel"
21
24
  require "cinch/user"
22
25
  require "cinch/constants"
@@ -27,81 +30,103 @@ require "cinch/isupport"
27
30
  require "cinch/plugin"
28
31
  require "cinch/pattern"
29
32
  require "cinch/mode_parser"
30
- require "cinch/cache_manager"
31
- require "cinch/channel_manager"
32
- require "cinch/user_manager"
33
+ require "cinch/dcc"
34
+ require "cinch/sasl"
35
+
36
+ require "cinch/handler_list"
37
+ require "cinch/cached_list"
38
+ require "cinch/channel_list"
39
+ require "cinch/user_list"
40
+ require "cinch/plugin_list"
41
+
42
+ require "cinch/timer"
43
+ require "cinch/formatting"
44
+
45
+ require "cinch/configuration"
46
+ require "cinch/configuration/bot"
47
+ require "cinch/configuration/plugins"
48
+ require "cinch/configuration/ssl"
49
+ require "cinch/configuration/timeouts"
50
+ require "cinch/configuration/storage"
51
+ require "cinch/configuration/dcc"
52
+ require "cinch/configuration/sasl"
33
53
 
34
54
  module Cinch
55
+ # @attr nick
56
+ # @version 2.0.0
57
+ class Bot < User
58
+ include Helpers
59
+
35
60
 
36
- class Bot
37
- # @return [Config]
61
+ # @return [Configuration::Bot]
62
+ # @version 2.0.0
38
63
  attr_reader :config
64
+
65
+ # The underlying IRC connection
66
+ #
39
67
  # @return [IRC]
40
68
  attr_reader :irc
41
- # @return [Logger]
42
- attr_accessor :logger
69
+
70
+ # The logger list containing all loggers
71
+ #
72
+ # @return [LoggerList]
73
+ # @since 2.0.0
74
+ attr_accessor :loggers
75
+
43
76
  # @return [Array<Channel>] All channels the bot currently is in
44
77
  attr_reader :channels
45
- # @return [String] the bot's hostname
46
- attr_reader :host
47
- # @return [Mask]
48
- attr_reader :mask
49
- # @return [String]
50
- attr_reader :user
51
- # @return [String]
52
- attr_reader :realname
53
- # @return [Time]
54
- attr_reader :signed_on_at
55
- # @return [Array<Plugin>] All registered plugins
78
+
79
+ # @return [PluginList] All registered plugins
80
+ # @version 2.0.0
56
81
  attr_reader :plugins
57
- # @return [Array<Thread>]
58
- # @api private
59
- attr_reader :handler_threads
82
+
60
83
  # @return [Boolean] whether the bot is in the process of disconnecting
61
84
  attr_reader :quitting
62
- # @return [UserManager]
63
- attr_reader :user_manager
64
- # @return [ChannelManager]
65
- attr_reader :channel_manager
85
+
86
+ # @return [UserList] All {User users} the bot knows about.
87
+ # @see UserList
88
+ # @since 1.1.0
89
+ attr_reader :user_list
90
+
91
+ # @return [ChannelList] All {Channel channels} the bot knows about.
92
+ # @see ChannelList
93
+ # @since 1.1.0
94
+ attr_reader :channel_list
95
+
96
+ # @return [PluginList] All loaded plugins.
97
+ # @version 2.0.0
98
+ attr_reader :plugins
99
+
66
100
  # @return [Boolean]
67
101
  # @api private
68
102
  attr_accessor :last_connection_was_successful
69
103
 
70
- # @group Helper methods
104
+ # @return [Callback]
105
+ # @api private
106
+ attr_reader :callback
71
107
 
72
- # Helper method for turning a String into a {Channel} object.
108
+ # The {HandlerList}, providing access to all registered plugins
109
+ # and plugin manipulation as well as {HandlerList#dispatch calling handlers}.
73
110
  #
74
- # @param [String] channel a channel name
75
- # @return [Channel] a {Channel} object
76
- # @example
77
- # on :message, /^please join (#.+)$/ do |m, target|
78
- # Channel(target).join
79
- # end
80
- def Channel(channel)
81
- return channel if channel.is_a?(Channel)
82
- @channel_manager.find_ensured(channel)
83
- end
111
+ # @return [HandlerList]
112
+ # @see HandlerList
113
+ # @since 2.0.0
114
+ attr_reader :handlers
84
115
 
85
- # Helper method for turning a String into an {User} object.
116
+ # The bot's modes.
86
117
  #
87
- # @param [String] user a user's nickname
88
- # @return [User] an {User} object
89
- # @example
90
- # on :message, /^tell me everything about (.+)$/ do |m, target|
91
- # user = User(target)
92
- # m.reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
93
- # end
94
- def User(user)
95
- return user if user.is_a?(User)
96
- @user_manager.find_ensured(user)
97
- end
118
+ # @return [Array<String>]
119
+ # @since 2.0.0
120
+ attr_reader :modes
121
+
122
+ # @group Helper methods
98
123
 
99
124
  # Define helper methods in the context of the bot.
100
125
  #
101
126
  # @yield Expects a block containing method definitions
102
127
  # @return [void]
103
128
  def helpers(&b)
104
- Callback.class_eval(&b)
129
+ @callback.instance_eval(&b)
105
130
  end
106
131
 
107
132
  # Since Cinch uses threads, all handlers can be run
@@ -142,234 +167,47 @@ module Cinch
142
167
  semaphore.synchronize(&block)
143
168
  end
144
169
 
145
- # Stop execution of the current {#on} handler.
146
- #
147
- # @return [void]
148
- def halt
149
- throw :halt
150
- end
151
-
152
170
  # @endgroup
153
- # @group Sending messages
154
171
 
155
- # Sends a raw message to the server.
156
- #
157
- # @param [String] command The message to send.
158
- # @return [void]
159
- # @see IRC#message
160
- def raw(command)
161
- @irc.message(command)
162
- end
163
-
164
- # Sends a PRIVMSG to a recipient (a channel or user).
165
- # You should be using {Channel#send} and {User#send} instead.
166
- #
167
- # @param [String] recipient the recipient
168
- # @param [String] text the message to send
169
- # @param [Boolean] notice Use NOTICE instead of PRIVMSG?
170
- # @return [void]
171
- # @see Channel#send
172
- # @see User#send
173
- # @see #safe_msg
174
- def msg(recipient, text, notice = false)
175
- text = text.to_s
176
- split_start = @config.message_split_start || ""
177
- split_end = @config.message_split_end || ""
178
- command = notice ? "NOTICE" : "PRIVMSG"
179
-
180
- text.split(/\r\n|\r|\n/).each do |line|
181
- maxlength = 510 - (":" + " #{command} " + " :").size
182
- maxlength = maxlength - self.mask.to_s.length - recipient.to_s.length
183
- maxlength_without_end = maxlength - split_end.bytesize
184
-
185
- if line.bytesize > maxlength
186
- splitted = []
187
-
188
- while line.bytesize > maxlength_without_end
189
- pos = line.rindex(/\s/, maxlength_without_end)
190
- r = pos || maxlength_without_end
191
- splitted << line.slice!(0, r) + split_end.tr(" ", "\u00A0")
192
- line = split_start.tr(" ", "\u00A0") + line.lstrip
193
- end
194
-
195
- splitted << line
196
- splitted[0, (@config.max_messages || splitted.size)].each do |string|
197
- string.tr!("\u00A0", " ") # clean string from any non-breaking spaces
198
- raw("#{command} #{recipient} :#{string}")
199
- end
200
- else
201
- raw("#{command} #{recipient} :#{line}")
202
- end
203
- end
204
- end
205
- alias_method :privmsg, :msg
206
- alias_method :send, :msg
207
-
208
- # Sends a NOTICE to a recipient (a channel or user).
209
- # You should be using {Channel#notice} and {User#notice} instead.
210
- #
211
- # @param [String] recipient the recipient
212
- # @param [String] text the message to send
213
- # @return [void]
214
- # @see Channel#notice
215
- # @see User#notice
216
- # @see #safe_notice
217
- def notice(recipient, text)
218
- msg(recipient, text, true)
219
- end
220
-
221
- # Like {#msg}, but remove any non-printable characters from
222
- # `text`. The purpose of this method is to send text of untrusted
223
- # sources, like other users or feeds.
224
- #
225
- # Note: this will **break** any mIRC color codes embedded in the
226
- # string.
227
- #
228
- # @return (see #msg)
229
- # @param (see #msg)
230
- # @see #msg
231
- # @see User#safe_send
232
- # @see Channel#safe_send
233
- # @todo Handle mIRC color codes more gracefully.
234
- def safe_msg(recipient, text)
235
- msg(recipient, Cinch.filter_string(text))
236
- end
237
- alias_method :safe_privmsg, :safe_msg
238
- alias_method :safe_send, :safe_msg
239
-
240
- # Like {#safe_msg} but for notices.
241
- #
242
- # @return (see #safe_msg)
243
- # @param (see #safe_msg)
244
- # @see #safe_notice
245
- # @see #notice
246
- # @see User#safe_notice
247
- # @see Channel#safe_notice
248
- # @todo (see #safe_msg)
249
- def safe_notice(recipient, text)
250
- msg(recipient, Cinch.filter_string(text), true)
251
- end
252
-
253
- # Invoke an action (/me) in/to a recipient (a channel or user).
254
- # You should be using {Channel#action} and {User#action} instead.
255
- #
256
- # @param [String] recipient the recipient
257
- # @param [String] text the message to send
258
- # @return [void]
259
- # @see Channel#action
260
- # @see User#action
261
- # @see #safe_action
262
- def action(recipient, text)
263
- raw("PRIVMSG #{recipient} :\001ACTION #{text}\001")
264
- end
265
-
266
- # Like {#action}, but remove any non-printable characters from
267
- # `text`. The purpose of this method is to send text from
268
- # untrusted sources, like other users or feeds.
269
- #
270
- # Note: this will **break** any mIRC color codes embedded in the
271
- # string.
272
- #
273
- # @param (see #action)
274
- # @return (see #action)
275
- # @see #action
276
- # @see Channel#safe_action
277
- # @see User#safe_action
278
- # @todo Handle mIRC color codes more gracefully.
279
- def safe_action(recipient, text)
280
- action(recipient, Cinch.filter_string(text))
281
- end
282
-
283
- # @endgroup
284
172
  # @group Events &amp; Plugins
285
173
 
286
174
  # Registers a handler.
287
175
  #
288
- # @param [String, Symbol, Integer] event the event to match. Available
289
- # events are all IRC commands in lowercase as symbols, all numeric
290
- # replies, and the following:
176
+ # @param [String, Symbol, Integer] event the event to match. For a
177
+ # list of available events, check the {file:events.md Events
178
+ # documentation}.
291
179
  #
292
- # - :channel (a channel message)
293
- # - :private (a private message)
294
- # - :message (both channel and private messages)
295
- # - :error (handling errors, use a numeric error code as `match`)
296
- # - :ctcp (ctcp requests, use a ctcp command as `match`)
297
- #
298
- # @param [Regexp, String, Integer] match every message of the
180
+ # @param [Regexp, Pattern, String] regexp every message of the
299
181
  # right event will be checked against this argument and the event
300
182
  # will only be called if it matches
301
183
  #
184
+ # @param [Array<Object>] *args Arguments that should be passed to
185
+ # the block, additionally to capture groups of the regexp.
186
+ #
302
187
  # @yieldparam [String] *args each capture group of the regex will
303
- # be one argument to the block. It is optional to accept them,
304
- # though
188
+ # be one argument to the block.
305
189
  #
306
- # @return [void]
307
- def on(event, regexps = [], *args, &block)
308
- regexps = [*regexps]
309
- regexps = [//] if regexps.empty?
310
-
190
+ # @return [Handler] The handlers that have been registered
191
+ def on(event, regexp = //, *args, &block)
311
192
  event = event.to_sym
312
193
 
313
- regexps.map! do |regexp|
314
- pattern = case regexp
315
- when Pattern
316
- regexp
317
- when Regexp
318
- Pattern.new(nil, regexp, nil)
319
- else
320
- if event == :ctcp
321
- Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}(?:$| .+)/, nil)
322
- else
323
- Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
324
- end
325
- end
326
- debug "[on handler] Registering handler with pattern `#{pattern.inspect}`, reacting on `#{event}`"
327
- pattern
328
- end
329
- (@events[event] ||= []) << [regexps, args, block]
330
- end
194
+ pattern = case regexp
195
+ when Pattern
196
+ regexp
197
+ when Regexp
198
+ Pattern.new(nil, regexp, nil)
199
+ else
200
+ if event == :ctcp
201
+ Pattern.generate(:ctcp, regexp)
202
+ else
203
+ Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
204
+ end
205
+ end
331
206
 
332
- # @param [Symbol] event The event type
333
- # @param [Message, nil] msg The message which is responsible for
334
- # and attached to the event, or nil.
335
- # @param [Array] *arguments A list of additional arguments to pass
336
- # to event handlers
337
- # @return [void]
338
- def dispatch(event, msg = nil, *arguments)
339
- if handlers = find(event, msg)
340
- handlers.each do |handler|
341
- regexps, args, block = *handler
342
- # calling Message#match multiple times is not a problem
343
- # because we cache the result
344
- if msg
345
- regexp = regexps.find { |rx|
346
- msg.match(rx.to_r(msg), event)
347
- }
348
- captures = msg.match(regexp.to_r(msg), event).captures
349
- else
350
- captures = []
351
- end
207
+ handler = Handler.new(self, event, pattern, {args: args, execute_in_callback: true}, &block)
208
+ @handlers.register(handler)
352
209
 
353
- invoke(block, args, msg, captures, arguments)
354
- end
355
- end
356
- end
357
-
358
- # Register all plugins from `@config.plugins.plugins`.
359
- #
360
- # @return [void]
361
- def register_plugins
362
- @config.plugins.plugins.each do |plugin|
363
- register_plugin(plugin)
364
- end
365
- end
366
-
367
- # Registers a plugin.
368
- #
369
- # @param [Class<Plugin>] plugin The plugin class to register
370
- # @return [void]
371
- def register_plugin(plugin)
372
- @plugins << plugin.new(self)
210
+ return handler
373
211
  end
374
212
 
375
213
  # @endgroup
@@ -380,8 +218,8 @@ module Cinch
380
218
  #
381
219
  # @yieldparam [Struct] config the bot's config
382
220
  # @return [void]
383
- def configure(&block)
384
- @callback.instance_exec(@config, &block)
221
+ def configure
222
+ yield @config
385
223
  end
386
224
 
387
225
  # Disconnects from the server.
@@ -390,15 +228,11 @@ module Cinch
390
228
  # @return [void]
391
229
  def quit(message = nil)
392
230
  @quitting = true
393
- command = message ? "QUIT :#{message}" : "QUIT"
394
- raw command
231
+ command = message ? "QUIT :#{message}" : "QUIT"
232
+
233
+ @irc.send command
395
234
  end
396
235
 
397
- # Connects the bot to a server.
398
- #
399
- # @param [Boolean] plugins Automatically register plugins from
400
- # `@config.plugins.plugins`?
401
- # @return [void]
402
236
  # Connects the bot to a server.
403
237
  #
404
238
  # @param [Boolean] plugins Automatically register plugins from
@@ -406,21 +240,37 @@ module Cinch
406
240
  # @return [void]
407
241
  def start(plugins = true)
408
242
  @reconnects = 0
409
- register_plugins if plugins
243
+ @plugins.register_plugins(@config.plugins.plugins) if plugins
410
244
 
411
245
  begin
412
- @user_manager.each do |user|
246
+ @user_list.each do |user|
413
247
  user.in_whois = false
414
248
  user.unsync_all
415
249
  end # reset state of all users
416
250
 
417
- @channel_manager.each do |channel|
251
+ @channel_list.each do |channel|
418
252
  channel.unsync_all
419
253
  end # reset state of all channels
420
254
 
421
- @logger.debug "Connecting to #{@config.server}:#{@config.port}"
255
+ @join_handler.unregister if @join_handler
256
+ @join_timer.stop if @join_timer
257
+
258
+ join_lambda = lambda { @config.channels.each { |channel| Channel(channel).join }}
259
+
260
+ if @config.delay_joins.is_a?(Symbol)
261
+ @join_handler = join_handler = on(@config.delay_joins) {
262
+ join_handler.unregister
263
+ join_lambda.call
264
+ }.first
265
+ else
266
+ @join_timer = Timer.new(self, interval: @config.delay_joins, shots: 1) {
267
+ join_lambda.call
268
+ }
269
+ end
270
+
271
+ @loggers.info "Connecting to #{@config.server}:#{@config.port}"
422
272
  @irc = IRC.new(self)
423
- @irc.connect
273
+ @irc.start
424
274
 
425
275
  if @config.reconnect && !@quitting
426
276
  # double the delay for each unsuccesful reconnection attempt
@@ -431,15 +281,26 @@ module Cinch
431
281
  @reconnects += 1
432
282
  end
433
283
 
434
- # Sleep for a few seconds before reconnecting to prevent being
435
- # throttled by the IRC server
284
+ # Throttle reconnect attempts
436
285
  wait = 2**@reconnects
437
- @logger.debug "Waiting #{wait} seconds before reconnecting"
438
- sleep wait
286
+ wait = @config.max_reconnect_delay if wait > @config.max_reconnect_delay
287
+ @loggers.info "Waiting #{wait} seconds before reconnecting"
288
+ start_time = Time.now
289
+ while !@quitting && (Time.now - start_time) < wait
290
+ sleep 1
291
+ end
439
292
  end
440
293
  end while @config.reconnect and not @quitting
441
294
  end
442
295
 
296
+ # TODO document this
297
+ def stop
298
+ @plugins.each do |plugin|
299
+ plugin.storage.save
300
+ plugin.storage.unload
301
+ end
302
+ end
303
+
443
304
  # @endgroup
444
305
  # @group Channel Control
445
306
 
@@ -447,30 +308,30 @@ module Cinch
447
308
  #
448
309
  # @param [String, Channel] channel either the name of a channel or a {Channel} object
449
310
  # @param [String] key optionally the key of the channel
450
- # @return [void]
311
+ # @return [Channel] The joined channel
451
312
  # @see Channel#join
452
313
  def join(channel, key = nil)
453
- Channel(channel).join(key)
314
+ channel = Channel(channel)
315
+ channel.join(key)
316
+
317
+ channel
454
318
  end
455
319
 
456
320
  # Part a channel.
457
321
  #
458
322
  # @param [String, Channel] channel either the name of a channel or a {Channel} object
459
323
  # @param [String] reason an optional reason/part message
460
- # @return [void]
324
+ # @return [Channel] The channel that was left
461
325
  # @see Channel#part
462
326
  def part(channel, reason = nil)
463
- Channel(channel).part(reason)
327
+ channel = Channel(channel)
328
+ channel.part(reason)
329
+
330
+ channel
464
331
  end
465
332
 
466
333
  # @endgroup
467
334
 
468
- # (see Logger::Logger#debug)
469
- # @see Logger::Logger#debug
470
- def debug(msg)
471
- @logger.debug(msg)
472
- end
473
-
474
335
  # @return [Boolean] True if the bot reports ISUPPORT violations as
475
336
  # exceptions.
476
337
  def strict?
@@ -479,66 +340,76 @@ module Cinch
479
340
 
480
341
  # @yield
481
342
  def initialize(&b)
482
- @logger = Logger::FormattedLogger.new($stderr)
483
- @events = {}
484
- @config = OpenStruct.new({
485
- :server => "localhost",
486
- :port => 6667,
487
- :ssl => OpenStruct.new({
488
- :use => false,
489
- :verify => false,
490
- :client_cert => nil,
491
- :ca_path => "/etc/ssl/certs",
492
- }),
493
- :password => nil,
494
- :nick => "cinch",
495
- :nicks => nil,
496
- :realname => "cinch",
497
- :user => "cinch",
498
- :verbose => true,
499
- :messages_per_second => 0.5,
500
- :server_queue_size => 10,
501
- :strictness => :forgiving,
502
- :message_split_start => '... ',
503
- :message_split_end => ' ...',
504
- :max_messages => nil,
505
- :plugins => OpenStruct.new({
506
- :plugins => [],
507
- :prefix => /^!/,
508
- :suffix => nil,
509
- :options => Hash.new {|h,k| h[k] = {}},
510
- }),
511
- :channels => [],
512
- :encoding => :irc,
513
- :reconnect => true,
514
- :local_host => nil,
515
- :timeouts => OpenStruct.new({
516
- :read => 240,
517
- :connect => 10,
518
- }),
519
- :ping_interval => 120,
520
- })
343
+ @loggers = LoggerList.new
344
+ @loggers << Logger::FormattedLogger.new($stderr)
521
345
 
346
+ @config = Configuration::Bot.new
347
+ @handlers = HandlerList.new
522
348
  @semaphores_mutex = Mutex.new
523
- @semaphores = Hash.new { |h,k| h[k] = Mutex.new }
524
- @plugins = []
525
- @callback = Callback.new(self)
526
- @channels = []
527
- @handler_threads = []
528
- @quitting = false
349
+ @semaphores = Hash.new { |h, k| h[k] = Mutex.new }
350
+ @callback = Callback.new(self)
351
+ @channels = []
352
+ @quitting = false
353
+ @modes = []
529
354
 
530
- @user_manager = UserManager.new(self)
531
- @channel_manager = ChannelManager.new(self)
355
+ @user_list = UserList.new(self)
356
+ @channel_list = ChannelList.new(self)
357
+ @plugins = PluginList.new(self)
532
358
 
359
+ super(nil, self)
533
360
  instance_eval(&b) if block_given?
361
+ end
534
362
 
535
- on :connect do
536
- bot.config.channels.each do |channel|
537
- bot.join channel
538
- end
363
+ # @since 2.0.0
364
+ # @return [self]
365
+ # @api private
366
+ def bot
367
+ # This method is needed for the Helpers interface
368
+ self
369
+ end
370
+
371
+ # Sets a mode on the bot.
372
+ #
373
+ # @param [String] mode
374
+ # @return [void]
375
+ # @since 2.0.0
376
+ # @see Bot#modes
377
+ # @see Bot#unset_mode
378
+ def set_mode(mode)
379
+ @modes << mode unless @modes.include?(mode)
380
+ @irc.send "MODE #{nick} +#{mode}"
381
+ end
382
+
383
+ # Unsets a mode on the bot.
384
+ #
385
+ # @param [String] mode
386
+ # @return [void]
387
+ # @since 2.0.0
388
+ def unset_mode(mode)
389
+ @modes.delete(mode)
390
+ @irc.send "MODE #{nick} -#{mode}"
391
+ end
392
+
393
+ # @since 2.0.0
394
+ def modes=(modes)
395
+ (@modes - modes).each do |mode|
396
+ unset_mode(mode)
397
+ end
398
+
399
+ (modes - @modes).each do |mode|
400
+ set_mode(mode)
539
401
  end
540
402
  end
541
403
 
404
+ # Used for updating the bot's nick from within the IRC parser.
405
+ #
406
+ # @param [String] nick
407
+ # @api private
408
+ # @return [String]
409
+ def set_nick(nick)
410
+ @name = nick
411
+ end
412
+
542
413
  # The bot's nickname.
543
414
  # @overload nick=(new_nick)
544
415
  # @raise [Exceptions::NickTooLong] Raised if the bot is
@@ -548,11 +419,8 @@ module Cinch
548
419
  # @overload nick
549
420
  # @return [String]
550
421
  # @return [String]
551
- attr_accessor :nick
552
- undef_method "nick"
553
- undef_method "nick="
554
422
  def nick
555
- @config.nick
423
+ @name
556
424
  end
557
425
 
558
426
  def nick=(new_nick)
@@ -560,7 +428,7 @@ module Cinch
560
428
  raise Exceptions::NickTooLong, new_nick
561
429
  end
562
430
  @config.nick = new_nick
563
- raw "NICK #{new_nick}"
431
+ @irc.send "NICK #{new_nick}"
564
432
  end
565
433
 
566
434
  # Try to create a free nick, first by cycling through all
@@ -568,80 +436,37 @@ module Cinch
568
436
  #
569
437
  # @param [String] base The base nick to start trying from
570
438
  # @api private
571
- # @return String
572
- def generate_next_nick(base = nil)
439
+ # @return [String]
440
+ # @since 2.0.0
441
+ def generate_next_nick!(base = nil)
573
442
  nicks = @config.nicks || []
574
443
 
575
444
  if base
576
445
  # if `base` is not in our list of nicks to try, assume that it's
577
446
  # custom and just append an underscore
578
447
  if !nicks.include?(base)
579
- return base + "_"
448
+ new_nick = base + "_"
580
449
  else
581
450
  # if we have a base, try the next nick or append an
582
451
  # underscore if no more nicks are left
583
452
  new_index = nicks.index(base) + 1
584
453
  if nicks[new_index]
585
- return nicks[new_index]
454
+ new_nick = nicks[new_index]
586
455
  else
587
- return base + "_"
456
+ new_nick = base + "_"
588
457
  end
589
458
  end
590
459
  else
591
460
  # if we have no base, try the first possible nick
592
461
  new_nick = @config.nicks ? @config.nicks.first : @config.nick
593
462
  end
594
- end
595
-
596
- # @return [Boolean] True if the bot is using SSL to connect to the
597
- # server.
598
- def secure?
599
- @config[:ssl] == true || (@config[:ssl].is_a?(Hash) && @config[:ssl][:use])
600
- end
601
-
602
- # This method is only provided in order to give Bot and User a
603
- # common interface.
604
- #
605
- # @return [false] Always returns `false`.
606
- # @see User#unknown? See User#unknown? for the method's real use.
607
- def unknown?
608
- false
609
- end
610
-
611
- [:host, :mask, :user, :realname, :signed_on_at, :secure?].each do |attr|
612
- define_method(attr) do
613
- User(nick).__send__(attr)
614
- end
615
- end
616
463
 
617
- private
618
- def find(type, msg = nil)
619
- if events = @events[type]
620
- if msg.nil?
621
- return events
622
- end
623
-
624
- events.select { |regexps|
625
- regexps.first.any? { |regexp|
626
- msg.match(regexp.to_r(msg), type)
627
- }
628
- }
629
- end
464
+ @config.nick = new_nick
630
465
  end
631
466
 
632
- def invoke(block, args, msg, match, arguments)
633
- bargs = match + arguments
634
- @handler_threads << Thread.new do
635
- begin
636
- catch(:halt) do
637
- @callback.instance_exec(msg, *args, *bargs, &block)
638
- end
639
- rescue => e
640
- @logger.log_exception(e)
641
- ensure
642
- @handler_threads.delete Thread.current
643
- end
644
- end
467
+ # @return [String]
468
+ def inspect
469
+ "#<Bot nick=#{@name.inspect}>"
645
470
  end
646
471
  end
647
472
  end