xget 2.1.5 → 3.0.0

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.
Files changed (4) hide show
  1. checksums.yaml +5 -5
  2. data/xget.rb +757 -0
  3. metadata +11 -11
  4. data/bin/xget +0 -710
data/xget.rb ADDED
@@ -0,0 +1,757 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # xget.rb - Created by George Watson on 2013/05/19
4
+ # https://github.com/takeiteasy/xget
5
+ #
6
+ # Copyright (c) 2013 George Watson, All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without modification,
9
+ # are permitted provided that the following conditions are met:
10
+ #
11
+ # Redistributions of source code must retain the above copyright notice, this list
12
+ # of conditions and the following disclaimer.
13
+ # Redistributions in binary form must reproduce the above copyright notice, this
14
+ # list of conditions and the following disclaimer in the documentation and/or other
15
+ # materials provided with the distribution.
16
+ #
17
+ # Neither the name of the copyright holder nor the names of its contributors may
18
+ # be used to endorse or promote products derived from this software without specific
19
+ # prior written permission.
20
+ #
21
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24
+ # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
25
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26
+ # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
27
+ # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
+ # POSSIBILITY OF SUCH DAMAGE.
31
+
32
+ require 'socket'
33
+ require 'thread'
34
+ require 'timeout'
35
+ require 'optparse'
36
+
37
+ # Why isn't this enabled by default?
38
+ Thread.abort_on_exception = true
39
+ # Put standard output into syncronised mode
40
+ $stdout.sync = true
41
+
42
+ # Version values
43
+ $ver_maj, $ver_min, $ver_rev = 2, 2, 1
44
+ $ver_str = "#{$ver_maj}.#{$ver_min}.#{$ver_rev}"
45
+
46
+ config = {
47
+ "out-dir" => './',
48
+ "skip-existing" => false,
49
+ "servers" => {},
50
+ "sleep-interval" => 5,
51
+ "allow-queueing" => false
52
+ }
53
+
54
+ def puts_error msg
55
+ puts "! \e[31mERROR\e[0m: #{msg}"
56
+ end
57
+
58
+ def puts_abort msg
59
+ abort "! \e[31mERROR\e[0m: #{msg}"
60
+ end
61
+
62
+ def puts_warning msg
63
+ puts "! \e[33mWARNING:\e[0m: #{msg}"
64
+ end
65
+
66
+ # Extend IO to readlines without blocking
67
+ class IO
68
+ def gets_nonblock
69
+ @rlnb_buffer ||= ""
70
+ ch = nil
71
+ while ch = self.read_nonblock(1)
72
+ @rlnb_buffer += ch
73
+ if ch == "\n" then
74
+ res = @rlnb_buffer
75
+ @rlnb_buffer = ""
76
+ return res
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # Extend Array to get averages
83
+ class Array
84
+ def average
85
+ inject(:+) / count
86
+ end
87
+ end
88
+
89
+ # Class to hold XDCC requests
90
+ class XDCC_REQ
91
+ attr_accessor :serv, :chan, :bot, :pack, :info
92
+
93
+ def initialize serv, chan, bot, pack, info = "*"
94
+ @serv = serv
95
+ @chan = chan
96
+ @bot = bot
97
+ @pack = pack
98
+ @info = info
99
+ end
100
+
101
+ def eql? other
102
+ self.serv == other.serv and self.chan == other.chan and self.bot == other.bot and self.pack == other.pack
103
+ end
104
+
105
+ def to_s
106
+ "[ #{self.serv}, #{self.chan}, #{self.bot}, #{self.pack}, #{self.info} ]"
107
+ end
108
+ end
109
+
110
+ # Class to hold DCC SEND info for when waiting for DCC ACCEPT
111
+ class XDCC_SEND
112
+ attr_accessor :fname, :fsize, :ip, :port
113
+
114
+ def initialize fname, fsize, ip, port
115
+ @fname = fname
116
+ @fsize = fsize
117
+ @ip = ip
118
+ @port = port
119
+ end
120
+
121
+ def to_s
122
+ "[ #{self.fname}, #{self.fsize}, #{self.ip}, #{self.port} ]"
123
+ end
124
+ end
125
+
126
+ # Class to emit events
127
+ module Emitter
128
+ def callbacks
129
+ @callbacks ||= Hash.new { |h, k| h[k] = [] }
130
+ end
131
+
132
+ def on type, &block
133
+ callbacks[type] << block
134
+ self
135
+ end
136
+
137
+ def emit type, *args
138
+ callbacks[type].each do |block|
139
+ block.call(*args)
140
+ end
141
+ end
142
+ end
143
+
144
+ # Class to handle IRC stream and emit events
145
+ class Stream
146
+ include Emitter
147
+ attr_accessor :io, :buf
148
+
149
+ def initialize serv
150
+ @buf = []
151
+ Timeout.timeout(5) { @io = TCPSocket.new serv, 6667 }
152
+ rescue SocketError => e
153
+ puts_abort "Failed to connect to #{serv}! #{e.message}"
154
+ rescue Timeout::Error
155
+ puts_abort "Connection to #{serv} timed out!"
156
+ end
157
+
158
+ def disconnect
159
+ @io.puts 'QUIT'
160
+ rescue Errno::EPIPE
161
+ end
162
+
163
+ def << data
164
+ @buf << data
165
+ end
166
+
167
+ def write
168
+ @buf.each do |x|
169
+ @io.puts x
170
+ emit :WROTE, x
171
+ end
172
+ @buf = []
173
+ rescue EOFError, Errno::ECONNRESET
174
+ emit :CLOSED
175
+ end
176
+
177
+ def read
178
+ read = @io.gets_nonblock
179
+ emit :READ, read
180
+ rescue IO::WaitReadable
181
+ emit :WAITING
182
+ rescue EOFError, Errno::ECONNRESET
183
+ emit :CLOSED
184
+ end
185
+ end
186
+
187
+ # Class to handle IRC stream
188
+ class Bot
189
+ attr_reader :stream
190
+
191
+ def initialize stream
192
+ @stream = stream
193
+ stream.on :CLOSED do stop; end
194
+ end
195
+
196
+ def start
197
+ @running = true
198
+ tick while @running
199
+ end
200
+
201
+ def stop
202
+ @running = false
203
+ end
204
+
205
+ def tick
206
+ stream.read
207
+ stream.write
208
+ sleep 0.001
209
+ end
210
+ end
211
+
212
+ # Get relative size from bytes
213
+ def bytes_to_closest bytes
214
+ fsize_arr = [ 'B', 'KB', 'MB', 'GB', 'TB' ]
215
+ exp = (Math.log(bytes) / Math.log(1024)).to_i
216
+ exp = fsize_arr.length if exp > fsize_arr.length
217
+ bytes /= 1024.0 ** exp
218
+ return "#{bytes.round(2)}#{fsize_arr[exp]}"
219
+ end
220
+
221
+ # Loop until there is no file with the same name
222
+ def safe_fname fname
223
+ return fname unless File.exists? fname
224
+
225
+ ext = File.extname fname
226
+ base = File.basename fname, ext
227
+ dir = File.dirname fname
228
+
229
+ cur = 2
230
+ while true
231
+ test = "#{dir}/#{base} (#{cur})#{ext}"
232
+ return test unless File.exists? test
233
+ cur += 1
234
+ end
235
+ end
236
+
237
+ # Get a close relative time remaining, in words
238
+ def time_distance t
239
+ if t < 60
240
+ case t
241
+ when 0 then "- nevermind, done!"
242
+ when 1..4 then "in a moment!"
243
+ when 5..9 then "less than 10 seconds"
244
+ when 10..19 then "less than 20 seconds"
245
+ when 20..39 then "half a minute"
246
+ else "less than a minute"
247
+ end
248
+ else # Use minutes, to aovid big numbers
249
+ t = t / 60.0
250
+ case t.to_i
251
+ when 1 then "about a minute"
252
+ when 2..45 then "#{t.round} minutes"
253
+ when 45..90 then "about an hour"
254
+ when 91..1440 then "about #{(t / 60.0).round} hours"
255
+ when 1441..2520 then "about a day"
256
+ when 2521..86400 then "about #{(t / 1440.0).round} days"
257
+ else "about #{(t/ 43200.0).round} months"
258
+ end
259
+ end
260
+ end
261
+
262
+ # Get elapsed time in words
263
+ def time_elapsed t
264
+ return "instantly!" if t <= 0
265
+
266
+ # Get the GMTime from seconds and split
267
+ ta = Time.at(t).gmtime.strftime('%S|%M|%H|%-d|%-m|%Y').split('|', 6).collect { |i| i.to_i }
268
+ ta[-1] -= 1970 # fuck the police
269
+ ta[-2] -= 1 # fuck, fuck
270
+ ta[-3] -= 1 # fuck the police
271
+
272
+ # Remove the 0 digets
273
+ i = 0
274
+ ta.reverse.each do |x|
275
+ break if x != 0
276
+ i += 1
277
+ end
278
+
279
+ # Unit suffixes
280
+ suffix = [ "seconds", "minutes", "hours", "days", "months", "years" ]
281
+ # Don't use plural if x is 1
282
+ plural = ->(x, y) { x == 1 ? y[0..-2] : y }
283
+ # Format string to "value unit"
284
+ format_str = ->(x) { "#{ta[x]} #{plural[ta[x], suffix[x]]}, " }
285
+
286
+ # Form the string
287
+ ta = ta.take(ta.length - i)
288
+ str = ""
289
+ (ta.length - 1).downto(0) { |x| str += format_str[x] }
290
+ "in #{str[0..-3]}"
291
+ end
292
+
293
+ # DCC download handler
294
+ def dcc_download ip, port, fname, fsize, read = 0
295
+ sock = nil
296
+ begin
297
+ Timeout.timeout(5) { sock = TCPSocket.new ip, port }
298
+ rescue Timeout::Error
299
+ puts_abort "Connection to #{ip} timed out!"
300
+ end
301
+ puts_abort "Failed to connect to \"#{ip}:#{port}\": #{e}" if sock.nil?
302
+
303
+ fsize_clean = bytes_to_closest fsize
304
+ avgs, last_check, start_time = [], Time.now - 2, Time.now
305
+ fh = File.open fname, (read == 0 ? "w" : "a") # Write or append
306
+ baca = read
307
+
308
+ # Form the status bar
309
+ print_bar = ->() {
310
+ print "\r\e[0K>> [ \e[1;35m"
311
+ pc = read.to_f / fsize.to_f * 100.0
312
+ bars = (pc / 5).to_i
313
+ bars.times { print "#" }
314
+ (20 - bars).times { print " " }
315
+ avg = avgs.average * 1024.0
316
+ kecepatan = (read - baca)
317
+ time_rem = time_distance ((fsize - read) / kecepatan) * 1.5
318
+ print "\e[0m ] \e[1;35m#{pc.round(2)}%\e[0m - #{bytes_to_closest read}/#{fsize_clean} \e[37m@\e[0m \e[1;33m#{bytes_to_closest kecepatan}/s\e[0m in \e[37m#{time_rem}\e[0m"
319
+
320
+ baca = read
321
+ last_check = Time.now
322
+ avgs.clear
323
+ }
324
+
325
+ while buf = sock.readpartial(8192)
326
+ read += buf.bytesize
327
+ avgs << buf.bytesize
328
+ print_bar[] if (Time.now - last_check) > 1 and not avgs.empty?
329
+
330
+ begin
331
+ sock.write_nonblock [read].pack('N')
332
+ rescue Errno::EWOULDBLOCK
333
+ rescue Errno::EAGAIN => e
334
+ puts_error "#{File.basename fname} timed out! #{e}"
335
+ end
336
+
337
+ fh << buf
338
+ break if read >= fsize
339
+ end
340
+ print_bar.call unless avgs.empty?
341
+ elapsed_time = time_elapsed (Time.now - start_time).to_i
342
+
343
+ sock.close
344
+ fh.close
345
+
346
+ puts "\n! \e[1;32mSUCCESS\e[0m downloaded \e[1;36m#{File.basename fname}\e[0m #{elapsed_time}"
347
+ rescue EOFError, SocketError => e
348
+ puts "\n! ERROR: #{File.basename fname} failed to download! #{e}"
349
+ end
350
+
351
+ opts = {"out-dir" => "./"}
352
+ OptionParser.new do |o|
353
+ o.banner = " Usage: #{$0} [options] [value] [links] [--files] [file1:file2:file3]\n"
354
+ o.on '-h', '--help', 'Prints help' do
355
+ puts o
356
+ puts "\n Examples"
357
+ puts " \txget.rb --config config.conf --nick test"
358
+ puts " \txget.rb --files test1.txt:test2.txt:test3.txt"
359
+ puts " \txget.rb #news@irc.rizon.net/ginpachi-sensei/1"
360
+ puts " \txget.rb #news@irc.rizon.net/ginpachi-sensei/41..46"
361
+ puts " \txget.rb #news@irc.rizon.net/ginpachi-sensei/41..46-2"
362
+ puts " \txget.rb #news@irc.rizon.net/ginpachi-sensei/41..46&49..52-2&30"
363
+ exit
364
+ end
365
+ o.on '-v', '--version', 'Print version' do
366
+ puts "#{$0}: v#{$ver_str}"
367
+ exit
368
+ end
369
+ o.on '-c', '--config CONFIG', String, 'Path to config file' do |a|
370
+ opts["config"] = a
371
+ end
372
+ o.on '-u', '--user USER', String, "IRC 'USER' for Ident" do |a|
373
+ opts["user"] = a
374
+ end
375
+ o.on '-n', '--nick NICK', String, "IRC nickname" do |a|
376
+ opts["nick"] = a
377
+ end
378
+ o.on '-p', '--pass PASS', String, "IRC 'PASS' for Ident" do |a|
379
+ opts["pass"] = a
380
+ end
381
+ o.on '-r', '--real NAME', String, "IRC 'Realname' for Ident" do |a|
382
+ opts["real"] = a
383
+ end
384
+ o.on '-s', '--nickserv PASS', String, "Password for Nickserv" do |a|
385
+ opts["nserv"] = a
386
+ end
387
+ o.on '-f', '--files A,B,C', Array, "Paths to file(s) that contain xget commands" do |a|
388
+ opts["files"] = a
389
+ end
390
+ o.on '-o', '--out DIR', String, "Path to output directory to save files to" do |a|
391
+ opts["out-dir"] = a
392
+ end
393
+ o.on '-q', '--allow-queueing', "Wait for pack to start downloading rather than fail immediately when queued" do |a|
394
+ opts["allow-queueing"] = true
395
+ end
396
+ o.on '-w', '--skip-existing', "Skip downloads that already exist" do |a|
397
+ opts["skip-existing"] = true
398
+ end
399
+ o.on '-z', '--sleep INTERVAL', Integer, "Time in seconds to sleep before requesting next pack. Zero for no sleep." do |a|
400
+ opts["sleep-interval"] = a
401
+ end
402
+ end.parse!
403
+
404
+ # Get the config location
405
+ config_loc = opts["config"]
406
+ config_loc = File.expand_path config_loc unless config_loc.nil?
407
+ if config_loc.nil? or not File.exists? config_loc
408
+ config_loc = File.expand_path "~/.xget.conf"
409
+ config_loc = ".xget.conf" unless File.exists? config_loc
410
+
411
+ unless File.exists? config_loc
412
+ puts "ERROR! Invalid config path '#{config_loc}''. Exiting!"
413
+ exit
414
+ end
415
+ end
416
+
417
+ # Insert config settings from arguments into config hash
418
+ cur_block = "*"
419
+ config["servers"][cur_block] = {}
420
+ %w(user nick pass real nserv).each do |x|
421
+ config["servers"][cur_block][x.to_sym] = opts[x] unless opts[x].nil?
422
+ end
423
+
424
+ # Check if specified output directory actually exists
425
+ puts_abort "Out directory, \"#{opts["out-dir"]}\" doesn't exist!" unless Dir.exists? opts["out-dir"]
426
+ config["out-dir"] = opts["out-dir"].dup
427
+ config["out-dir"] << "/" unless config["out-dir"][-1] == "/"
428
+
429
+ # Parse config
430
+ config_copies = {}
431
+ File.open(config_loc, "r").each_line do |line|
432
+ next if line.length <= 1 or line[0] == '#'
433
+
434
+ if line =~ /^\[(\S+)\]$/ # Check if header
435
+ cur_block = $1
436
+ if cur_block.include? ',' # Check if header contains more than one server
437
+ tmp_split = cur_block.split(",")
438
+ next unless tmp_split[0] =~ /^(\w+?).(\w+?).(\w+?)$/
439
+ config_copies[tmp_split[0]] = []
440
+ tmp_split.each do |x| # Add all copies to copies hash
441
+ next if x == tmp_split[0] or not x =~ /^(\w+?).(\w+?).(\w+?)$/
442
+ config_copies[tmp_split[0]].push x unless config_copies[tmp_split[0]].include? x
443
+ end
444
+ cur_block = tmp_split[0]
445
+ end
446
+
447
+ # Set current block to the new header
448
+ config["servers"][cur_block] = {} unless config["servers"].has_key? cur_block
449
+ elsif line =~ /^(\S+)=(.*+?)$/
450
+ # Check if current line is specifying out directory
451
+ case $1
452
+ when "out-dir"
453
+ t_out_dir = File.expand_path $2
454
+ puts_abort "Out directory, \"#{t_out_dir}\" doesn't exist!" unless Dir.exists? t_out_dir
455
+ config[$1] = t_out_dir
456
+ config[$1] << "/" unless config[$1][-1] == "/"
457
+ next
458
+ when "sleep-interval" then config[$1] = $2.to_i
459
+ when "skip-existing" then config[$1] = ($2 == "true")
460
+ when "allow-queueing" then config[$1] = ($2 == "true")
461
+ else
462
+ # Add value to current header, default is *
463
+ t_sym = $1.downcase.to_sym
464
+ config["servers"][cur_block][t_sym] = $2 unless config["servers"][cur_block].has_key? t_sym
465
+ end
466
+ end
467
+ end
468
+
469
+ # Go through each and make copies of the original
470
+ unless config_copies.empty?
471
+ config_copies.each do |k,v|
472
+ v.each { |x| config["servers"][x] = config["servers"][k] }
473
+ end
474
+ end
475
+
476
+ # Set the set the command line config options if specified
477
+ config["skip-existing"] = opts["skip-existing"] if opts["skip-existing"]
478
+ config["allow-queueing"] = opts["allow-queueing"] if opts["allow-queueing"]
479
+ config["sleep-interval"] = opts["sleep-interval"] unless opts["sleep-interval"].nil?
480
+
481
+ # Take remaining arguments and all lines from --files arg and put into array
482
+ to_check = ARGV
483
+ if opts['files'] != nil and not opts['files'].empty?
484
+ opts['files'].each do |x|
485
+ File.open(x, "r").each_line { |y| to_check << y.chomp } if File.exists? x
486
+ end
487
+ end
488
+
489
+ if to_check.empty?
490
+ puts opts
491
+ abort "\n No jobs, nothing to do!"
492
+ end
493
+
494
+ # Parse to_check array for valid XDCC links, irc.serv.org/#chan/bot/pack
495
+ tmp_requests = []
496
+ to_check.each do |x|
497
+ if x =~ /^(#\S+)@(irc.\S+.\w+{2,3})\/(\S+)\/([\.&\-\d]+)$/
498
+ chan = $1
499
+ serv = $2
500
+ bot = $3
501
+ info = config["servers"].has_key?(serv) ? serv : "*"
502
+ $4.split('&').each do |y|
503
+ if y =~ /^(\d+)(\.\.\d+(\-\d+)?)?$/
504
+ pack = $1.to_i
505
+ if $2.nil?
506
+ tmp_requests.push XDCC_REQ.new serv, chan, bot, pack, info
507
+ else
508
+ step = $3.nil? ? 1 : $3[1..-1].to_i
509
+ range = $2[2..-1].to_i
510
+
511
+ puts_abort "Invalid range #{pack} to #{range} in \"#{x}\"" if pack > range or pack == range
512
+
513
+ (pack..range).step(step).each do |z|
514
+ tmp_requests.push XDCC_REQ.new serv, chan, bot, z, info
515
+ end
516
+ end
517
+ end
518
+ end
519
+ else
520
+ puts_abort "#{x} is not a valid XDCC address\n XDCC Address format: #chan@irc.serv.com/bot/pack(s) or ^\/msg irc.serv.com bot xdcc send #id$"
521
+ end
522
+ end
523
+
524
+ # Remove duplicate entries from requests
525
+ i = j = 0
526
+ to_pop = []
527
+ tmp_requests.each do |x|
528
+ tmp_requests.each do |y|
529
+ to_pop << j if x.eql? y if i != j
530
+ j += 1
531
+ end
532
+ i += 1
533
+ end
534
+ to_pop.each { |x| tmp_requests.delete_at(x) }
535
+
536
+ # Sort requests array to hash, serv {} -> chan {} -> requests []
537
+ requests = {}
538
+ tmp_requests.each do |x|
539
+ requests[x.serv] = [] unless requests.has_key? x.serv
540
+ requests[x.serv] << x
541
+ end
542
+
543
+ if requests.empty?
544
+ puts opts
545
+ abort "\n No jobs, nothing to do!"
546
+ end
547
+
548
+ # Sort requests by pack
549
+ requests.each do |k,v|
550
+ puts "\e[1;33m#{k}\e[0m \e[1;37m->\e[0m"
551
+ v.sort_by { |x| [x.chan, x.bot, x.pack] }.each { |x| puts " #{x}" }
552
+ end
553
+ puts
554
+
555
+ exit 0
556
+
557
+ # H-h-here we g-go...
558
+ requests.each do |k, v|
559
+ req, info = v[0], config["servers"][v[0].info]
560
+ last_chan, cur_req, motd = "", -1, false
561
+ nick_sent, nick_check, nick_valid = false, false, false
562
+ xdcc_sent, xdcc_accepted, xdcc_queued = false, false, false
563
+ xdcc_accept_time, xdcc_ret, req_send_time = nil, nil, nil
564
+
565
+ stream = Stream.new req.serv
566
+ bot = Bot.new stream
567
+ stream << "NICK #{info[:nick]}"
568
+ stream << "USER #{info[:user]} 0 * #{info[:real]}"
569
+ stream << "PASS #{info[:pass]}" unless info[:pass].nil?
570
+
571
+ # Handle read data
572
+ stream.on :READ do |data|
573
+ /^(?:[:](?<prefix>\S+) )?(?<type>\S+)(?: (?!:)(?<dest>.+?))?(?: [:](?<msg>.+))?$/ =~ data
574
+ #puts "\e[1;37m>>\e[0m #{prefix} | #{type} | #{dest} | #{msg}"
575
+
576
+ case type
577
+ when 'NOTICE'
578
+ if dest == 'AUTH'
579
+ if msg =~ /erroneous nickname/i
580
+ puts_error 'Login failed'
581
+ stream.disconnect
582
+ end
583
+ #puts "> \e[1;32m#{msg}\e[0m"
584
+ else
585
+ if prefix =~ /^NickServ!/
586
+ if not nick_sent and info[:nserv] != nil
587
+ stream << "PRIVMSG NickServ :IDENTIFY #{info[:nserv]}"
588
+ nick_sent = true
589
+ elsif nick_sent and not nick_check
590
+ case msg
591
+ when /password incorrect/i
592
+ nick_valid = false
593
+ nick_check = true
594
+ when /password accepted/i
595
+ nick_valid = true
596
+ nick_check = true
597
+ end
598
+ end
599
+ #puts "> \e[1;33m#{msg}\e[0m"
600
+ elsif prefix =~ /^#{Regexp.escape req.bot}!(.*)$/i
601
+ case msg
602
+ when /already requested that pack/i, /closing connection/i, /you have a dcc pending/i
603
+ puts_error msg
604
+ stream << "PRIVMSG #{req.bot} :XDCC CANCEL"
605
+ stream << 'QUIT'
606
+ when /you can only have (\d+?) transfer at a time/i
607
+ if config["allow-queueing"]
608
+ puts "! #{prefix}: #{msg}"
609
+ puts_warning "Pack queued, waiting for transfer to start..."
610
+ xdcc_queued = true
611
+ else
612
+ puts_error msg
613
+ stream << "PRIVMSG #{req.bot} :XDCC CANCEL"
614
+ stream << 'QUIT'
615
+ end
616
+ else
617
+ puts "! #{prefix}: #{msg}"
618
+ end
619
+ end
620
+ end
621
+ when 'PRIVMSG'
622
+ if xdcc_sent and not xdcc_accepted and prefix =~ /#{Regexp.escape req.bot}!(.*)$/i
623
+ /^\001DCC SEND (?<fname>((".*?").*?|(\S+))) (?<ip>\d+) (?<port>\d+) (?<fsize>\d+)\001\015$/ =~ msg
624
+ unless $~.nil?
625
+ req_send_time = nil
626
+
627
+ tmp_fname = fname
628
+ fname = $1 if tmp_fname =~ /^"(.*)"$/
629
+ puts "Preparing to download: \e[1;36m#{fname}\e[0m"
630
+ fname = (config["out-dir"].dup << fname)
631
+ xdcc_ret = XDCC_SEND.new fname, fsize.to_i, [ip.to_i].pack('N').unpack('C4') * '.', port.to_i
632
+
633
+ # Check if the for unfinished download amd try to resume
634
+ if File.exists? xdcc_ret.fname and File.stat(xdcc_ret.fname).size < xdcc_ret.fsize
635
+ stream << "PRIVMSG #{req.bot} :\001DCC RESUME #{tmp_fname} #{xdcc_ret.port} #{File.stat(xdcc_ret.fname).size}\001"
636
+ xdcc_accepted = true
637
+ print "! Incomplete file detected. Attempting to resume..."
638
+ next # Skip and wait for "DCC ACCEPT"
639
+ elsif File.exists? xdcc_ret.fname
640
+ if config["skip-existing"]
641
+ puts_warning "File already exists, skipping..."
642
+ stream << "PRIVMSG #{req.bot} :XDCC CANCEL"
643
+
644
+ xdcc_sent, xdcc_accepted, xdcc_queued = false, false, false
645
+ xdcc_accept_time, xdcc_ret = nil, nil
646
+ next
647
+ else
648
+ puts_warnings "File already existing, using a safe name..."
649
+ xdcc_ret.fname = safe_fname xdcc_ret.fname
650
+ end
651
+ end
652
+
653
+ # It's a new download, start from beginning
654
+ Thread.new do
655
+ pid = fork do
656
+ puts "Connecting to: \e[1;34m#{req.bot}\e[0m @ #{xdcc_ret.ip}:#{xdcc_ret.port}"
657
+ dcc_download xdcc_ret.ip, xdcc_ret.port, xdcc_ret.fname, xdcc_ret.fsize
658
+ end
659
+
660
+ Process.wait pid
661
+ xdcc_sent, xdcc_accepted, xdcc_queued = false, false, false
662
+ xdcc_accept_time, xdcc_ret = nil, nil
663
+ end
664
+ end
665
+ elsif xdcc_accepted and xdcc_ret != nil and msg =~ /^\001DCC ACCEPT ((".*?").*?|(\S+)) (\d+) (\d+)\001\015$/
666
+ # DCC RESUME request accepted, continue the download!
667
+ xdcc_accept_time = nil
668
+ xdcc_accepted = false
669
+ puts "\e[1;32mSUCCESS\e[0m!"
670
+
671
+ Thread.new do
672
+ pid = fork do
673
+ puts "Connecting to: #{req.bot} @ #{xdcc_ret.ip}:#{xdcc_ret.port}"
674
+ dcc_download xdcc_ret.ip, xdcc_ret.port, xdcc_ret.fname, xdcc_ret.fsize, File.stat(xdcc_ret.fname).size
675
+ end
676
+
677
+ Process.wait pid
678
+ xdcc_sent, xdcc_accepted, xdcc_queued = false, false, false
679
+ xdcc_accept_time, xdcc_ret = nil, nil
680
+ end
681
+ end
682
+ when /^\d+?$/
683
+ type_i = type.to_i
684
+ case type_i
685
+ # when 1 # Print welcome message, because it's nice
686
+ # msg.sub!(/#{Regexp.escape info[:nick]}/, "\e[34m#{info[:nick]}\e[0m")
687
+ # puts "! #{msg}"
688
+ when 400..533 # Handle errors, except a few
689
+ next if [439, 462, 477].include? type_i
690
+ puts_error "#{msg}"
691
+ stream.disconnect
692
+ when 376 then motd = true # Mark the end of the MOTD
693
+ end
694
+ when 'PING' then stream << "PONG :#{msg}"
695
+ when 'ERROR' then (msg =~ /closing link/i ? puts(msg) : puts_error(msg))
696
+ end
697
+ end
698
+
699
+ # Handle things while waiting for data
700
+ stream.on :WAITING do
701
+ unless xdcc_accepted
702
+ if motd and not xdcc_sent
703
+ cur_req += 1
704
+ if cur_req >= v.length
705
+ stream.disconnect
706
+ next
707
+ end
708
+ req = v[cur_req]
709
+
710
+ if req.chan != last_chan
711
+ stream << "PART #{last_chan}" unless last_chan == ""
712
+ last_chan = req.chan
713
+ stream << "JOIN #{req.chan}"
714
+ end
715
+
716
+ # Cooldown between downloads
717
+ if cur_req > 0
718
+ puts "Sleeping for #{config["sleep-interval"]} seconds before requesting the next pack"
719
+ sleep(config["sleep-interval"])
720
+ end
721
+
722
+ stream << "PRIVMSG #{req.bot} :XDCC SEND #{req.pack}"
723
+ req_send_time = Time.now
724
+ xdcc_sent = true
725
+ end
726
+
727
+ # Wait 25 seconds for DCC SEND response, if there isn't one, abort
728
+ if xdcc_sent and not req_send_time.nil? and not xdcc_accepted
729
+ if config["allow-queueing"] and xdcc_queued
730
+ next
731
+ end
732
+ if (Time.now - req_send_time).floor > 25
733
+ puts_error "#{req.bot} took too long to respond, are you sure it's a bot?"
734
+ stream.disconnect
735
+ bot.stop
736
+ end
737
+ end
738
+
739
+ # Wait 25 seconds for a DCC ACCEPT response, if there isn't one, don't resume
740
+ if xdcc_sent and xdcc_accepted and not xdcc_accept_time.nil?
741
+ if (Time.now - xdcc_accept_time).floor > 25
742
+ puts "FAILED! Bot client doesn't support resume!"
743
+ puts "Connecting to: #{req.bot} @ #{xdcc_ret.ip}:#{xdcc_ret.port}"
744
+ dcc_download xdcc_ret.ip, xdcc_ret.port, xdcc_ret.fname, xdcc_ret.fsize
745
+ end
746
+ end
747
+ end
748
+ end
749
+
750
+ # Print sent data, for debugging only really
751
+ stream.on :WROTE do |data|
752
+ #puts "\e[1;37m<<\e[0m #{data}"
753
+ end
754
+
755
+ # Start the bot
756
+ bot.start
757
+ end