mcinch 2.4.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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +23 -0
  4. data/README.md +15 -0
  5. data/README_OLD.md +175 -0
  6. data/docs/bot_options.md +454 -0
  7. data/docs/changes.md +541 -0
  8. data/docs/common_mistakes.md +60 -0
  9. data/docs/common_tasks.md +57 -0
  10. data/docs/encodings.md +69 -0
  11. data/docs/events.md +273 -0
  12. data/docs/getting_started.md +184 -0
  13. data/docs/logging.md +90 -0
  14. data/docs/migrating.md +267 -0
  15. data/docs/plugins.md +4 -0
  16. data/docs/readme.md +20 -0
  17. data/examples/basic/autovoice.rb +32 -0
  18. data/examples/basic/google.rb +35 -0
  19. data/examples/basic/hello.rb +15 -0
  20. data/examples/basic/join_part.rb +34 -0
  21. data/examples/basic/memo.rb +39 -0
  22. data/examples/basic/msg.rb +16 -0
  23. data/examples/basic/seen.rb +36 -0
  24. data/examples/basic/urban_dict.rb +35 -0
  25. data/examples/basic/url_shorten.rb +35 -0
  26. data/examples/plugins/autovoice.rb +37 -0
  27. data/examples/plugins/custom_prefix.rb +23 -0
  28. data/examples/plugins/dice_roll.rb +38 -0
  29. data/examples/plugins/google.rb +36 -0
  30. data/examples/plugins/hello.rb +22 -0
  31. data/examples/plugins/hooks.rb +36 -0
  32. data/examples/plugins/join_part.rb +42 -0
  33. data/examples/plugins/lambdas.rb +35 -0
  34. data/examples/plugins/last_nick.rb +24 -0
  35. data/examples/plugins/msg.rb +22 -0
  36. data/examples/plugins/multiple_matches.rb +32 -0
  37. data/examples/plugins/own_events.rb +37 -0
  38. data/examples/plugins/seen.rb +45 -0
  39. data/examples/plugins/timer.rb +22 -0
  40. data/examples/plugins/url_shorten.rb +33 -0
  41. data/lib/cinch.rb +5 -0
  42. data/lib/cinch/ban.rb +50 -0
  43. data/lib/cinch/bot.rb +489 -0
  44. data/lib/cinch/cached_list.rb +19 -0
  45. data/lib/cinch/callback.rb +20 -0
  46. data/lib/cinch/channel.rb +463 -0
  47. data/lib/cinch/channel_list.rb +29 -0
  48. data/lib/cinch/configuration.rb +73 -0
  49. data/lib/cinch/configuration/bot.rb +48 -0
  50. data/lib/cinch/configuration/dcc.rb +16 -0
  51. data/lib/cinch/configuration/plugins.rb +41 -0
  52. data/lib/cinch/configuration/sasl.rb +19 -0
  53. data/lib/cinch/configuration/ssl.rb +19 -0
  54. data/lib/cinch/configuration/timeouts.rb +14 -0
  55. data/lib/cinch/constants.rb +533 -0
  56. data/lib/cinch/dcc.rb +12 -0
  57. data/lib/cinch/dcc/dccable_object.rb +37 -0
  58. data/lib/cinch/dcc/incoming.rb +1 -0
  59. data/lib/cinch/dcc/incoming/send.rb +147 -0
  60. data/lib/cinch/dcc/outgoing.rb +1 -0
  61. data/lib/cinch/dcc/outgoing/send.rb +122 -0
  62. data/lib/cinch/exceptions.rb +46 -0
  63. data/lib/cinch/formatting.rb +125 -0
  64. data/lib/cinch/handler.rb +118 -0
  65. data/lib/cinch/handler_list.rb +90 -0
  66. data/lib/cinch/helpers.rb +231 -0
  67. data/lib/cinch/irc.rb +972 -0
  68. data/lib/cinch/isupport.rb +98 -0
  69. data/lib/cinch/log_filter.rb +21 -0
  70. data/lib/cinch/logger.rb +168 -0
  71. data/lib/cinch/logger/formatted_logger.rb +97 -0
  72. data/lib/cinch/logger/zcbot_logger.rb +22 -0
  73. data/lib/cinch/logger_list.rb +85 -0
  74. data/lib/cinch/mask.rb +69 -0
  75. data/lib/cinch/message.rb +391 -0
  76. data/lib/cinch/message_queue.rb +107 -0
  77. data/lib/cinch/mode_parser.rb +76 -0
  78. data/lib/cinch/network.rb +104 -0
  79. data/lib/cinch/open_ended_queue.rb +26 -0
  80. data/lib/cinch/pattern.rb +65 -0
  81. data/lib/cinch/plugin.rb +515 -0
  82. data/lib/cinch/plugin_list.rb +38 -0
  83. data/lib/cinch/rubyext/float.rb +3 -0
  84. data/lib/cinch/rubyext/module.rb +26 -0
  85. data/lib/cinch/rubyext/string.rb +33 -0
  86. data/lib/cinch/sasl.rb +34 -0
  87. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  88. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  89. data/lib/cinch/sasl/mechanism.rb +6 -0
  90. data/lib/cinch/sasl/plain.rb +26 -0
  91. data/lib/cinch/syncable.rb +83 -0
  92. data/lib/cinch/target.rb +199 -0
  93. data/lib/cinch/timer.rb +145 -0
  94. data/lib/cinch/user.rb +488 -0
  95. data/lib/cinch/user_list.rb +87 -0
  96. data/lib/cinch/utilities/deprecation.rb +16 -0
  97. data/lib/cinch/utilities/encoding.rb +37 -0
  98. data/lib/cinch/utilities/kernel.rb +13 -0
  99. data/lib/cinch/version.rb +6 -0
  100. metadata +147 -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
