vetinari 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,249 @@
1
+ module Vetinari
2
+ # Data structure which is used for storing users:
3
+ # {lower_cased_nick => {User => [modes]}}
4
+ #
5
+ # Example:
6
+ # {'ponder' => {:user => #<User nick="Ponder">, :modes => ['v', 'o']}}
7
+ #
8
+ # TODO: Actor?
9
+ class Channel
10
+ attr_reader :name, :users, :users_with_modes, :modes, :lists
11
+
12
+ def initialize(name, bot)
13
+ @name = name
14
+ @bot = bot
15
+ @users = Set.new
16
+ @users_with_modes = {}
17
+ @modes = {}
18
+ @lists = Hash.new { |hash, key| hash[key] = [] }
19
+ @mutex = Mutex.new
20
+ end
21
+
22
+ # Experimental, no tests so far.
23
+ # def topic
24
+ # if @topic
25
+ # @topic
26
+ # else
27
+ # connected do
28
+ # fiber = Fiber.current
29
+ # callbacks = {}
30
+ # [331, 332, 403, 442].each do |numeric|
31
+ # callbacks[numeric] = @thaum.on(numeric) do |event_data|
32
+ # topic = event_data[:params].match(':(.*)').captures.first
33
+ # fiber.resume topic
34
+ # end
35
+ # end
36
+
37
+ # @bot.raw "TOPIC #{@name}"
38
+ # @topic = Fiber.yield
39
+ # callbacks.each do |type, callback|
40
+ # @thaum.callbacks[type].delete(callback)
41
+ # end
42
+
43
+ # @topic
44
+ # end
45
+ # end
46
+ # end
47
+
48
+ def topic=(topic)
49
+ @bot.raw "TOPIC #{@name} :#{topic}"
50
+ end
51
+
52
+ def ban(hostmask)
53
+ mode '+b', hostmask
54
+ end
55
+
56
+ def unban(hostmask)
57
+ mode '-b', hostmask
58
+ end
59
+
60
+ def lock(key)
61
+ @bot.raw "MODE #{@name} +k #{key}"
62
+ end
63
+
64
+ def unlock
65
+ key = @modes['k']
66
+ @bot.raw "MODE #{@name} -k #{key}" if key
67
+ end
68
+
69
+ def kick(user, reason = nil)
70
+ if reason
71
+ @bot.raw "KICK #{@name} #{user.nick} :#{reason}"
72
+ else
73
+ @bot.raw "KICK #{@name} #{user.nick}"
74
+ end
75
+ end
76
+
77
+ def invite(user)
78
+ @bot.raw "INVITE #{@name} #{user.nick}"
79
+ end
80
+
81
+ def op(user)
82
+ mode '+o', user.nick
83
+ end
84
+
85
+ def deop(user)
86
+ mode '-o', user.nick
87
+ end
88
+
89
+ def voice(user_or_nick)
90
+ mode '+v', user.nick
91
+ end
92
+
93
+ def devoice(user_or_nick)
94
+ mode '-v', user.nick
95
+ end
96
+
97
+ def join(key = nil)
98
+ if key
99
+ @bot.raw "JOIN #{@name} #{key}"
100
+ else
101
+ @bot.raw "JOIN #{@name}"
102
+ end
103
+ end
104
+
105
+ def part(message = nil)
106
+ if message
107
+ @bot.raw "PART #{@name} :#{message}"
108
+ else
109
+ @bot.raw "PART #{@name}"
110
+ end
111
+ end
112
+
113
+ def hop(message = nil)
114
+ key = @modes['k']
115
+ part message
116
+ join key
117
+ end
118
+
119
+ def add_user(user, modes = [])
120
+ @mutex.synchronize do
121
+ @users << user
122
+ @users_with_modes[user] = modes
123
+ end
124
+ end
125
+
126
+ def remove_user(user)
127
+ @mutex.synchronize do
128
+ @users_with_modes.delete(user)
129
+ @users.delete?(user) ? user : nil
130
+ end
131
+ end
132
+
133
+ def has_user?(user)
134
+ @users.include?(user)
135
+ end
136
+
137
+ # def find_user(user)
138
+ # case user_or_nick
139
+ # when String
140
+ # @users.find { |u| u.nick.downcase == user_or_nick.downcase }
141
+ # when User
142
+ # has_user?(user_or_nick) ? user_or_nick : nil
143
+ # end
144
+ # end
145
+
146
+ # def find_user_with_modes(user_or_nick)
147
+ # user = case user_or_nick
148
+ # when String
149
+ # @users.find { |u| u.nick.downcase == user_or_nick.downcase }
150
+ # when User
151
+ # has_user?(user_or_nick) ? user_or_nick : nil
152
+ # end
153
+
154
+ # {:user => user, :modes => @users_with_modes[user]} if user
155
+ # end
156
+
157
+ def modes_of(user)
158
+ @users_with_modes[user] if has_user?(user)
159
+ end
160
+
161
+ # TODO
162
+ def set_mode(mode, isupport)
163
+ if isupport['PREFIX'].keys.include?(mode[:mode])
164
+ user = find_user(mode[:param])
165
+ if user
166
+ case mode[:direction]
167
+ when :'+'
168
+ @users_with_modes[user] << mode[:mode]
169
+ when :'-'
170
+ @users_with_modes[user].delete(mode[:mode])
171
+ end
172
+ end
173
+ elsif isupport['CHANMODES']['A'].include?(mode[:mode])
174
+ case mode[:direction]
175
+ when :'+'
176
+ add_to_list(mode[:mode], mode[:param])
177
+ when :'-'
178
+ remove_from_list(mode[:mode], mode[:param])
179
+ end
180
+ elsif isupport['CHANMODES']['B'].include?(mode[:mode])
181
+ case mode[:direction]
182
+ when :'+'
183
+ set_channel_mode(mode[:mode], mode[:param])
184
+ when :'-'
185
+ unset_channel_mode(mode[:mode])
186
+ end
187
+ elsif isupport['CHANMODES']['C'].include?(mode[:mode])
188
+ case mode[:direction]
189
+ when :'+'
190
+ set_channel_mode(mode[:mode], mode[:param])
191
+ when :'-'
192
+ unset_channel_mode(mode[:mode])
193
+ end
194
+ elsif isupport['CHANMODES']['D'].include?(mode[:mode])
195
+ case mode[:direction]
196
+ when :'+'
197
+ set_channel_mode(mode[:mode], true)
198
+ when :'-'
199
+ unset_channel_mode(mode[:mode])
200
+ end
201
+ end
202
+ end
203
+
204
+ def mode(modes, params = nil)
205
+ if params
206
+ @bot.raw "MODE #{@name} #{modes} #{params}"
207
+ else
208
+ @bot.raw "MODE #{@name} #{modes}"
209
+ end
210
+ end
211
+
212
+ def get_mode
213
+ @bot.raw "MODE #{@name}"
214
+ end
215
+
216
+ def message(message)
217
+ @bot.raw "PRIVMSG #{@name} :#{message}"
218
+ end
219
+
220
+ def inspect
221
+ "#<Channel name=#{@name.inspect}>"
222
+ end
223
+
224
+ private
225
+
226
+ def set_channel_mode(mode, param)
227
+ @modes[mode] = param
228
+ end
229
+
230
+ def unset_channel_mode(mode)
231
+ @modes.delete(mode)
232
+ end
233
+
234
+ def add_to_list(list, param)
235
+ @mutex.synchronize do
236
+ @lists[list] ||= []
237
+ @lists[list] << param
238
+ end
239
+ end
240
+
241
+ def remove_from_list(list, param)
242
+ @mutex.synchronize do
243
+ if @lists[list].include?(param)
244
+ @lists[list].delete(param)
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,74 @@
1
+ module Vetinari
2
+ class ChannelContainer
3
+ include Celluloid
4
+
5
+ attr_reader :channels
6
+
7
+ exclusive
8
+
9
+ def initialize
10
+ @channels = Set.new
11
+ end
12
+
13
+ def add(channel)
14
+ @channels << channel
15
+ end
16
+
17
+ def [](channel_or_channel_name)
18
+ case channel_or_channel_name
19
+ when Channel
20
+ if @channels.include?(channel_or_channel_name)
21
+ channel_or_channel_name
22
+ end
23
+ when String
24
+ @channels.find do |c|
25
+ c.name.downcase == channel_or_channel_name.downcase
26
+ end
27
+ end
28
+ end
29
+
30
+ def has_channel?(channel)
31
+ self[channel] ? true : false
32
+ end
33
+
34
+ def remove(channel)
35
+ if has_channel?(channel)
36
+ @channels.delete(channel)
37
+ end
38
+ end
39
+
40
+ # Removes a User from all Channels from the ChannelList.
41
+ # Returning a Set of Channels in which the User was.
42
+ def remove_user(user)
43
+ channels = Set.new
44
+
45
+ @channels.each do |channel|
46
+ if channel.remove_user(user)
47
+ channels << channel
48
+ end
49
+ end
50
+
51
+ channels
52
+ end
53
+
54
+ # Removes all Channels from the ChannelList and returns them.
55
+ def clear
56
+ channels = @channels.dup
57
+ @channels.clear
58
+ channels
59
+ end
60
+
61
+ # Returns a Set of all Users that are in one of the Channels from the
62
+ # ChannelList.
63
+ # TODO: Refactor
64
+ def users
65
+ users = Set.new
66
+
67
+ @channels.each do |channel|
68
+ users.merge(channel.users)
69
+ end
70
+
71
+ users
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,54 @@
1
+ module Vetinari
2
+ class Configuration < OpenStruct
3
+ def initialize(&block)
4
+ super
5
+ self.server = 'chat.freenode.org'
6
+ self.port = 6667
7
+ self.ssl = false
8
+ self.nick = "Vetinari#{rand(10_000)}"
9
+ self.username = 'Vetinari'
10
+ self.real_name = 'Havelock Vetinari'
11
+ self.verbose = true
12
+ self.logging = false
13
+ self.logger = nil
14
+ self.reconnect = true
15
+ self.reconnect_interval = 10
16
+ self.hide_ping_pongs = true
17
+ self.rejoin_after_kick = false
18
+ self.password = nil
19
+
20
+ self.isupport = ISupport.new
21
+ self.dcc = OpenStruct.new
22
+ self.dcc.ports = []
23
+
24
+ block.call(self) if block_given?
25
+ setup_loggers
26
+ end
27
+
28
+ private
29
+
30
+ def setup_loggers
31
+ self.console_logger = if self.verbose
32
+ Logging::Logger.new($stdout)
33
+ else
34
+ Logging::NullLogger.new
35
+ end
36
+
37
+ self.logger = if self.logging
38
+ if self.logger
39
+ self.logger
40
+ else
41
+ log_path = File.join($0, 'logs', 'log.log')
42
+ log_dir = File.dirname(log_path)
43
+ FileUtils.mkdir_p(log_dir) unless File.exist?(log_dir)
44
+ Logging::Logger.new(log_path, File::WRONLY | File::APPEND)
45
+ end
46
+ else
47
+ Logging::NullLogger.new
48
+ end
49
+
50
+ self.loggers = Logging::LoggerList.new
51
+ self.loggers.push(self.console_logger, self.logger)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,113 @@
1
+ module Vetinari
2
+ module Dcc
3
+ module Incoming
4
+ class File
5
+ include Celluloid::IO, Celluloid::Notifications
6
+
7
+ attr_reader :user, :filename, :ip, :port, :filesize, :state
8
+
9
+ def initialize(user, filename, ip, port, filesize, bot)
10
+ @user = user
11
+ @filename = filename
12
+ @ip = ip
13
+ @port = port
14
+ @filesize = filesize
15
+ @bot = bot
16
+ @mutex = Mutex.new
17
+
18
+ self.state = :pending
19
+ end
20
+
21
+ def accept(directory = '~/Downloads/', resume = false)
22
+ if @state == :pending
23
+ self.state = :accepted
24
+ directory = ::File.expand_path(directory)
25
+ @filepath = ::File.join(directory, @filename)
26
+
27
+ if resume && resumable?
28
+ resume_transfer
29
+ else
30
+ download
31
+ end
32
+ end
33
+ end
34
+
35
+ def inspect
36
+ "#<File filename=#{@filename} filesize=#{@filesize} user=#{@user.inspect}"
37
+ end
38
+
39
+ def resume_accepted(position)
40
+ self.state = :resume_accepted
41
+ download(position)
42
+ end
43
+
44
+ def state=(state)
45
+ old_state = @state
46
+ @state = state
47
+ publish('vetinari.dcc.incoming.file', Actor.current, old_state, @state)
48
+ end
49
+
50
+ private
51
+
52
+ def download(position = 0)
53
+ self.state = :connecting
54
+
55
+ begin
56
+ @socket = TCPSocket.new(@ip.to_s, @port)
57
+ self.state = :connected
58
+ rescue Errno::ECONNREFUSED
59
+ self.state = :failed
60
+ return
61
+ end
62
+
63
+ file_mode = position > 0 ? 'a' : 'w'
64
+
65
+ begin
66
+ ::File.open(@filepath, file_mode) do |file|
67
+ self.state = :downloading
68
+
69
+ while buffer = @socket.readpartial(8192)
70
+ position += buffer.bytesize
71
+ file.write(buffer)
72
+ break if position >= @filesize
73
+ end
74
+ end
75
+
76
+ self.state = :finished
77
+ rescue EOFError
78
+ self.state = :aborted
79
+ ensure
80
+ @socket.close
81
+ end
82
+ ensure
83
+ return self
84
+ end
85
+
86
+ def resumable?
87
+ ::File.file?(@filepath) && ::File.size(@filepath) <= @filesize
88
+ end
89
+
90
+ def resume_transfer
91
+ self.state = :resuming
92
+ filename = Regexp.escape(@filename)
93
+ position = ::File.size(@filepath)
94
+ file = Actor.current
95
+
96
+ cb = @bot.on(:query, /\A\001DCC ACCEPT \"?#{filename}\"? #{@port} #{position}\001\z/, 1) do |env|
97
+ file.async.resume_accepted(position)
98
+ cb.async.remove
99
+ end
100
+
101
+ @user.message("\001DCC RESUME \"#{@filename}\" #{@port} #{position}\001")
102
+
103
+ after(30) do
104
+ if @state == :resuming
105
+ cb.remove
106
+ file.state = :resume_not_accepted
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,107 @@
1
+ module Vetinari
2
+ module Dcc
3
+ class Server
4
+ include Celluloid
5
+
6
+ attr_reader :user, :filepath, :state
7
+
8
+ def initialize(user, filepath, filename, server_manager)
9
+ @user = user
10
+ @filepath = filepath
11
+ @filename = filename
12
+ @server_manager = server_manager
13
+ @state = :idling
14
+ end
15
+
16
+ def run(port)
17
+ @port = port
18
+ register_resume_callback
19
+ start
20
+ end
21
+
22
+ def inspect
23
+ "#<Server @user=#{@user.inspect} @filepath=#{filepath}"
24
+ end
25
+
26
+ private
27
+
28
+ def start
29
+ @thread = Thread.new do
30
+ begin
31
+ int_ip = IPAddr.new(@server_manager.external_ip).to_i
32
+ @socket = TCPServer.new(@server_manager.internal_ip, @port)
33
+ @state = :running
34
+ @user.message("\001DCC SEND \"#{@filename}\" #{int_ip} #{@port} #{File.size(@filepath)}\001")
35
+ @client = @socket.accept
36
+ @state = :sending
37
+
38
+ File.open(@filepath, 'rb') do |file|
39
+ file.seek(@position) if @position
40
+
41
+ while buffer = file.read(8096)
42
+ @client.write(buffer)
43
+ end
44
+ end
45
+
46
+ # Clients like mIRC want to close the connection
47
+ # client side. Else the transfer will count as
48
+ # failed. So, give the client a second to close.
49
+ sleep 1
50
+ rescue StandardError => e
51
+ p e.message
52
+ ensure
53
+ # binding.pry
54
+ async.stop
55
+ end
56
+ end
57
+
58
+ after(5) do
59
+ p 'TIMMERERRRRRRRRRRRRRRRRRRRRR'
60
+ if @state == :running
61
+ async.stop(:timeout)
62
+ p :async_stop
63
+ end
64
+ end
65
+ end
66
+
67
+ def stop(state = :stopped)
68
+ p 'SERVER STOPPED'
69
+ @state = state
70
+ @client.close rescue nil
71
+ @socket.close rescue nil
72
+ remove_resume_callback
73
+ start_next
74
+ end
75
+
76
+ def start_next
77
+ @server_manager.release_port(@port)
78
+ p 'release port'
79
+ @server_manager.async.start_sending
80
+ p 'start sending'
81
+ end
82
+
83
+ def register_resume_callback
84
+ @resume_callback = @server_manager.bot.on(:query, /\A\001DCC RESUME \"#{Regexp.escape(@filename)}\" #{@port} \d+\001\z/, 1) do |env|
85
+ if @state == :running
86
+ position = begin
87
+ result = env[:message].scan(/\A\001DCC RESUME \"#{Regexp.escape(@filename)}\" #{@port} (\d+)\001\z/)
88
+ Integer(result.first.first)
89
+ rescue
90
+ 0
91
+ end
92
+
93
+ if position > 0 && position < File.size(@filepath)
94
+ @position = position
95
+ @user.message("\001DCC ACCEPT \"#{@filename}\" #{@port} #{@position}")
96
+ remove_resume_callback
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def remove_resume_callback
103
+ @resume_callback.async.remove
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,83 @@
1
+ module Vetinari
2
+ module Dcc
3
+ class ServerManager
4
+ include Celluloid
5
+
6
+ attr_reader :bot, :internal_ip, :external_ip
7
+
8
+ def initialize(bot)
9
+ @bot = bot
10
+ @external_ip = @bot.config.dcc.external_ip
11
+
12
+ ip = Socket.ip_address_list.detect { |i| i.ipv4_private? }
13
+ @internal_ip = ip ? ip.ip_address : '0.0.0.0'
14
+
15
+ @ports = @bot.config.dcc.ports
16
+ @queue = []
17
+ @mutex = Mutex.new
18
+ @running_servers = {} # {port => #<Vetinari::Dcc::Server>}
19
+ end
20
+
21
+ def add_offering(user, filepath, filename)
22
+ server = Server.new(user, filepath, filename, Actor.current)
23
+ @mutex.synchronize { @queue << server }
24
+ async.start_sending
25
+ server
26
+ end
27
+
28
+ def start_sending
29
+ @mutex.synchronize do
30
+ if @queue.any?
31
+ port = get_available_port
32
+
33
+ if port
34
+ server = @queue.pop
35
+ server.async.run(port)
36
+ @running_servers[port] = server
37
+ else
38
+ # There are items in the queue but for some reasons no ports are
39
+ # available. Try again later.
40
+ after(3) { start_sending }
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def release_port(port)
47
+ @mutex.synchronize do
48
+ @running_servers.delete(port)
49
+ end
50
+ end
51
+
52
+ # def find_server(user, filename, port)
53
+ # server = @running_servers[port]
54
+
55
+ # if server && server.user == user && server.filename == filename
56
+ # end
57
+ # end
58
+
59
+ private
60
+
61
+ def get_available_port
62
+ (@ports - @running_servers.keys).shuffle.each do |port|
63
+ return port if port_available?(port)
64
+ end
65
+
66
+ nil
67
+ end
68
+
69
+ def port_available?(port)
70
+ Timeout::timeout(3) do
71
+ begin
72
+ TCPServer.new(port).close
73
+ true
74
+ rescue Errno::EADDRINUSE
75
+ false
76
+ end
77
+ end
78
+ rescue Timeout::Error
79
+ false
80
+ end
81
+ end
82
+ end
83
+ end