vetinari 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +69 -0
- data/examples/echo_bot.rb +24 -0
- data/lib/vetinari/bot.rb +358 -0
- data/lib/vetinari/callback.rb +48 -0
- data/lib/vetinari/callback_container.rb +85 -0
- data/lib/vetinari/channel.rb +249 -0
- data/lib/vetinari/channel_container.rb +74 -0
- data/lib/vetinari/configuration.rb +54 -0
- data/lib/vetinari/dcc/incoming/file.rb +113 -0
- data/lib/vetinari/dcc/server.rb +107 -0
- data/lib/vetinari/dcc/server_manager.rb +83 -0
- data/lib/vetinari/irc.rb +42 -0
- data/lib/vetinari/isupport.rb +175 -0
- data/lib/vetinari/logging/logger.rb +13 -0
- data/lib/vetinari/logging/logger_list.rb +19 -0
- data/lib/vetinari/logging/null_logger.rb +9 -0
- data/lib/vetinari/message_parser.rb +35 -0
- data/lib/vetinari/mode_parser.rb +46 -0
- data/lib/vetinari/user.rb +104 -0
- data/lib/vetinari/user_container.rb +52 -0
- data/lib/vetinari/version.rb +3 -0
- data/lib/vetinari.rb +38 -0
- data/spec/callback_spec.rb +19 -0
- data/spec/channel_management_spec.rb +39 -0
- data/spec/default_callbacks_spec.rb +53 -0
- data/spec/isupport_spec.rb +260 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/user_management_spec.rb +135 -0
- data/vetinari.gemspec +30 -0
- metadata +154 -0
data/lib/vetinari/irc.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Vetinari
|
2
|
+
module IRC
|
3
|
+
def register
|
4
|
+
raw "PASS #{@config.password}" if @config.password
|
5
|
+
raw "NICK #{@config.nick}"
|
6
|
+
raw "USER #{@config.username} * * :#{@config.real_name}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def rename(nick)
|
10
|
+
raw "NICK :#{nick}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def away(message = nil)
|
14
|
+
if message
|
15
|
+
raw "AWAY :#{message}"
|
16
|
+
else
|
17
|
+
raw "AWAY"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def back
|
22
|
+
away
|
23
|
+
end
|
24
|
+
|
25
|
+
def quit(message = nil)
|
26
|
+
@quitted = true
|
27
|
+
|
28
|
+
if message
|
29
|
+
raw "QUIT :#{message}"
|
30
|
+
else
|
31
|
+
raw 'QUIT'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def join(channel_name, key = nil)
|
36
|
+
unless @channels.has_channel?(channel_name)
|
37
|
+
channel = Channel.new(channel_name, @actor)
|
38
|
+
channel.join key
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Vetinari
|
2
|
+
class ISupport < Hash
|
3
|
+
def initialize
|
4
|
+
super
|
5
|
+
|
6
|
+
# Defaults:
|
7
|
+
self['CASEMAPPING'] = 'rfc1459'
|
8
|
+
self['CHANLIMIT'] = {}
|
9
|
+
self['CHANMODES'] = {
|
10
|
+
'A' => ['b'],
|
11
|
+
'B' => ['k'],
|
12
|
+
'C' => ['l'],
|
13
|
+
'D' => %w(i m n p s t r)
|
14
|
+
}
|
15
|
+
self['CHANNELLEN'] = 200
|
16
|
+
self['CHANTYPES'] = ['#', '&']
|
17
|
+
self['EXCEPTS'] = false
|
18
|
+
self['IDCHAN'] = {}
|
19
|
+
self['INVEX'] = false
|
20
|
+
self['KICKLEN'] = Float::INFINITY
|
21
|
+
self['MAXLIST'] = {}
|
22
|
+
self['MODES'] = 3
|
23
|
+
self['NETWORK'] = ''
|
24
|
+
self['NICKLEN'] = Float::INFINITY
|
25
|
+
self['PREFIX'] = {'o' => '@', 'v' => '+'}
|
26
|
+
self['SAFELIST'] = false
|
27
|
+
self['STATUSMSG'] = false
|
28
|
+
self['STD'] = false
|
29
|
+
self['TARGMAX'] = {}
|
30
|
+
self['TOPICLEN'] = Float::INFINITY
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(message)
|
34
|
+
patterns = message.split(' ')
|
35
|
+
patterns.delete_at(0)
|
36
|
+
patterns.each do |pattern|
|
37
|
+
return self if pattern.start_with?(':')
|
38
|
+
key = pattern.scan(/\w+/).first
|
39
|
+
method_name = "set_#{key.downcase}"
|
40
|
+
begin
|
41
|
+
if respond_to?(method_name, true)
|
42
|
+
send method_name, pattern
|
43
|
+
else
|
44
|
+
set_different key, pattern
|
45
|
+
end
|
46
|
+
rescue
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def set_casemapping(pattern)
|
54
|
+
self['CASEMAPPING'] = pattern.split('=')[1]
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_chanlimit(pattern)
|
58
|
+
value = pattern.split('=')[1]
|
59
|
+
value.split(',').each do |prefixes_and_limit|
|
60
|
+
prefixes, limit = prefixes_and_limit.split(':')
|
61
|
+
limit = limit.nil? ? Float::INFINITY : limit.to_i
|
62
|
+
prefixes.split('').each do |prefix|
|
63
|
+
self['CHANLIMIT'][prefix] = limit
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_chanmodes(pattern)
|
69
|
+
value = pattern.split('=')[1]
|
70
|
+
modes_per_type = value.split(',').map { |modes| modes.split('') }
|
71
|
+
('A'..'D').each_with_index do |type, index|
|
72
|
+
self['CHANMODES'][type] = modes_per_type[index]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_channellen(pattern)
|
77
|
+
self['CHANNELLEN'] = pattern.split('=')[1].to_i
|
78
|
+
end
|
79
|
+
|
80
|
+
def set_chantypes(pattern)
|
81
|
+
self['CHANTYPES'] = pattern.split('=')[1].split('')
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_excepts(pattern)
|
85
|
+
mode_char = pattern.split('=')[1]
|
86
|
+
self['EXCEPTS'] = mode_char.nil? ? true : mode_char
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_idchan(pattern)
|
90
|
+
value = pattern.split('=')[1]
|
91
|
+
value.split(',').each do |prefix_and_number|
|
92
|
+
prefix, number = prefix_and_number.split(':')
|
93
|
+
self['IDCHAN'][prefix] = number.to_i
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def set_invex(pattern)
|
98
|
+
mode_char = pattern.split('=')[1]
|
99
|
+
self['INVEX'] = mode_char.nil? ? true : mode_char
|
100
|
+
end
|
101
|
+
|
102
|
+
def set_kicklen(pattern)
|
103
|
+
self['KICKLEN'] = pattern.split('=')[1].to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
def set_maxlist(pattern)
|
107
|
+
value = pattern.split('=')[1]
|
108
|
+
value.split(',').each do |prefixes_and_maximum|
|
109
|
+
prefixes, maximum = prefixes_and_maximum.split(':')
|
110
|
+
prefixes.split('').each do |prefix|
|
111
|
+
self['MAXLIST'][prefix] = maximum.to_i
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_modes(pattern)
|
117
|
+
mode_char = pattern.split('=')[1]
|
118
|
+
self['MODES'] = mode_char.nil? ? Float::INFINITY : mode_char.to_i
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_network(pattern)
|
122
|
+
self['NETWORK'] = pattern.split('=')[1]
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_nicklen(pattern)
|
126
|
+
self['NICKLEN'] = pattern.split('=')[1].to_i
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_prefix(pattern)
|
130
|
+
modes, prefixes = pattern.scan(/\((.+)\)(.+)/).flatten
|
131
|
+
modes = modes.split('')
|
132
|
+
prefixes = prefixes.split('')
|
133
|
+
modes.zip(prefixes).each do |pair|
|
134
|
+
self['PREFIX'][pair.first] = pair.last
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def set_safelist(pattern)
|
139
|
+
self['SAFELIST'] = true
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_statusmsg(pattern)
|
143
|
+
self['STATUSMSG'] = pattern.split('=')[1].split('')
|
144
|
+
end
|
145
|
+
|
146
|
+
def set_std(pattern)
|
147
|
+
self['STD'] = pattern.split('=')[1].split(',')
|
148
|
+
end
|
149
|
+
|
150
|
+
def set_targmax(pattern)
|
151
|
+
targets = pattern.split('=')[1].split(',')
|
152
|
+
targets.each do |target_with_maximum|
|
153
|
+
target, maximum = target_with_maximum.split(':')
|
154
|
+
maximum = maximum.nil? ? Float::INFINITY : maximum.to_i
|
155
|
+
self['TARGMAX'][target] = maximum
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def set_topiclen(pattern)
|
160
|
+
self['TOPICLEN'] = pattern.split('=')[1].to_i
|
161
|
+
end
|
162
|
+
|
163
|
+
def set_different(key, pattern)
|
164
|
+
if pattern.include? '='
|
165
|
+
if pattern.include? ','
|
166
|
+
self[key] = pattern.split('=')[1].split(',')
|
167
|
+
else
|
168
|
+
self[key] = pattern.split('=')[1]
|
169
|
+
end
|
170
|
+
else
|
171
|
+
self[key] = true
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Vetinari
|
2
|
+
module Logging
|
3
|
+
class Logger < ::Logger
|
4
|
+
def initialize(*args)
|
5
|
+
super(*args)
|
6
|
+
|
7
|
+
self.formatter = proc do |severity, datetime, progname, msg|
|
8
|
+
"#{severity} #{datetime.strftime('%Y-%m-%d %H:%M:%S')} #{msg}\n"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Vetinari
|
2
|
+
module Logging
|
3
|
+
class LoggerList < Array
|
4
|
+
%w(debug info warn error fatal unknown).each do |method_name|
|
5
|
+
define_method(method_name) do |*args, &block|
|
6
|
+
each do |logger|
|
7
|
+
logger.send(method_name, *args, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(method_name, *args, &block)
|
13
|
+
each do |logger|
|
14
|
+
logger.send(method_name, *args, &block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Vetinari
|
2
|
+
module MessageParser
|
3
|
+
def parse(message, chantypes)
|
4
|
+
chantypes = chantypes.map { |c| Regexp.escape(c) }.join('|')
|
5
|
+
|
6
|
+
case message
|
7
|
+
# :kornbluth.freenode.net 001 Vetinari5824 :Welcome to the freenode Internet Relay Chat Network Vetinari5824
|
8
|
+
when /^(?:\:\S+ )?(\d\d\d) /
|
9
|
+
number = $1.to_i
|
10
|
+
{:type => number, :params => $'}
|
11
|
+
when /^:(\S+)!(\S+)@(\S+) PRIVMSG ((?:#{chantypes})\S+) :/
|
12
|
+
{:type => :channel, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'}
|
13
|
+
when /^:(\S+)!(\S+)@(\S+) PRIVMSG \S+ :/
|
14
|
+
{:type => :query, :nick => $1, :user => $2, :host => $3, :message => $'}
|
15
|
+
when /^:(\S+)!(\S+)@(\S+) JOIN :*(\S+)$/
|
16
|
+
{:type => :join, :nick => $1, :user => $2, :host => $3, :channel => $4}
|
17
|
+
when /^:(\S+)!(\S+)@(\S+) PART (\S+)/
|
18
|
+
{:type => :part, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'.sub(/ :/, '')}
|
19
|
+
when /^:(\S+)!(\S+)@(\S+) QUIT/
|
20
|
+
{:type => :quit, :nick => $1, :user => $2, :host => $3, :message => $'.sub(/ :/, '')}
|
21
|
+
when /^:(\S+)!(\S+)@(\S+) MODE ((?:#{chantypes})\S+) ([+-]\S+)/
|
22
|
+
{:type => :channel_mode, :nick => $1, :user => $2, :host => $3, :channel => $4, :modes => $5, :params => $'.lstrip}
|
23
|
+
when /^:(\S+)!(\S+)@(\S+) NICK :/
|
24
|
+
{:type => :nickchange, :nick => $1, :user => $2, :host => $3, :new_nick => $'}
|
25
|
+
when /^:(\S+)!(\S+)@(\S+) KICK (\S+) (\S+) :/
|
26
|
+
{:type => :kick, :nick => $1, :user => $2, :host => $3, :channel => $4, :kickee => $5, :message => $'}
|
27
|
+
when /^:(\S+)!(\S+)@(\S+) TOPIC (\S+) :/
|
28
|
+
{:type => :topic, :nick => $1, :user => $2, :host => $3, :channel => $4, :topic => $'}
|
29
|
+
else
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
module_function :parse
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Vetinari
|
2
|
+
module ModeParser
|
3
|
+
def parse(modes, params, isupport)
|
4
|
+
modes = modes.split(//)
|
5
|
+
direction = modes.shift.to_sym
|
6
|
+
unless [:'+', :'-'].include?(direction)
|
7
|
+
raise(ArgumentError, "Direction for modes argument not given. +/- needed, got: #{direction}.")
|
8
|
+
end
|
9
|
+
params = params.split(/ /) if params.is_a?(String)
|
10
|
+
mode_changes = []
|
11
|
+
|
12
|
+
modes.each do |mode|
|
13
|
+
if needs_a_param?(mode, direction, isupport)
|
14
|
+
param = params.shift
|
15
|
+
|
16
|
+
if param
|
17
|
+
mode_change = {
|
18
|
+
:direction => direction,
|
19
|
+
:mode => mode,
|
20
|
+
:param => param
|
21
|
+
}
|
22
|
+
mode_changes << mode_change
|
23
|
+
end
|
24
|
+
else
|
25
|
+
mode_change = {
|
26
|
+
:direction => direction,
|
27
|
+
:mode => mode
|
28
|
+
}
|
29
|
+
mode_changes << mode_change
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
mode_changes
|
34
|
+
end
|
35
|
+
module_function :parse
|
36
|
+
|
37
|
+
def needs_a_param?(mode, direction, isupport)
|
38
|
+
modes = isupport['CHANMODES']['A'] +
|
39
|
+
isupport['CHANMODES']['B'] +
|
40
|
+
isupport['PREFIX'].keys
|
41
|
+
modes.concat(isupport['CHANMODES']['C']) if direction == :'+'
|
42
|
+
modes.include?(mode)
|
43
|
+
end
|
44
|
+
module_function :needs_a_param?
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Vetinari
|
2
|
+
# TODO: Actor?
|
3
|
+
class User
|
4
|
+
attr_reader :nick, :user, :host, :online, :observed
|
5
|
+
|
6
|
+
def initialize(nick, bot)
|
7
|
+
@bot = bot
|
8
|
+
|
9
|
+
@nick = nick
|
10
|
+
@user = nil
|
11
|
+
@host = nil
|
12
|
+
|
13
|
+
@online = nil
|
14
|
+
@observed = false
|
15
|
+
|
16
|
+
@channels = Set.new
|
17
|
+
@channels_with_modes = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Updates the properties of an user.
|
21
|
+
# def whois
|
22
|
+
# connected do
|
23
|
+
# fiber = Fiber.current
|
24
|
+
# callbacks = {}
|
25
|
+
|
26
|
+
# # User is online.
|
27
|
+
# callbacks[311] = @thaum.on(311) do |event_data|
|
28
|
+
# nick = event_data[:params].split(' ')[1]
|
29
|
+
# if nick.downcase == @nick.downcase
|
30
|
+
# @online = true
|
31
|
+
# # TODO: Add properties.
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
|
35
|
+
# # User is not online.
|
36
|
+
# callbacks[401] = @thaum.on(401) do |event_data|
|
37
|
+
# nick = event_data[:params].split(' ')[1]
|
38
|
+
# if nick.downcase == @nick.downcase
|
39
|
+
# @online = false
|
40
|
+
# fiber.resume
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
|
44
|
+
# # End of WHOIS.
|
45
|
+
# callbacks[318] = @thaum.on(318) do |event_data|
|
46
|
+
# nick = event_data[:params].split(' ')[1]
|
47
|
+
# if nick.downcase == @nick.downcase
|
48
|
+
# fiber.resume
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
|
52
|
+
# raw "WHOIS #{@nick}"
|
53
|
+
# Fiber.yield
|
54
|
+
|
55
|
+
# callbacks.each do |type, callback|
|
56
|
+
# @thaum.callbacks[type].delete(callback)
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
|
60
|
+
# self
|
61
|
+
# end
|
62
|
+
|
63
|
+
def online?
|
64
|
+
if @bot.users[@nick]
|
65
|
+
@online = true
|
66
|
+
else
|
67
|
+
# TODO
|
68
|
+
# whois if @online.nil?
|
69
|
+
# @online
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def bot?
|
74
|
+
self == @bot.user
|
75
|
+
end
|
76
|
+
|
77
|
+
def dcc_send(filepath, filename = nil)
|
78
|
+
if @bot.server_manager
|
79
|
+
if File.exist?(filepath)
|
80
|
+
filename = File.basename(filepath) unless filename
|
81
|
+
@bot.server_manager.add_offering(self, filepath, filename)
|
82
|
+
else
|
83
|
+
raise "File '#{filepath}' does not exist."
|
84
|
+
end
|
85
|
+
else
|
86
|
+
raise 'DCC not available: Missing external IP or ports'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# TODO: ping, version, time methods?
|
91
|
+
|
92
|
+
def message(message)
|
93
|
+
@bot.raw "PRIVMSG #{@nick} :#{message}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def notice(message)
|
97
|
+
@bot.raw "NOTICE #{@nick} :#{message}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def inspect
|
101
|
+
"#<User nick=#{@nick}>"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Vetinari
|
2
|
+
# The UserList class holds information about users a Thaum is able to see
|
3
|
+
# in channels.
|
4
|
+
class UserContainer
|
5
|
+
include Celluloid
|
6
|
+
|
7
|
+
attr_reader :users
|
8
|
+
|
9
|
+
exclusive
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@users = Set.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(user)
|
16
|
+
@users << user
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO
|
20
|
+
def remove(user)
|
21
|
+
@users.delete(user)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Find a User given the nick.
|
25
|
+
def [](user_or_nick)
|
26
|
+
case user_or_nick
|
27
|
+
when User
|
28
|
+
user_or_nick if @users.include?(user_or_nick)
|
29
|
+
when String
|
30
|
+
@users.find do |u|
|
31
|
+
u.nick.downcase == user_or_nick.downcase
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_user?(user)
|
37
|
+
self[user] ? true : false
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
@users.clear
|
42
|
+
end
|
43
|
+
|
44
|
+
# Removes all users from the UserContainer that don't share channels with
|
45
|
+
# the Bot.
|
46
|
+
def kill_zombie_users(users)
|
47
|
+
(@users - users).each do |user|
|
48
|
+
@users.delete(user) unless user.bot?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/vetinari.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
require 'logger'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
require 'celluloid/autostart'
|
7
|
+
require 'celluloid/io'
|
8
|
+
|
9
|
+
module Vetinari
|
10
|
+
require 'vetinari/irc'
|
11
|
+
require 'vetinari/bot'
|
12
|
+
require 'vetinari/callback'
|
13
|
+
require 'vetinari/callback_container'
|
14
|
+
require 'vetinari/channel'
|
15
|
+
require 'vetinari/channel_container'
|
16
|
+
require 'vetinari/configuration'
|
17
|
+
require 'vetinari/isupport'
|
18
|
+
require 'vetinari/message_parser'
|
19
|
+
require 'vetinari/mode_parser'
|
20
|
+
require 'vetinari/user'
|
21
|
+
require 'vetinari/user_container'
|
22
|
+
require 'vetinari/version'
|
23
|
+
|
24
|
+
module Dcc
|
25
|
+
require 'vetinari/dcc/server_manager'
|
26
|
+
require 'vetinari/dcc/server'
|
27
|
+
|
28
|
+
module Incoming
|
29
|
+
require 'vetinari/dcc/incoming/file'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Logging
|
34
|
+
require 'vetinari/logging/logger'
|
35
|
+
require 'vetinari/logging/logger_list'
|
36
|
+
require 'vetinari/logging/null_logger'
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Callback' do
|
4
|
+
subject { Vetinari::Bot.new { |c| c.verbose = false } }
|
5
|
+
let(:callbacks) { subject.callbacks.instance_variable_get('@callbacks') }
|
6
|
+
|
7
|
+
it 'is added correctly' do
|
8
|
+
expect(callbacks[:channel]).to have(1).callback
|
9
|
+
subject.on(:channel)
|
10
|
+
expect(callbacks[:channel]).to have(2).callbacks
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can be removed' do
|
14
|
+
cb = subject.on(:channel)
|
15
|
+
expect(callbacks[:channel]).to have(2).callback
|
16
|
+
cb.remove
|
17
|
+
expect(callbacks[:channel]).to have(1).callback
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Channel Management' do
|
4
|
+
subject { Vetinari::Bot.new { |c| c.verbose = false } }
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
subject.parse(':server 001 Vetinari :Welcome message')
|
8
|
+
subject.parse(':server 376 Vetinari :End of /MOTD command.')
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'adds a channel to the channel_list when joining a channel' do
|
12
|
+
expect(subject.channels.channels).to be_empty
|
13
|
+
subject.parse(':Vetinari!foo@bar JOIN #mended_drum')
|
14
|
+
expect(subject.channels.channels).to have(1).channel
|
15
|
+
expect(subject.channels.channels.first.name).to eq('#mended_drum')
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'removes a channel from the channel_list when leaving a channel' do
|
19
|
+
subject.parse(':Vetinari!foo@bar JOIN #mended_drum')
|
20
|
+
expect(subject.channels.channels).to have(1).channel
|
21
|
+
subject.parse(':Vetinari!foo@bar PART #mended_drum')
|
22
|
+
expect(subject.channels.channels).to be_empty
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'removes a channel from the channel_list when being kicked from a channel' do
|
26
|
+
subject.parse(':Vetinari!foo@bar JOIN #mended_drum')
|
27
|
+
expect(subject.channels.channels).to have(1).channel
|
28
|
+
subject.parse(':TheLibrarian!foo@bar KICK #mended_drum Vetinari :No humans allowed!')
|
29
|
+
expect(subject.channels.channels).to be_empty
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'removes all channels from the channel_list when quitting' do
|
33
|
+
subject.parse(':Vetinari!foo@bar JOIN #mended_drum')
|
34
|
+
subject.parse(':Vetinari!foo@bar JOIN #library')
|
35
|
+
expect(subject.channels.channels).to have(2).channels
|
36
|
+
subject.parse(':Vetinari!foo@bar QUIT :Bye mates!')
|
37
|
+
expect(subject.channels.channels).to be_empty
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Vetinari::Bot do
|
4
|
+
subject { Vetinari::Bot.new { |c| c.verbose = false } }
|
5
|
+
let(:bare) { subject.bare_object }
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
subject.parse(':server 001 Vetinari :Welcome message')
|
9
|
+
subject.parse(':server 376 Vetinari :End of /MOTD command.')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'PING PONGs (server)' do
|
13
|
+
bare.should_receive(:raw).with("PONG :server", false)
|
14
|
+
subject.parse("PING :server")
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'PING PONGs (user)' do
|
18
|
+
time = Time.now.to_i
|
19
|
+
bare.should_receive(:raw).with("NOTICE nick :\001PING #{time}\001")
|
20
|
+
subject.parse(":nick!user@host PRIVMSG Vetinari :\001PING #{time}\001")
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'responses to VERSION request' do
|
24
|
+
bare.should_receive(:raw).with("NOTICE nick :\001VERSION Vetinari #{Vetinari::VERSION} (https://github.com/tbuehlmann/vetinari)")
|
25
|
+
subject.parse(":nick!user@host PRIVMSG Vetinari :\001VERSION\001")
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'responses to TIME request' do
|
29
|
+
bare.should_receive(:raw).with("NOTICE nick :\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001")
|
30
|
+
subject.parse(":nick!user@host PRIVMSG Vetinari :\001TIME\001")
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'rejoin channel after kick' do
|
34
|
+
let(:channel) { subject.channels['#mended_drum'] }
|
35
|
+
|
36
|
+
before(:each) do
|
37
|
+
subject.config.rejoin_after_kick = true
|
38
|
+
subject.parse(':Vetinari!foo@bar JOIN #mended_drum')
|
39
|
+
subject.parse(':TheLibrarian!foo@bar JOIN #mended_drum')
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'without a channel key' do
|
43
|
+
channel.should_receive(:join)
|
44
|
+
subject.parse(':TheLibrarian!foo@bar KICK #mended_drum Vetinari :foo')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'with a channel key' do
|
48
|
+
channel.should_receive(:join).with('thaum')
|
49
|
+
subject.parse(':Vetinari!foo@bar MODE #mended_drum +k thaum')
|
50
|
+
subject.parse(':TheLibrarian!foo@bar KICK #mended_drum Vetinari :foo')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|