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.
@@ -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