grinch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +22 -0
  4. data/README.md +180 -0
  5. data/docs/bot_options.md +454 -0
  6. data/docs/changes.md +541 -0
  7. data/docs/common_mistakes.md +60 -0
  8. data/docs/common_tasks.md +57 -0
  9. data/docs/encodings.md +69 -0
  10. data/docs/events.md +273 -0
  11. data/docs/getting_started.md +184 -0
  12. data/docs/logging.md +90 -0
  13. data/docs/migrating.md +267 -0
  14. data/docs/plugins.md +4 -0
  15. data/docs/readme.md +20 -0
  16. data/examples/basic/autovoice.rb +32 -0
  17. data/examples/basic/google.rb +35 -0
  18. data/examples/basic/hello.rb +15 -0
  19. data/examples/basic/join_part.rb +34 -0
  20. data/examples/basic/memo.rb +39 -0
  21. data/examples/basic/msg.rb +16 -0
  22. data/examples/basic/seen.rb +36 -0
  23. data/examples/basic/urban_dict.rb +35 -0
  24. data/examples/basic/url_shorten.rb +35 -0
  25. data/examples/plugins/autovoice.rb +37 -0
  26. data/examples/plugins/custom_prefix.rb +23 -0
  27. data/examples/plugins/dice_roll.rb +38 -0
  28. data/examples/plugins/google.rb +36 -0
  29. data/examples/plugins/hello.rb +22 -0
  30. data/examples/plugins/hooks.rb +36 -0
  31. data/examples/plugins/join_part.rb +42 -0
  32. data/examples/plugins/lambdas.rb +35 -0
  33. data/examples/plugins/last_nick.rb +24 -0
  34. data/examples/plugins/msg.rb +22 -0
  35. data/examples/plugins/multiple_matches.rb +32 -0
  36. data/examples/plugins/own_events.rb +37 -0
  37. data/examples/plugins/seen.rb +45 -0
  38. data/examples/plugins/timer.rb +22 -0
  39. data/examples/plugins/url_shorten.rb +33 -0
  40. data/lib/cinch.rb +5 -0
  41. data/lib/cinch/ban.rb +50 -0
  42. data/lib/cinch/bot.rb +479 -0
  43. data/lib/cinch/cached_list.rb +19 -0
  44. data/lib/cinch/callback.rb +20 -0
  45. data/lib/cinch/channel.rb +463 -0
  46. data/lib/cinch/channel_list.rb +29 -0
  47. data/lib/cinch/configuration.rb +73 -0
  48. data/lib/cinch/configuration/bot.rb +48 -0
  49. data/lib/cinch/configuration/dcc.rb +16 -0
  50. data/lib/cinch/configuration/plugins.rb +41 -0
  51. data/lib/cinch/configuration/sasl.rb +19 -0
  52. data/lib/cinch/configuration/ssl.rb +19 -0
  53. data/lib/cinch/configuration/timeouts.rb +14 -0
  54. data/lib/cinch/constants.rb +533 -0
  55. data/lib/cinch/dcc.rb +12 -0
  56. data/lib/cinch/dcc/dccable_object.rb +37 -0
  57. data/lib/cinch/dcc/incoming.rb +1 -0
  58. data/lib/cinch/dcc/incoming/send.rb +147 -0
  59. data/lib/cinch/dcc/outgoing.rb +1 -0
  60. data/lib/cinch/dcc/outgoing/send.rb +122 -0
  61. data/lib/cinch/exceptions.rb +46 -0
  62. data/lib/cinch/formatting.rb +125 -0
  63. data/lib/cinch/handler.rb +118 -0
  64. data/lib/cinch/handler_list.rb +90 -0
  65. data/lib/cinch/helpers.rb +231 -0
  66. data/lib/cinch/irc.rb +924 -0
  67. data/lib/cinch/isupport.rb +98 -0
  68. data/lib/cinch/log_filter.rb +21 -0
  69. data/lib/cinch/logger.rb +168 -0
  70. data/lib/cinch/logger/formatted_logger.rb +97 -0
  71. data/lib/cinch/logger/zcbot_logger.rb +22 -0
  72. data/lib/cinch/logger_list.rb +85 -0
  73. data/lib/cinch/mask.rb +69 -0
  74. data/lib/cinch/message.rb +392 -0
  75. data/lib/cinch/message_queue.rb +107 -0
  76. data/lib/cinch/mode_parser.rb +76 -0
  77. data/lib/cinch/network.rb +104 -0
  78. data/lib/cinch/open_ended_queue.rb +26 -0
  79. data/lib/cinch/pattern.rb +65 -0
  80. data/lib/cinch/plugin.rb +515 -0
  81. data/lib/cinch/plugin_list.rb +38 -0
  82. data/lib/cinch/rubyext/float.rb +3 -0
  83. data/lib/cinch/rubyext/module.rb +26 -0
  84. data/lib/cinch/rubyext/string.rb +33 -0
  85. data/lib/cinch/sasl.rb +34 -0
  86. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  87. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  88. data/lib/cinch/sasl/mechanism.rb +6 -0
  89. data/lib/cinch/sasl/plain.rb +26 -0
  90. data/lib/cinch/syncable.rb +83 -0
  91. data/lib/cinch/target.rb +199 -0
  92. data/lib/cinch/timer.rb +145 -0
  93. data/lib/cinch/user.rb +488 -0
  94. data/lib/cinch/user_list.rb +87 -0
  95. data/lib/cinch/utilities/deprecation.rb +16 -0
  96. data/lib/cinch/utilities/encoding.rb +37 -0
  97. data/lib/cinch/utilities/kernel.rb +13 -0
  98. data/lib/cinch/version.rb +4 -0
  99. metadata +140 -0
@@ -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