grinch 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,22 @@
1
+ require 'cinch'
2
+
3
+ class Hello
4
+ include Cinch::Plugin
5
+
6
+ match "hello"
7
+
8
+ def execute(m)
9
+ m.reply "Hello, #{m.user.nick}"
10
+ end
11
+ end
12
+
13
+ bot = Cinch::Bot.new do
14
+ configure do |c|
15
+ c.server = "irc.freenode.org"
16
+ c.channels = ["#cinch-bots"]
17
+ c.plugins.plugins = [Hello]
18
+ end
19
+ end
20
+
21
+ bot.start
22
+
@@ -0,0 +1,36 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'cinch'
3
+
4
+ class HooksDemo
5
+ include Cinch::Plugin
6
+
7
+ hook :pre, method: :generate_random_number
8
+ def generate_random_number(m)
9
+ # Hooks are called in the same thread as the handler and thus
10
+ # using thread local variables is possible.
11
+ Thread.current[:rand] = Kernel.rand
12
+ end
13
+
14
+ hook :post, method: :cheer
15
+ def cheer(m)
16
+ m.reply "Yay, I successfully ran a command…"
17
+ end
18
+
19
+ match "rand"
20
+ def execute(m)
21
+ m.reply "Random number: " + Thread.current[:rand].to_s
22
+ end
23
+ end
24
+
25
+ bot = Cinch::Bot.new do
26
+ configure do |c|
27
+ c.nick = "cinch_hooks"
28
+ c.server = "irc.freenode.org"
29
+ c.channels = ["#cinch-bots"]
30
+ c.verbose = true
31
+ c.plugins.plugins = [HooksDemo]
32
+ end
33
+ end
34
+
35
+
36
+ bot.start
@@ -0,0 +1,42 @@
1
+ require 'cinch'
2
+
3
+ class JoinPart
4
+ include Cinch::Plugin
5
+
6
+ match /join (.+)/, method: :join
7
+ match /part(?: (.+))?/, method: :part
8
+
9
+ def initialize(*args)
10
+ super
11
+
12
+ @admins = ["injekt", "DominikH"]
13
+ end
14
+
15
+ def check_user(user)
16
+ user.refresh # be sure to refresh the data, or someone could steal
17
+ # the nick
18
+ @admins.include?(user.authname)
19
+ end
20
+
21
+ def join(m, channel)
22
+ return unless check_user(m.user)
23
+ Channel(channel).join
24
+ end
25
+
26
+ def part(m, channel)
27
+ return unless check_user(m.user)
28
+ channel ||= m.channel
29
+ Channel(channel).part if channel
30
+ end
31
+ end
32
+
33
+ bot = Cinch::Bot.new do
34
+ configure do |c|
35
+ c.server = "irc.freenode.org"
36
+ c.nick = "CinchBot"
37
+ c.channels = ["#cinch-bots"]
38
+ c.plugins.plugins = [JoinPart]
39
+ end
40
+ end
41
+
42
+ bot.start
@@ -0,0 +1,35 @@
1
+ require 'cinch'
2
+
3
+ class DirectAddressing
4
+ include Cinch::Plugin
5
+
6
+ # Note: the lambda will be executed in the context it has been
7
+ # defined in, in this case the class DirectAddressing (and not an
8
+ # instance of said class).
9
+ #
10
+ # The reason we are using a lambda is that the bot's nick can change
11
+ # and the prefix has to be up to date.
12
+ set :prefix, lambda{ |m| Regexp.new("^" + Regexp.escape(m.bot.nick + ": " ))}
13
+
14
+ match "hello", method: :greet
15
+ def greet(m)
16
+ m.reply "Hello to you, too."
17
+ end
18
+
19
+ match "rename", method: :rename
20
+ def rename(m)
21
+ @bot.nick += "_"
22
+ end
23
+ end
24
+
25
+ bot = Cinch::Bot.new do
26
+ configure do |c|
27
+ c.nick = "cinch_lambda"
28
+ c.server = "irc.freenode.org"
29
+ c.channels = ["#cinch-bots"]
30
+ c.verbose = true
31
+ c.plugins.plugins = [DirectAddressing]
32
+ end
33
+ end
34
+
35
+ bot.start
@@ -0,0 +1,24 @@
1
+ require 'cinch'
2
+
3
+ class Nickchange
4
+ include Cinch::Plugin
5
+ listen_to :nick
6
+
7
+ def listen(m)
8
+ # This will send a PM to the user who changed their nick and inform
9
+ # them of their old nick.
10
+ m.reply "Your old nick was: #{m.user.last_nick}" ,true
11
+ end
12
+ end
13
+
14
+ bot = Cinch::Bot.new do
15
+ configure do |c|
16
+ c.nick = "cinch_nickchange"
17
+ c.server = "irc.freenode.org"
18
+ c.channels = ["#cinch-bots"]
19
+ c.verbose = true
20
+ c.plugins.plugins = [Nickchange]
21
+ end
22
+ end
23
+
24
+ bot.start
@@ -0,0 +1,22 @@
1
+ require 'cinch'
2
+
3
+ class Messenger
4
+ include Cinch::Plugin
5
+
6
+ match /msg (.+?) (.+)/
7
+ def execute(m, receiver, message)
8
+ User(receiver).send(message)
9
+ end
10
+ end
11
+
12
+ bot = Cinch::Bot.new do
13
+ configure do |c|
14
+ c.server = "irc.freenode.org"
15
+ c.nick = "CinchBot"
16
+ c.channels = ["#cinch-bots"]
17
+ c.plugins.plugins = [Messenger]
18
+ end
19
+ end
20
+
21
+ bot.start
22
+
@@ -0,0 +1,32 @@
1
+ require 'cinch'
2
+
3
+ class MultiCommands
4
+ include Cinch::Plugin
5
+ match /command1 (.+)/, method: :command1
6
+ match /command2 (.+)/, method: :command2
7
+ match /^command3 (.+)/, use_prefix: false
8
+
9
+ def command1(m, arg)
10
+ m.reply "command1, arg: #{arg}"
11
+ end
12
+
13
+ def command2(m, arg)
14
+ m.reply "command2, arg: #{arg}"
15
+ end
16
+
17
+ def execute(m, arg)
18
+ m.reply "command3, arg: #{arg}"
19
+ end
20
+ end
21
+
22
+ bot = Cinch::Bot.new do
23
+ configure do |c|
24
+ c.nick = "cinch_multi"
25
+ c.server = "irc.freenode.org"
26
+ c.channels = ["#cinch-bots"]
27
+ c.verbose = true
28
+ c.plugins.plugins = [MultiCommands]
29
+ end
30
+ end
31
+
32
+ bot.start
@@ -0,0 +1,37 @@
1
+ require 'cinch'
2
+
3
+ class RandomNumberGenerator
4
+ def initialize(bot)
5
+ @bot = bot
6
+ end
7
+
8
+ def start
9
+ while true
10
+ sleep 5 # pretend that we are waiting for some kind of entropy
11
+ @bot.handlers.dispatch(:random_number, nil, Kernel.rand)
12
+ end
13
+ end
14
+ end
15
+
16
+ class DoSomethingRandom
17
+ include Cinch::Plugin
18
+
19
+ listen_to :random_number
20
+ def listen(m, number)
21
+ Channel("#cinch-bots").send "I got a random number: #{number}"
22
+ end
23
+ end
24
+
25
+ bot = Cinch::Bot.new do
26
+ configure do |c|
27
+ c.nick = "cinch_events"
28
+ c.server = "irc.freenode.org"
29
+ c.channels = ["#cinch-bots"]
30
+ c.verbose = true
31
+ c.plugins.plugins = [DoSomethingRandom]
32
+ end
33
+ end
34
+
35
+
36
+ Thread.new { RandomNumberGenerator.new(bot).start }
37
+ bot.start
@@ -0,0 +1,45 @@
1
+ require 'cinch'
2
+
3
+ class Seen
4
+ class SeenStruct < Struct.new(:who, :where, :what, :time)
5
+ def to_s
6
+ "[#{time.asctime}] #{who} was seen in #{where} saying #{what}"
7
+ end
8
+ end
9
+
10
+ include Cinch::Plugin
11
+ listen_to :channel
12
+ match /seen (.+)/
13
+
14
+ def initialize(*args)
15
+ super
16
+ @users = {}
17
+ end
18
+
19
+ def listen(m)
20
+ @users[m.user.nick] = SeenStruct.new(m.user, m.channel, m.message, Time.now)
21
+ end
22
+
23
+ def execute(m, nick)
24
+ if nick == @bot.nick
25
+ m.reply "That's me!"
26
+ elsif nick == m.user.nick
27
+ m.reply "That's you!"
28
+ elsif @users.key?(nick)
29
+ m.reply @users[nick].to_s
30
+ else
31
+ m.reply "I haven't seen #{nick}"
32
+ end
33
+ end
34
+ end
35
+
36
+ bot = Cinch::Bot.new do
37
+ configure do |c|
38
+ c.server = 'irc.freenode.org'
39
+ c.channels = ["#cinch-bots"]
40
+ c.plugins.plugins = [Seen]
41
+ end
42
+ end
43
+
44
+ bot.start
45
+
@@ -0,0 +1,22 @@
1
+ require 'cinch'
2
+
3
+ class TimedPlugin
4
+ include Cinch::Plugin
5
+
6
+ timer 5, method: :timed
7
+ def timed
8
+ Channel("#cinch-bots").send "5 seconds have passed"
9
+ end
10
+ end
11
+
12
+ bot = Cinch::Bot.new do
13
+ configure do |c|
14
+ c.nick = "cinch_timer"
15
+ c.server = "irc.freenode.org"
16
+ c.channels = ["#cinch-bots"]
17
+ c.verbose = true
18
+ c.plugins.plugins = [TimedPlugin]
19
+ end
20
+ end
21
+
22
+ bot.start
@@ -0,0 +1,33 @@
1
+ require 'open-uri'
2
+ require 'cinch'
3
+
4
+ class TinyURL
5
+ include Cinch::Plugin
6
+
7
+ listen_to :channel
8
+
9
+ def shorten(url)
10
+ url = open("http://tinyurl.com/api-create.php?url=#{URI.escape(url)}").read
11
+ url == "Error" ? nil : url
12
+ rescue OpenURI::HTTPError
13
+ nil
14
+ end
15
+
16
+ def listen(m)
17
+ urls = URI.extract(m.message, "http")
18
+ short_urls = urls.map { |url| shorten(url) }.compact
19
+ unless short_urls.empty?
20
+ m.reply short_urls.join(", ")
21
+ end
22
+ end
23
+ end
24
+
25
+ bot = Cinch::Bot.new do
26
+ configure do |c|
27
+ c.server = "irc.freenode.org"
28
+ c.channels = ["#cinch-bots"]
29
+ c.plugins.plugins = [TinyURL]
30
+ end
31
+ end
32
+
33
+ bot.start
data/lib/cinch.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'cinch/version'
2
+ require 'cinch/utilities/kernel'
3
+ require 'cinch/utilities/deprecation'
4
+ require 'cinch/utilities/encoding'
5
+ require 'cinch/bot'
data/lib/cinch/ban.rb ADDED
@@ -0,0 +1,50 @@
1
+ require "cinch/mask"
2
+ module Cinch
3
+ # This class represents channel bans.
4
+ class Ban
5
+ # @return [Mask] A {Mask} object for non-extended bans
6
+ # @return [String] A String object for extended bans (see {#extended})
7
+ attr_reader :mask
8
+
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
14
+ attr_reader :by
15
+
16
+ # @return [Time]
17
+ attr_reader :created_at
18
+
19
+ # @return [Boolean] whether this is an extended ban (as used by for example Freenode)
20
+ attr_reader :extended
21
+
22
+ # @param [String, Mask] mask The mask
23
+ # @param [User, nil] by The user who created the ban.
24
+ # @param [Time] at The time at which the ban was created
25
+ def initialize(mask, by, at)
26
+ @by, @created_at = by, at
27
+ if mask =~ /^[\$~]/
28
+ @extended = true
29
+ @mask = mask
30
+ else
31
+ @extended = false
32
+ @mask = Mask.from(mask)
33
+ end
34
+ end
35
+
36
+ # @return [Boolean] true if the ban matches `user`
37
+ # @raise [Exceptions::UnsupportedFeature] Cinch does not support
38
+ # Freenode's extended bans
39
+ def match(user)
40
+ raise UnsupportedFeature, "extended bans are not supported yet" if @extended
41
+ @mask =~ user
42
+ end
43
+ alias_method :=~, :match
44
+
45
+ # @return [String]
46
+ def to_s
47
+ @mask.to_s
48
+ end
49
+ end
50
+ end
data/lib/cinch/bot.rb ADDED
@@ -0,0 +1,479 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'socket'
3
+ require "thread"
4
+ require "ostruct"
5
+ require "cinch/rubyext/module"
6
+ require "cinch/rubyext/string"
7
+ require "cinch/rubyext/float"
8
+
9
+ require "cinch/exceptions"
10
+
11
+ require "cinch/handler"
12
+ require "cinch/helpers"
13
+
14
+ require "cinch/logger_list"
15
+ require "cinch/logger"
16
+
17
+ require "cinch/logger/formatted_logger"
18
+ require "cinch/syncable"
19
+ require "cinch/message"
20
+ require "cinch/message_queue"
21
+ require "cinch/irc"
22
+ require "cinch/target"
23
+ require "cinch/channel"
24
+ require "cinch/user"
25
+ require "cinch/constants"
26
+ require "cinch/callback"
27
+ require "cinch/ban"
28
+ require "cinch/mask"
29
+ require "cinch/isupport"
30
+ require "cinch/plugin"
31
+ require "cinch/pattern"
32
+ require "cinch/mode_parser"
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/dcc"
51
+ require "cinch/configuration/sasl"
52
+
53
+ module Cinch
54
+ # @attr nick
55
+ # @version 2.0.0
56
+ class Bot < User
57
+ include Helpers
58
+
59
+
60
+ # @return [Configuration::Bot]
61
+ # @version 2.0.0
62
+ attr_reader :config
63
+
64
+ # The underlying IRC connection
65
+ #
66
+ # @return [IRC]
67
+ attr_reader :irc
68
+
69
+ # The logger list containing all loggers
70
+ #
71
+ # @return [LoggerList]
72
+ # @since 2.0.0
73
+ attr_accessor :loggers
74
+
75
+ # @return [Array<Channel>] All channels the bot currently is in
76
+ attr_reader :channels
77
+
78
+ # @return [PluginList] The {PluginList} giving access to
79
+ # (un)loading plugins
80
+ # @version 2.0.0
81
+ attr_reader :plugins
82
+
83
+ # @return [Boolean] whether the bot is in the process of disconnecting
84
+ attr_reader :quitting
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 [Boolean]
97
+ # @api private
98
+ attr_accessor :last_connection_was_successful
99
+
100
+ # @return [Callback]
101
+ # @api private
102
+ attr_reader :callback
103
+
104
+ # The {HandlerList}, providing access to all registered plugins
105
+ # and plugin manipulation as well as {HandlerList#dispatch calling handlers}.
106
+ #
107
+ # @return [HandlerList]
108
+ # @see HandlerList
109
+ # @since 2.0.0
110
+ attr_reader :handlers
111
+
112
+ # The bot's modes.
113
+ #
114
+ # @return [Array<String>]
115
+ # @since 2.0.0
116
+ attr_reader :modes
117
+
118
+ # @group Helper methods
119
+
120
+ # Define helper methods in the context of the bot.
121
+ #
122
+ # @yield Expects a block containing method definitions
123
+ # @return [void]
124
+ def helpers(&b)
125
+ @callback.instance_eval(&b)
126
+ end
127
+
128
+ # Since Cinch uses threads, all handlers can be run
129
+ # simultaneously, even the same handler multiple times. This also
130
+ # means, that your code has to be thread-safe. Most of the time,
131
+ # this is not a problem, but if you are accessing stored data, you
132
+ # will most likely have to synchronize access to it. Instead of
133
+ # managing all mutexes yourself, Cinch provides a synchronize
134
+ # method, which takes a name and block.
135
+ #
136
+ # Synchronize blocks with the same name share the same mutex,
137
+ # which means that only one of them will be executed at a time.
138
+ #
139
+ # @param [String, Symbol] name a name for the synchronize block.
140
+ # @return [void]
141
+ # @yield
142
+ #
143
+ # @example
144
+ # configure do |c|
145
+ # …
146
+ # @i = 0
147
+ # end
148
+ #
149
+ # on :channel, /^start counting!/ do
150
+ # synchronize(:my_counter) do
151
+ # 10.times do
152
+ # val = @i
153
+ # # at this point, another thread might've incremented :i already.
154
+ # # this thread wouldn't know about it, though.
155
+ # @i = val + 1
156
+ # end
157
+ # end
158
+ # end
159
+ def synchronize(name, &block)
160
+ # Must run the default block +/ fetch in a thread safe way in order to
161
+ # ensure we always get the same mutex for a given name.
162
+ semaphore = @semaphores_mutex.synchronize { @semaphores[name] }
163
+ semaphore.synchronize(&block)
164
+ end
165
+
166
+ # @endgroup
167
+
168
+ # @group Events &amp; Plugins
169
+
170
+ # Registers a handler.
171
+ #
172
+ # @param [String, Symbol, Integer] event the event to match. For a
173
+ # list of available events, check the {file:docs/events.md Events
174
+ # documentation}.
175
+ #
176
+ # @param [Regexp, Pattern, String] regexp every message of the
177
+ # right event will be checked against this argument and the event
178
+ # will only be called if it matches
179
+ #
180
+ # @param [Array<Object>] args Arguments that should be passed to
181
+ # the block, additionally to capture groups of the regexp.
182
+ #
183
+ # @yieldparam [Array<String>] args each capture group of the regex will
184
+ # be one argument to the block.
185
+ #
186
+ # @return [Handler] The handlers that have been registered
187
+ def on(event, regexp = //, *args, &block)
188
+ event = event.to_s.to_sym
189
+
190
+ pattern = case regexp
191
+ when Pattern
192
+ regexp
193
+ when Regexp
194
+ Pattern.new(nil, regexp, nil)
195
+ else
196
+ if event == :ctcp
197
+ Pattern.generate(:ctcp, regexp)
198
+ else
199
+ Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
200
+ end
201
+ end
202
+
203
+ handler = Handler.new(self, event, pattern, {args: args, execute_in_callback: true}, &block)
204
+ @handlers.register(handler)
205
+
206
+ return handler
207
+ end
208
+
209
+ # @endgroup
210
+ # @group Bot Control
211
+
212
+ # This method is used to set a bot's options. It indeed does
213
+ # nothing else but yielding {Bot#config}, but it makes for a nice DSL.
214
+ #
215
+ # @yieldparam [Struct] config the bot's config
216
+ # @return [void]
217
+ def configure
218
+ yield @config
219
+ end
220
+
221
+ # Disconnects from the server.
222
+ #
223
+ # @param [String] message The quit message to send while quitting
224
+ # @return [void]
225
+ def quit(message = nil)
226
+ @quitting = true
227
+ command = message ? "QUIT :#{message}" : "QUIT"
228
+
229
+ @irc.send command
230
+ end
231
+
232
+ # Connects the bot to a server.
233
+ #
234
+ # @param [Boolean] plugins Automatically register plugins from
235
+ # `@config.plugins.plugins`?
236
+ # @return [void]
237
+ def start(plugins = true)
238
+ @reconnects = 0
239
+ @plugins.register_plugins(@config.plugins.plugins) if plugins
240
+
241
+ begin
242
+ @user_list.each do |user|
243
+ user.in_whois = false
244
+ user.unsync_all
245
+ end # reset state of all users
246
+
247
+ @channel_list.each do |channel|
248
+ channel.unsync_all
249
+ end # reset state of all channels
250
+
251
+ @channels = [] # reset list of channels the bot is in
252
+
253
+ @join_handler.unregister if @join_handler
254
+ @join_timer.stop if @join_timer
255
+
256
+ join_lambda = lambda { @config.channels.each { |channel| Channel(channel).join }}
257
+
258
+ if @config.delay_joins.is_a?(Symbol)
259
+ @join_handler = join_handler = on(@config.delay_joins) {
260
+ join_handler.unregister
261
+ join_lambda.call
262
+ }
263
+ else
264
+ @join_timer = Timer.new(self, interval: @config.delay_joins, shots: 1) {
265
+ join_lambda.call
266
+ }
267
+ end
268
+
269
+ @modes = []
270
+
271
+ @loggers.info "Connecting to #{@config.server}:#{@config.port}"
272
+ @irc = IRC.new(self)
273
+ @irc.start
274
+
275
+ if @config.reconnect && !@quitting
276
+ # double the delay for each unsuccesful reconnection attempt
277
+ if @last_connection_was_successful
278
+ @reconnects = 0
279
+ @last_connection_was_successful = false
280
+ else
281
+ @reconnects += 1
282
+ end
283
+
284
+ # Throttle reconnect attempts
285
+ wait = 2**@reconnects
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
292
+ end
293
+ end while @config.reconnect and not @quitting
294
+ end
295
+
296
+ # @endgroup
297
+ # @group Channel Control
298
+
299
+ # Join a channel.
300
+ #
301
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
302
+ # @param [String] key optionally the key of the channel
303
+ # @return [Channel] The joined channel
304
+ # @see Channel#join
305
+ def join(channel, key = nil)
306
+ channel = Channel(channel)
307
+ channel.join(key)
308
+
309
+ channel
310
+ end
311
+
312
+ # Part a channel.
313
+ #
314
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
315
+ # @param [String] reason an optional reason/part message
316
+ # @return [Channel] The channel that was left
317
+ # @see Channel#part
318
+ def part(channel, reason = nil)
319
+ channel = Channel(channel)
320
+ channel.part(reason)
321
+
322
+ channel
323
+ end
324
+
325
+ # @endgroup
326
+
327
+ # @return [Boolean] True if the bot reports ISUPPORT violations as
328
+ # exceptions.
329
+ def strict?
330
+ @config.strictness == :strict
331
+ end
332
+
333
+ # @yield
334
+ def initialize(&b)
335
+ @config = Configuration::Bot.new
336
+
337
+ @loggers = LoggerList.new
338
+ @loggers << Logger::FormattedLogger.new($stderr, level: @config.default_logger_level)
339
+ @handlers = HandlerList.new
340
+ @semaphores_mutex = Mutex.new
341
+ @semaphores = Hash.new { |h, k| h[k] = Mutex.new }
342
+ @callback = Callback.new(self)
343
+ @channels = []
344
+ @quitting = false
345
+ @modes = []
346
+
347
+ @user_list = UserList.new(self)
348
+ @channel_list = ChannelList.new(self)
349
+ @plugins = PluginList.new(self)
350
+
351
+ @join_handler = nil
352
+ @join_timer = nil
353
+
354
+ super(nil, self)
355
+ instance_eval(&b) if block_given?
356
+ end
357
+
358
+ # @since 2.0.0
359
+ # @return [self]
360
+ # @api private
361
+ def bot
362
+ # This method is needed for the Helpers interface
363
+ self
364
+ end
365
+
366
+ # Sets a mode on the bot.
367
+ #
368
+ # @param [String] mode
369
+ # @return [void]
370
+ # @since 2.0.0
371
+ # @see Bot#modes
372
+ # @see Bot#unset_mode
373
+ def set_mode(mode)
374
+ @modes << mode unless @modes.include?(mode)
375
+ @irc.send "MODE #{nick} +#{mode}"
376
+ end
377
+
378
+ # Unsets a mode on the bot.
379
+ #
380
+ # @param [String] mode
381
+ # @return [void]
382
+ # @since 2.0.0
383
+ def unset_mode(mode)
384
+ @modes.delete(mode)
385
+ @irc.send "MODE #{nick} -#{mode}"
386
+ end
387
+
388
+ # @since 2.0.0
389
+ def modes=(modes)
390
+ (@modes - modes).each do |mode|
391
+ unset_mode(mode)
392
+ end
393
+
394
+ (modes - @modes).each do |mode|
395
+ set_mode(mode)
396
+ end
397
+ end
398
+
399
+ # Used for updating the bot's nick from within the IRC parser.
400
+ #
401
+ # @param [String] nick
402
+ # @api private
403
+ # @return [String]
404
+ def set_nick(nick)
405
+ @name = nick
406
+ end
407
+
408
+ # The bot's nickname.
409
+ # @overload nick=(new_nick)
410
+ # @raise [Exceptions::NickTooLong] Raised if the bot is
411
+ # operating in {#strict? strict mode} and the new nickname is
412
+ # too long
413
+ # @return [String]
414
+ # @overload nick
415
+ # @return [String]
416
+ # @return [String]
417
+ def nick
418
+ @name
419
+ end
420
+
421
+ def nick=(new_nick)
422
+ if new_nick.size > @irc.isupport["NICKLEN"] && strict?
423
+ raise Exceptions::NickTooLong, new_nick
424
+ end
425
+ @config.nick = new_nick
426
+ @irc.send "NICK #{new_nick}"
427
+ end
428
+
429
+ # Gain oper privileges.
430
+ #
431
+ # @param [String] password
432
+ # @param [String] user The username to use. Defaults to the bot's
433
+ # nickname
434
+ # @since 2.1.0
435
+ # @return [void]
436
+ def oper(password, user = nil)
437
+ user ||= self.nick
438
+ @irc.send "OPER #{user} #{password}"
439
+ end
440
+
441
+ # Try to create a free nick, first by cycling through all
442
+ # available alternatives and then by appending underscores.
443
+ #
444
+ # @param [String] base The base nick to start trying from
445
+ # @api private
446
+ # @return [String]
447
+ # @since 2.0.0
448
+ def generate_next_nick!(base = nil)
449
+ nicks = @config.nicks || []
450
+
451
+ if base
452
+ # if `base` is not in our list of nicks to try, assume that it's
453
+ # custom and just append an underscore
454
+ if !nicks.include?(base)
455
+ new_nick = base + "_"
456
+ else
457
+ # if we have a base, try the next nick or append an
458
+ # underscore if no more nicks are left
459
+ new_index = nicks.index(base) + 1
460
+ if nicks[new_index]
461
+ new_nick = nicks[new_index]
462
+ else
463
+ new_nick = base + "_"
464
+ end
465
+ end
466
+ else
467
+ # if we have no base, try the first possible nick
468
+ new_nick = @config.nicks ? @config.nicks.first : @config.nick
469
+ end
470
+
471
+ @config.nick = new_nick
472
+ end
473
+
474
+ # @return [String]
475
+ def inspect
476
+ "#<Bot nick=#{@name.inspect}>"
477
+ end
478
+ end
479
+ end