Permalink
Browse files

Initial commit, of AH sniper

  • Loading branch information...
0 parents commit 4fbf2abfc79ca06c68a4ede073be96679f38d357 Shadowed committed Jul 19, 2010
Showing with 374 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +302 −0 auction_house.rb
  3. +7 −0 exceptions.rb
  4. +28 −0 scanner.rb
  5. 0 snipe.rb
  6. +19 −0 snipe_presets.rb
  7. +17 −0 utilities.rb
1 .gitignore
@@ -0,0 +1 @@
+auctionsnipe.yml
302 auction_house.rb
@@ -0,0 +1,302 @@
+require 'rubygems'
+require 'mechanize'
+require 'logger'
+require 'cgi'
+require 'active_support'
+require 'nokogiri'
+require "base64"
+require 'yaml'
+
+module WowArmory
+ module AuctionHouse
+ class Scanner
+ attr_reader :config, :transactions, :total_gold, :next_check, :seen_seeds
+ CHECK_INTERVAL = 10.minutes
+ REPORT_INTERVAL = 10.minutes
+ THROTTLED_SLEEP = 5.seconds
+ MAINTENANCE_SLEEP = 10.minutes
+ SLEEP_INTERVAL = 0.52
+ http_time = 0
+ CONFIG_PATH = File.join("./", "auctionsnipe.yml")
+
+ def initialize
+ load_config()
+
+ @agent = Mechanize.new {|agent|
+ agent.user_agent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4"
+ agent.gzip_enabled = true
+ }
+ @agent.pre_connect_hooks << lambda { |params| params[:request]['Connection'] = 'keep-alive' }
+ end
+
+ def load_config
+ begin
+ @config = YAML::load(File.open(CONFIG_PATH).read) || {}
+ @config[:password] = Base64.decode64(@config[:password]) if !@config[:password].blank?
+
+ puts "Auctioning from #{@config[:char_name].capitalize} of #{@config[:region].upcase}-#{@config[:char_realm].camelize}"
+ rescue Exception => e
+ @config = {}
+
+ puts "Initial configuration! You can reset this by deleting auctionsnipe.yml."
+ ask_option(:key => "region", :text => "Account region [US]:", :default => "us")
+ ask_option(:key => "char_name", :text => "Character name:")
+ ask_option(:key => "char_realm", :text => "Character realm:")
+
+ @config[:search] = "http://#{@config[:region].downcase}.wowarmory.com/auctionhouse/search.json?sort=buyout&reverse=false&pageSize=40&rhtml=false&cn=#{CGI::escape(@config[:char_name])}&r=#{@config[:char_realm]}"
+ @config[:base] = "http://#{@config[:region].downcase}.wowarmory.com/auctionhouse"
+ end
+ end
+
+ def login!
+ @agent.get("#{@config[:base]}/") do |page|
+ login_result = page.form_with(:name => "loginForm") do |login|
+ login.accountName = ask_option(:key => "login", :text => "Account name:")
+ login.password = ask_option(:key => "password", :text => "Account password:")
+ end.submit
+
+ return true if login_result.code.to_i == 200 && !login_result.body.match(/#{@config[:char_name]}/i).blank?
+ return nil
+ end
+ end
+
+ # Pull from configuration, otherwise save
+ def ask_option(args)
+ return @config[args[:key].to_sym] if !@config[args[:key].to_sym].blank?
+
+ print "#{args[:text]} "
+ input = gets.chomp
+ if args[:boolean]
+ input = input.downcase
+ input = true if input == "t" || input == "true" || input == "y" || input == "yes"
+ input = nil if input == "f" || input == "false" || input == "n" || input == "no"
+ else
+ input = input.blank? && !args[:default].blank? && args[:default] || !input.blank? && input || nil
+ end
+
+ @config[args[:key].to_sym] = input
+ return input
+ end
+
+ def flush_config
+ @config[:password] = Base64.encode64(@config[:password]) if !@config[:password].nil?
+ open(CONFIG_PATH, "w+") do |file|
+ file.write(@config.to_yaml)
+ end
+ end
+
+ def process_error(code, message)
+ puts colorize("[#{code}] #{message}", "1;31;40")
+
+ sleep MAINTENANCE_SLEEP.to_i if code == 114
+ sleep THROTTLED_SLEEP.to_i if code == 1013
+ end
+
+ def check_errors(doc)
+ if doc.is_a?(Nokogiri::XML::Document)
+ error = doc.css("error")
+ if error.length > 0
+ process_error(error.attr("code").to_s.to_i, error.attr("message"))
+ return true
+ end
+ elsif doc.is_a?(Hash) && !doc["error"].blank?
+ process_error(doc["error"]["code"].to_i, doc["error"]["message"])
+ return true
+ end
+
+ return nil
+ end
+
+ def search(url)
+ begin
+ start_time = Time.now.to_f
+ response = @agent.get(url)
+ @http_time = Time.now.to_f - start_time
+ rescue Errno::ECONNRESET, Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EHOSTUNREACH => e
+ puts "#{e.class}: #{e.message}"
+ rescue Mechanize::ResponseCodeError => e
+ puts "Response code #{e.response_code}"
+ return
+ end
+
+ data = JSON::load(response.body)
+ return if check_errors(data)
+
+ # Check for maintenance
+ if data.match(/maintenancelogo\.gif/) || data.match(/thermaplugg\.jpg/) || data.match(/maintenance/)
+ raise ArmoryMaintenanceError
+ end
+
+ return data
+ end
+
+ def check_requirements(requires, auction, env_tbl)
+ return eval requires, binding
+ end
+
+ def buy_auction(guid, money)
+ begin
+ response = @agent.post("#{@config[:base]}/bid.json", {:auc => guid, :money => money})
+ rescue Errno::ECONNRESET, Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EHOSTUNREACH => e
+ puts "#{e.class}: #{e.message}"
+ rescue Mechanize::ResponseCodeError => e
+ puts "Response code #{e.response_code}"
+ return
+ end
+ data = JSON::load(response.body)
+
+ # When the no transactions error is returned, it still gives us transaction data to work with
+ if check_errors(data)
+ if !data["transactions"].nil?
+ @transactions = {:left => data["transactions"]["numLeft"].to_i, :reset_at => data["transactions"]["resetMillis"].to_f / 1000}
+ end
+ return
+ end
+
+ if !@transactions.nil? && data["transactions"] && data["transactions"]["numLeft"] != @transactions[:left]
+ puts "won!"
+ else
+ puts "not sure if we won."
+ end
+
+ if data["transactions"]
+ @transactions = {:left => data["transactions"]["numLeft"].to_i, :reset_at => data["transactions"]["resetMillis"].to_f / 1000}
+ end
+
+ puts colorize("Transactions left: #{@transactions[:left]}", "1;31;40") unless @transactions.nil?
+ end
+
+ def check_gold
+ begin
+ response = @agent.get("#{config[:base]}/money.json")
+ rescue Errno::ECONNRESET, Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EHOSTUNREACH => e
+ puts "#{e.class}: #{e.message}"
+ rescue Mechanize::ResponseCodeError => e
+ puts "Response code #{e.response_code}"
+ return
+ end
+
+ data = JSON::load(response.body)
+ return if check_errors(data)
+
+ if @total_gold.nil?
+ puts "Total gold: #{format_gold(data["money"].to_i)}"
+ elsif @total_gold != data["money"].to_i
+ gold = data["money"].to_i
+ puts "Gold changed, now have #{format_gold(gold)}, #{gold > @total_gold ? "gained" : "lost"} #{format_gold((gold - @total_gold).abs)}"
+ end
+
+ @total_gold = data["money"].to_i
+ end
+
+ def check_mail
+ begin
+ response = @agent.get("#{config[:base]}/create/?rhtml=no&cn=#{config[:char_name]}&r=#{config[:char_realm]}&f=#{AUCTION_TYPES[:faction]}")
+ rescue Errno::ECONNRESET, Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EHOSTUNREACH => e
+ puts "#{e.class}: #{e.message}"
+ rescue Mechanize::ResponseCodeError => e
+ puts "Response code #{e.response_code}"
+ return
+ end
+
+ doc = Nokogiri::XML(response.body)
+
+ if doc.nil?
+ puts colorize("Failed to load mail, invalid response from nokogiri parse", "30;10;10")
+ puts response.to_yaml
+ return
+ else
+ return if check_errors(doc)
+ end
+
+ @seen_seeds ||= {}
+
+ doc.css("inventory items invItem").each do |item|
+ if item.attr("mail").to_i >= 1 && ( @seen_seeds[item.attr("n")].nil? || @seen_seeds[item.attr("n")] != item.attr("mail").to_i )
+ puts colorize("New item in mail: #{item.attr("n")} x #{item.attr("mail")}", "1;32;40")
+ @seen_seeds[item.attr("n")] = item.attr("mail").to_i
+ end
+ end
+ end
+
+ def snipe(queries)
+ check_gold
+ check_mail
+ @next_check = Time.now + CHECK_INTERVAL
+ next_report = Time.now + REPORT_INTERVAL
+
+ print "Sleeping to be safe... "
+ sleep 5
+ puts "onward!"
+
+ previous_totals = {}
+
+ while true do
+ # Make sure we have enough transactions, otherwise will wait until they reset
+ if !@transactions.nil? && @transactions[:left] <= 0
+ puts colorize("Ran out of transactions, resets in %.2f minutes" % (@transactions[:reset_at] - Time.now.to_f) / 3600)
+ sleep (@transactions[:reset_at] - Time.now.to_f) + 5.minutes
+
+ puts "Finished sleeping, let's roll!"
+ end
+
+ # Every interval, check gold and mail
+ if @next_check < Time.now
+ check_gold
+ check_mail
+ @next_check = Time.now + CHECK_INTERVAL
+
+ sleep 5
+ end
+
+ if next_report < Time.now
+ next_report = Time.now + REPORT_INTERVAL
+ previous_totals = {}
+ end
+
+ # Run the queries, then repeat
+ queries.each do |name, query|
+ args = {"f" => query[:run_at]}
+ args = args.merge!(query[:filters]) unless query[:filters].nil?
+ url = "#{config[:search]}&#{args.map {|k, v| "#{k}=#{v}"}.join("&")}"
+
+ data = search(url)
+ if data.nil?
+ sleep SLEEP_INTERVAL
+ next
+ end
+
+ if previous_totals[name] != data["auctionSearch"]["end"]
+ puts "#{name}: #{data["auctionSearch"]["total"]} auctions found, showing #{data["auctionSearch"]["end"]} (%.3f seconds)" % [@http_time]
+ previous_totals[name] = data["auctionSearch"]["end"]
+ end
+
+ data["auctionSearch"]["auctions"].each do |auction|
+ next if auction["seller"].downcase == @config[:char_name].downcase || auction["buy"].blank? || auction["buy"].to_i == 0
+
+ # Check blacklist if we have one set
+ if query[:blacklist]
+ found = nil
+ query[:blacklist].each do |item|
+ if auction["n"].match(/#{item}/i)
+ found = true
+ break
+ end
+ end
+
+ next if !found.nil?
+ end
+
+ if check_requirements(query[:requires], auction, query[:env_tbl])
+ print "Trying to buy #{auction["n"]}x#{auction["quan"]} (#{auction["auc"]}) from #{auction["seller"]} at #{format_gold(auction["buy"])}... "
+ buy_auction(auction["auc"], auction["buy"])
+ end
+ end
+
+ sleep SLEEP_INTERVAL
+ end
+ end
+ end
+ end
+ end
+end
7 exceptions.rb
@@ -0,0 +1,7 @@
+module WowArmory
+ # Armory is under maintenance
+ class ArmoryMaintenanceError < RuntimeError; end
+
+ # Raised when the armory is temporarily unavailable
+ class TemporarilyUnavailableError < RuntimeError; end
+end
28 scanner.rb
@@ -0,0 +1,28 @@
+require 'auction_house'
+require 'snipe_presets'
+require 'utilities'
+include WowArmory::AuctionHouse::Utilities
+
+def setup
+ ah = WowArmory::AuctionHouse::Scanner.new
+ if ah.login! then
+ puts colorize("Logged in!", "1;32;40")
+ else
+ puts colorize("Invalid credentials or Armory not available", "1;31;40")
+ Kernel.exit
+ end
+
+ ah.flush_config
+ return ah
+end
+
+ah = setup
+
+if AUCTION_QUERIES.length == 0
+ puts colorize("Failed to load any auction queries.", "1;31;40")
+ return
+else
+ puts "Using queries: #{AUCTION_QUERIES.keys.join(", ")}"
+end
+
+ah.snipe(AUCTION_QUERIES)
0 snipe.rb
No changes.
19 snipe_presets.rb
@@ -0,0 +1,19 @@
+AUCTION_TYPES = {:faction => 1, :neutral => 2}
+AUCTION_NAMES = {1 => "faction", 2 => "neutral"}
+
+#{"auc":852360063,"n":"Accurate Ametrine","icon":"inv_jewelcrafting_gem_39","buy":746000,"bid":745000,"nbid":745000,"seller":"Porps","time":2,"id":40162,"qual":4,"quan":1,"req":1,"ilvl":80,"seed":1818733184,"charges":0}
+AUCTION_QUERIES = {
+ "Epics" => {
+ :requires => 'auction["buy"] <= 50000 && ( env_tbl.include?(auction["n"]) || ( auction["qual"] >= 4 && ( auction["ilvl"] >= 80 || auction["ilvl"] <= 60 ) ) )',
+ :env_tbl => ["Parrot Cage (Green Wing Macaw)", "Mulgore Hatchling", "Ammen Vale Lashling", "Durator Scorpion", "Elwynn Lamb", "Enchanted Broom", "Mechanopeep", "Sen'jin Fetish", "Teldrassil Sproutling", "Tirisfal Batling", "Captured Firefly", "Cat Carrier (White Kitten)", "Cat Carrier (Black Tabby)", "Dark Whelpling", "Darting Hatchling", "Deviate Hatchling", "Disgusting Oozling", "Gundrak Hatchling", "Leaping Hatchling", "Lil' Smoky", "Lifelike Mechanical Toad", "Mechanical Chicken", "Mechanical Squirrel Box", "Pet Bombling", "Ravasaur Hatchling", "Razormaw Hatchling", "Razzashi Hatchling", "Tiny Crimson Whelpling", "y Emerald Whelpling", "Cobra Hatchling", "X-51 Nether-Rocket X-TREME", "Teebu's Blazing Longsword", "Abyssal Scepter", "Big Battle Bear", "Battered Hilt", "Parrot Cage (Hyacinth Macaw)", ""],
+ #:filters => {"qual" => 4},
+ :blacklist => ["Nexus Crystal", "Iceblade Arrow", "Void Crystal", "Mysterious Arrow", "The Macho Gnome's Arrow", "Timeless Arrow", "Mysterious Shell", "Shatter Rounds", "The Sarge's Bullet", "Timeless Shell", "Staff of Jordan", "Fiery War Axe"],
+ :run_at => AUCTION_TYPES[:neutral],
+ },
+ #{
+ # :name => "Pets",
+ # :requires => 'auction["buy"] <= 50000',
+ # :filters => {"filterId" => "10%2C91", "qual" => 0}, # Should be Misc -> Pets
+ # :only_at => AUCTION_TYPES[:neutral],
+ #},
+}
17 utilities.rb
@@ -0,0 +1,17 @@
+module WowArmory
+ module AuctionHouse
+ module Utilities
+ def format_gold(copper)
+ copper = copper.to_i
+ gold = sprintf("%dg", copper / 10000)
+ silver = sprintf("%ds", (copper / 100) % 100)
+ copper = sprintf("%dc", copper % 100)
+ sprintf("%s %s %s", colorize(gold, "7;30;43"), colorize(silver, "7;30;47"), colorize(copper, "7;30;41"))
+ end
+
+ def colorize(text, color_code)
+ "\033[#{color_code}m#{text}\033[0m"
+ end
+ end
+ end
+end

0 comments on commit 4fbf2ab

Please sign in to comment.