vetinari 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,9 @@
1
+ module Vetinari
2
+ module Logging
3
+ class NullLogger
4
+ %w(debug info warn error fatal unknown method_missing).each do |method_name|
5
+ define_method(method_name) { |*args, &block| }
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,3 @@
1
+ module Vetinari
2
+ VERSION = Gem::Version.new('0.0.1')
3
+ 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