vetinari 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -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
|