├── .gitignore ├── .xget.conf ├── LICENSE ├── README.md ├── xget-ss.jpg ├── xget.gemspec └── xget.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .*.*.swp 2 | *.swp 3 | .DS_Store 4 | *.gem 5 | bin/ 6 | -------------------------------------------------------------------------------- /.xget.conf: -------------------------------------------------------------------------------- 1 | # Specify directory to save to, this can be anywhere 2 | out-dir=~/Downloads 3 | # Skip downloads that already exist, instead of just downloading 4 | # agin under a slightly different name 5 | skip-existing=true 6 | sleep-interval=5 7 | allow-queueing=false 8 | 9 | # Default information (no heading) 10 | nick=radchad007 11 | user=radchad007 12 | real=chad 13 | #pass=no pass :( 14 | #nserv=no nserv pass :( 15 | 16 | # Information for specified server(s) 17 | [irc.rizon.net,irc.lolipower.org] 18 | nick=radchad007 19 | user=radchad007 20 | real=chad 21 | pass=2rad4u 22 | nserv=2rad4u 23 | 24 | # Some more information for a example 25 | [test.test.test] 26 | nick=radchad007 27 | user=radchad007 28 | real=chad 29 | #pass=no pass :( 30 | #nserv=no nserv pass :( 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 George Watson, All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xget 2 | 3 | _xget_ is a simple IRC bot that can easily automate downloading files over XDCC. 4 | 5 | You can download can either download the `xget.rb` file from here and chmod it, or download the gem `gem install xget`. 6 | 7 | ![screenshot](https://raw.githubusercontent.com/chocolateshirt/xget/master/xget-ss.jpg) 8 | 9 | ## Usage 10 | 11 | To get started, here is a basic example: 12 | 13 | ``` 14 | xget #news@irc.rizon.net/ginpachi-sensei/1 15 | ``` 16 | 17 | If you are familiar with IRC and XDCC bots this shouldn't be too hard to work out. If you're not, you might want to familiarise yourself with some basic concepts first. This command will instruct xget to connect to the `news` channel on the `irc.rizon.net` server and request XDCC package `1` from the bot `ginpanchi-sensei`. 18 | 19 | ``` 20 | xget #[channel]@[server]/[bot]/[packages] 21 | ``` 22 | 23 | To download multiple different packages at the same time, you can add a range, for example ```x..y```. This will queue downloads for all packages from x to y. You can also add a step to the range, ```x..y-n```. This will download all packages from x to y taking n steps through the range. For example, `10..20-2` would queue ```10, 12, 14, 16, 18 & 20```. 24 | 25 | Multiple ranges or packages can be chained together with ```&```. 26 | 27 | ``` 28 | xget #news@irc.rizon.net/ginpachi-sensei/1 29 | xget #news@irc.rizon.net/ginpachi-sensei/41..46 30 | xget #news@irc.rizon.net/ginpachi-sensei/41..46-2 31 | xget #news@irc.rizon.net/ginpachi-sensei/41..46&49..52-2&30 32 | ``` 33 | xget also supports `DCC RESUME`. So if the connection is cut off, you can continue the download where it left off. 34 | 35 | See `.xget.conf` for an example configuration file. Different aliases and accounts can be setup per-server. xget will look for a .xget.conf file in the directory it's located or in your home directory. 36 | 37 | ## License 38 | 39 | ``` 40 | Copyright (c) 2013 George Watson, All rights reserved. 41 | 42 | Redistribution and use in source and binary forms, with or without modification, 43 | are permitted provided that the following conditions are met: 44 | 45 | Redistributions of source code must retain the above copyright notice, this list 46 | of conditions and the following disclaimer. Redistributions in binary form must 47 | reproduce the above copyright notice, this list of conditions and the following 48 | disclaimer in the documentation and/or other materials provided with the distribution. 49 | Neither the name of the copyright holder nor the names of its contributors may be used 50 | to endorse or promote products derived from this software without specific prior written 51 | permission. 52 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 53 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 54 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 55 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 56 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 57 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 58 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 59 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 60 | POSSIBILITY OF SUCH DAMAGE. 61 | ``` 62 | -------------------------------------------------------------------------------- /xget-ss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takeiteasy/xget/fa5bbd30fdf8b49e848476663432512fe9eb410d/xget-ss.jpg -------------------------------------------------------------------------------- /xget.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "xget" 3 | s.version = "3.0.0" 4 | s.summary = "XDCC download client" 5 | s.description = "XDCC download client, see https://www.github.com/takeiteasy/xget for more information" 6 | s.authors = ["George Watson"] 7 | s.email = "gigolo@hotmail.co.uk" 8 | s.files = ["xget.rb"] 9 | s.homepage = "https://github.com/takeiteasy/xget" 10 | s.license = "BSD-3-Clause" 11 | end -------------------------------------------------------------------------------- /xget.rb: -------------------------------------------------------------------------------- 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 | /^(?:[:](?\S+) )?(?\S+)(?: (?!:)(?.+?))?(?: [:](?.+))?$/ =~ 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 (?((".*?").*?|(\S+))) (?\d+) (?\d+) (?\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 758 | --------------------------------------------------------------------------------