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