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.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/LICENSE +22 -0
- data/README.md +180 -0
- data/docs/bot_options.md +454 -0
- data/docs/changes.md +541 -0
- data/docs/common_mistakes.md +60 -0
- data/docs/common_tasks.md +57 -0
- data/docs/encodings.md +69 -0
- data/docs/events.md +273 -0
- data/docs/getting_started.md +184 -0
- data/docs/logging.md +90 -0
- data/docs/migrating.md +267 -0
- data/docs/plugins.md +4 -0
- data/docs/readme.md +20 -0
- data/examples/basic/autovoice.rb +32 -0
- data/examples/basic/google.rb +35 -0
- data/examples/basic/hello.rb +15 -0
- data/examples/basic/join_part.rb +34 -0
- data/examples/basic/memo.rb +39 -0
- data/examples/basic/msg.rb +16 -0
- data/examples/basic/seen.rb +36 -0
- data/examples/basic/urban_dict.rb +35 -0
- data/examples/basic/url_shorten.rb +35 -0
- data/examples/plugins/autovoice.rb +37 -0
- data/examples/plugins/custom_prefix.rb +23 -0
- data/examples/plugins/dice_roll.rb +38 -0
- data/examples/plugins/google.rb +36 -0
- data/examples/plugins/hello.rb +22 -0
- data/examples/plugins/hooks.rb +36 -0
- data/examples/plugins/join_part.rb +42 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/msg.rb +22 -0
- data/examples/plugins/multiple_matches.rb +32 -0
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/seen.rb +45 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +33 -0
- data/lib/cinch.rb +5 -0
- data/lib/cinch/ban.rb +50 -0
- data/lib/cinch/bot.rb +479 -0
- data/lib/cinch/cached_list.rb +19 -0
- data/lib/cinch/callback.rb +20 -0
- data/lib/cinch/channel.rb +463 -0
- data/lib/cinch/channel_list.rb +29 -0
- data/lib/cinch/configuration.rb +73 -0
- data/lib/cinch/configuration/bot.rb +48 -0
- data/lib/cinch/configuration/dcc.rb +16 -0
- data/lib/cinch/configuration/plugins.rb +41 -0
- data/lib/cinch/configuration/sasl.rb +19 -0
- data/lib/cinch/configuration/ssl.rb +19 -0
- data/lib/cinch/configuration/timeouts.rb +14 -0
- data/lib/cinch/constants.rb +533 -0
- data/lib/cinch/dcc.rb +12 -0
- data/lib/cinch/dcc/dccable_object.rb +37 -0
- data/lib/cinch/dcc/incoming.rb +1 -0
- data/lib/cinch/dcc/incoming/send.rb +147 -0
- data/lib/cinch/dcc/outgoing.rb +1 -0
- data/lib/cinch/dcc/outgoing/send.rb +122 -0
- data/lib/cinch/exceptions.rb +46 -0
- data/lib/cinch/formatting.rb +125 -0
- data/lib/cinch/handler.rb +118 -0
- data/lib/cinch/handler_list.rb +90 -0
- data/lib/cinch/helpers.rb +231 -0
- data/lib/cinch/irc.rb +924 -0
- data/lib/cinch/isupport.rb +98 -0
- data/lib/cinch/log_filter.rb +21 -0
- data/lib/cinch/logger.rb +168 -0
- data/lib/cinch/logger/formatted_logger.rb +97 -0
- data/lib/cinch/logger/zcbot_logger.rb +22 -0
- data/lib/cinch/logger_list.rb +85 -0
- data/lib/cinch/mask.rb +69 -0
- data/lib/cinch/message.rb +392 -0
- data/lib/cinch/message_queue.rb +107 -0
- data/lib/cinch/mode_parser.rb +76 -0
- data/lib/cinch/network.rb +104 -0
- data/lib/cinch/open_ended_queue.rb +26 -0
- data/lib/cinch/pattern.rb +65 -0
- data/lib/cinch/plugin.rb +515 -0
- data/lib/cinch/plugin_list.rb +38 -0
- data/lib/cinch/rubyext/float.rb +3 -0
- data/lib/cinch/rubyext/module.rb +26 -0
- data/lib/cinch/rubyext/string.rb +33 -0
- data/lib/cinch/sasl.rb +34 -0
- data/lib/cinch/sasl/dh_blowfish.rb +71 -0
- data/lib/cinch/sasl/diffie_hellman.rb +47 -0
- data/lib/cinch/sasl/mechanism.rb +6 -0
- data/lib/cinch/sasl/plain.rb +26 -0
- data/lib/cinch/syncable.rb +83 -0
- data/lib/cinch/target.rb +199 -0
- data/lib/cinch/timer.rb +145 -0
- data/lib/cinch/user.rb +488 -0
- data/lib/cinch/user_list.rb +87 -0
- data/lib/cinch/utilities/deprecation.rb +16 -0
- data/lib/cinch/utilities/encoding.rb +37 -0
- data/lib/cinch/utilities/kernel.rb +13 -0
- data/lib/cinch/version.rb +4 -0
- 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
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 & 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
|