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,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