@@ -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'
@@ -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
@@ -0,0 +1,489 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
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
+ # @return [Configuration::Bot]
60
+ # @version 2.0.0
61
+ attr_reader :config
62
+
63
+ # The underlying IRC connection
64
+ #
65
+ # @return [IRC]
66
+ attr_reader :irc
67
+
68
+ # The logger list containing all loggers
69
+ #
70
+ # @return [LoggerList]
71
+ # @since 2.0.0
72
+ attr_accessor :loggers
73
+
74
+ # @return [Array<Channel>] All channels the bot currently is in
75
+ attr_reader :channels
76
+
77
+ # @return [PluginList] The {PluginList} giving access to
78
+ # (un)loading plugins
79
+ # @version 2.0.0
80
+ attr_reader :plugins
81
+
82
+ # @return [Boolean] whether the bot is in the process of disconnecting
83
+ attr_reader :quitting
84
+
85
+ # @return [Queue] queue for gentle process of disconnecting
86
+ attr_reader :quit_queue
87
+
88
+ # @return [UserList] All {User users} the bot knows about.
89
+ # @see UserList
90
+ # @since 1.1.0
91
+ attr_reader :user_list
92
+
93
+ # @return [ChannelList] All {Channel channels} the bot knows about.
94
+ # @see ChannelList
95
+ # @since 1.1.0
96
+ attr_reader :channel_list
97
+
98
+ # Whether the last connection was successful.
99
+ # @return [Boolean]
100
+ # @api private
101
+ attr_accessor :last_connection_was_successful
102
+
103
+ # The exception that occurred in the last connection.
104
+ # @return [StandardError, nil]
105
+ # @since 2.4.0
106
+ attr_accessor :last_connection_error
107
+
108
+ # @return [Callback]
109
+ # @api private
110
+ attr_reader :callback
111
+
112
+ # The {HandlerList}, providing access to all registered plugins
113
+ # and plugin manipulation as well as {HandlerList#dispatch calling handlers}.
114
+ #
115
+ # @return [HandlerList]
116
+ # @see HandlerList
117
+ # @since 2.0.0
118
+ attr_reader :handlers
119
+
120
+ # The bot's modes.
121
+ #
122
+ # @return [Array<String>]
123
+ # @since 2.0.0
124
+ attr_reader :modes
125
+
126
+ # @group Helper methods
127
+
128
+ # Define helper methods in the context of the bot.
129
+ #
130
+ # @yield Expects a block containing method definitions
131
+ # @return [void]
132
+ def helpers(&b)
133
+ @callback.instance_eval(&b)
134
+ end
135
+
136
+ # Since Cinch uses threads, all handlers can be run
137
+ # simultaneously, even the same handler multiple times. This also
138
+ # means, that your code has to be thread-safe. Most of the time,
139
+ # this is not a problem, but if you are accessing stored data, you
140
+ # will most likely have to synchronize access to it. Instead of
141
+ # managing all mutexes yourself, Cinch provides a synchronize
142
+ # method, which takes a name and block.
143
+ #
144
+ # Synchronize blocks with the same name share the same mutex,
145
+ # which means that only one of them will be executed at a time.
146
+ #
147
+ # @param [String, Symbol] name a name for the synchronize block.
148
+ # @return [void]
149
+ # @yield
150
+ #
151
+ # @example
152
+ # configure do |c|
153
+ # …
154
+ # @i = 0
155
+ # end
156
+ #
157
+ # on :channel, /^start counting!/ do
158
+ # synchronize(:my_counter) do
159
+ # 10.times do
160
+ # val = @i
161
+ # # at this point, another thread might've incremented :i already.
162
+ # # this thread wouldn't know about it, though.
163
+ # @i = val + 1
164
+ # end
165
+ # end
166
+ # end
167
+ def synchronize(name, &block)
168
+ # Must run the default block +/ fetch in a thread safe way in order to
169
+ # ensure we always get the same mutex for a given name.
170
+ semaphore = @semaphores_mutex.synchronize { @semaphores[name] }
171
+ semaphore.synchronize(&block)
172
+ end
173
+
174
+ # @endgroup
175
+
176
+ # @group Events &amp; Plugins
177
+
178
+ # Registers a handler.
179
+ #
180
+ # @param [String, Symbol, Integer] event the event to match. For a
181
+ # list of available events, check the {file:docs/events.md Events
182
+ # documentation}.
183
+ #
184
+ # @param [Regexp, Pattern, String] regexp every message of the
185
+ # right event will be checked against this argument and the event
186
+ # will only be called if it matches
187
+ #
188
+ # @param [Array<Object>] args Arguments that should be passed to
189
+ # the block, additionally to capture groups of the regexp.
190
+ #
191
+ # @yieldparam [Array<String>] args each capture group of the regex will
192
+ # be one argument to the block.
193
+ #
194
+ # @return [Handler] The handlers that have been registered
195
+ def on(event, regexp = //, *args, &block)
196
+ event = event.to_s.to_sym
197
+
198
+ pattern = case regexp
199
+ when Pattern
200
+ regexp
201
+ when Regexp
202
+ Pattern.new(nil, regexp, nil)
203
+ else
204
+ if event == :ctcp
205
+ Pattern.generate(:ctcp, regexp)
206
+ else
207
+ Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
208
+ end
209
+ end
210
+
211
+ handler = Handler.new(self, event, pattern, { args: args, execute_in_callback: true }, &block)
212
+ @handlers.register(handler)
213
+
214
+ handler
215
+ end
216
+
217
+ # @endgroup
218
+ # @group Bot Control
219
+
220
+ # This method is used to set a bot's options. It indeed does
221
+ # nothing else but yielding {Bot#config}, but it makes for a nice DSL.
222
+ #
223
+ # @yieldparam [Struct] config the bot's config
224
+ # @return [void]
225
+ def configure
226
+ yield @config
227
+ end
228
+
229
+ # Disconnects from the server.
230
+ #
231
+ # @param [String] message The quit message to send while quitting
232
+ # @return [void]
233
+ def quit(message = nil)
234
+ @quitting = true
235
+ @quit_queue.push([:quit, message])
236
+ end
237
+
238
+ # Connects the bot to a server.
239
+ #
240
+ # @param [Boolean] plugins Automatically register plugins from
241
+ # `@config.plugins.plugins`?
242
+ # @return [Boolean] Whether the connection was successful.
243
+ def start(plugins = true)
244
+ @last_connection_was_successful = false
245
+ @last_connection_error = nil
246
+
247
+ @reconnects = 0
248
+ @plugins.register_plugins(@config.plugins.plugins) if plugins
249
+
250
+ begin
251
+ @user_list.each do |user|
252
+ user.in_whois = false
253
+ user.unsync_all
254
+ end # reset state of all users
255
+
256
+ @channel_list.each(&:unsync_all) # reset state of all channels
257
+
258
+ @channels = [] # reset list of channels the bot is in
259
+
260
+ @join_handler&.unregister
261
+ @join_timer&.stop
262
+
263
+ join_lambda = -> { @config.channels.each { |channel| Channel(channel).join } }
264
+
265
+ if @config.delay_joins.is_a?(Symbol)
266
+ @join_handler = join_handler = on(@config.delay_joins) do
267
+ join_handler.unregister
268
+ join_lambda.call
269
+ end
270
+ else
271
+ @join_timer = Timer.new(self, interval: @config.delay_joins, shots: 1) do
272
+ join_lambda.call
273
+ end
274
+ end
275
+
276
+ @modes = []
277
+
278
+ @loggers.info "Connecting to #{@config.server}:#{@config.port}"
279
+ @irc = IRC.new(self)
280
+ @irc.start
281
+
282
+ if @config.reconnect && !@quitting
283
+ # double the delay for each unsuccesful reconnection attempt
284
+ if @last_connection_was_successful
285
+ @reconnects = 0
286
+ @last_connection_was_successful = false
287
+ else
288
+ @reconnects += 1
289
+ end
290
+
291
+ # Throttle reconnect attempts
292
+ wait = 2**@reconnects
293
+ wait = @config.max_reconnect_delay if wait > @config.max_reconnect_delay
294
+ @loggers.info "Waiting #{wait} seconds before reconnecting"
295
+ start_time = Time.now
296
+ while !@quitting && (Time.now - start_time) < wait
297
+ sleep 1
298
+ end
299
+ end
300
+ end while @config.reconnect && !@quitting
301
+
302
+ @last_connection_was_successful
303
+ end
304
+
305
+ # @endgroup
306
+ # @group Channel Control
307
+
308
+ # Join a channel.
309
+ #
310
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
311
+ # @param [String] key optionally the key of the channel
312
+ # @return [Channel] The joined channel
313
+ # @see Channel#join
314
+ def join(channel, key = nil)
315
+ channel = Channel(channel)
316
+ channel.join(key)
317
+
318
+ channel
319
+ end
320
+
321
+ # Part a channel.
322
+ #
323
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
324
+ # @param [String] reason an optional reason/part message
325
+ # @return [Channel] The channel that was left
326
+ # @see Channel#part
327
+ def part(channel, reason = nil)
328
+ channel = Channel(channel)
329
+ channel.part(reason)
330
+
331
+ channel
332
+ end
333
+
334
+ # @endgroup
335
+
336
+ # @return [Boolean] True if the bot reports ISUPPORT violations as
337
+ # exceptions.
338
+ def strict?
339
+ @config.strictness == :strict
340
+ end
341
+
342
+ # @yield
343
+ def initialize(&b)
344
+ @config = Configuration::Bot.new
345
+
346
+ @loggers = LoggerList.new
347
+ @loggers << Logger::FormattedLogger.new($stderr, level: @config.default_logger_level)
348
+ @handlers = HandlerList.new
349
+ @semaphores_mutex = Mutex.new
350
+ @semaphores = Hash.new { |h, k| h[k] = Mutex.new }
351
+ @callback = Callback.new(self)
352
+ @channels = []
353
+ @quitting = false
354
+ @quit_queue = Queue.new
355
+ @modes = []
356
+
357
+ @user_list = UserList.new(self)
358
+ @channel_list = ChannelList.new(self)
359
+ @plugins = PluginList.new(self)
360
+
361
+ @join_handler = nil
362
+ @join_timer = nil
363
+
364
+ @last_connection_was_successful = false
365
+ @last_connection_error = nil
366
+
367
+ super(nil, self)
368
+ instance_eval(&b) if block_given?
369
+ end
370
+
371
+ # @since 2.0.0
372
+ # @return [self]
373
+ # @api private
374
+ def bot
375
+ # This method is needed for the Helpers interface
376
+ self
377
+ end
378
+
379
+ # Sets a mode on the bot.
380
+ #
381
+ # @param [String] mode
382
+ # @return [void]
383
+ # @since 2.0.0
384
+ # @see Bot#modes
385
+ # @see Bot#unset_mode
386
+ def set_mode(mode)
387
+ @modes << mode unless @modes.include?(mode)
388
+ @irc.send "MODE #{nick} +#{mode}"
389
+ end
390
+
391
+ # Unsets a mode on the bot.
392
+ #
393
+ # @param [String] mode
394
+ # @return [void]
395
+ # @since 2.0.0
396
+ def unset_mode(mode)
397
+ @modes.delete(mode)
398
+ @irc.send "MODE #{nick} -#{mode}"
399
+ end
400
+
401
+ # @since 2.0.0
402
+ def modes=(modes)
403
+ (@modes - modes).each do |mode|
404
+ unset_mode(mode)
405
+ end
406
+
407
+ (modes - @modes).each do |mode|
408
+ set_mode(mode)
409
+ end
410
+ end
411
+
412
+ # Used for updating the bot's nick from within the IRC parser.
413
+ #
414
+ # @param [String] nick
415
+ # @api private
416
+ # @return [String]
417
+ def set_nick(nick)
418
+ @name = nick
419
+ end
420
+
421
+ # The bot's nickname.
422
+ # @overload nick=(new_nick)
423
+ # @raise [Exceptions::NickTooLong] Raised if the bot is
424
+ # operating in {#strict? strict mode} and the new nickname is
425
+ # too long
426
+ # @return [String]
427
+ # @overload nick
428
+ # @return [String]
429
+ # @return [String]
430
+ def nick
431
+ @name
432
+ end
433
+
434
+ def nick=(new_nick)
435
+ if new_nick.size > @irc.isupport['NICKLEN'] && strict?
436
+ raise Exceptions::NickTooLong, new_nick
437
+ end
438
+
439
+ @config.nick = new_nick
440
+ @irc.send "NICK #{new_nick}"
441
+ end
442
+
443
+ # Gain oper privileges.
444
+ #
445
+ # @param [String] password
446
+ # @param [String] user The username to use. Defaults to the bot's
447
+ # nickname
448
+ # @since 2.1.0
449
+ # @return [void]
450
+ def oper(password, user = nil)
451
+ user ||= nick
452
+ @irc.send "OPER #{user} #{password}"
453
+ end
454
+
455
+ # Try to create a free nick, first by cycling through all
456
+ # available alternatives and then by appending underscores.
457
+ #
458
+ # @param [String] base The base nick to start trying from
459
+ # @api private
460
+ # @return [String]
461
+ # @since 2.0.0
462
+ def generate_next_nick!(base = nil)
463
+ nicks = @config.nicks || []
464
+
465
+ if base
466
+ # if `base` is not in our list of nicks to try, assume that it's
467
+ # custom and just append an underscore
468
+ if !nicks.include?(base)
469
+ new_nick = base + '_'
470
+ else
471
+ # if we have a base, try the next nick or append an
472
+ # underscore if no more nicks are left
473
+ new_index = nicks.index(base) + 1
474
+ new_nick = nicks[new_index] || base + '_'
475
+ end
476
+ else
477
+ # if we have no base, try the first possible nick
478
+ new_nick = @config.nicks ? @config.nicks.first : @config.nick
479
+ end
480
+
481
+ @config.nick = new_nick
482
+ end
483
+
484
+ # @return [String]
485
+ def inspect
486
+ "#<Bot nick=#{@name.inspect}>"
487
+ end
488
+ end
489
+ end