#!/usr/bin/env ruby
# autoexploit.rb / @jamiew <jamie@internetfamo.us>
# FREE ART & TECHNOLOGY LAB (F.A.T.) <http://fffff.at>
require 'rubygems'
require 'mechanize'
require 'hpricot'
require 'yaml'

# configuration
config = YAML.load(File.open('config.yml'))
$username = config['user']
$password = config['pass']
$collect_from = "autofollowbacks"

$snooze = 3 # minimum seconds b/w adding another follower
$pages = 30 # number of follower pages to grab
$followed_log = "log.yml"


# append to transaction log
def log(data, logfile = $followed_log)
  puts "#{logfile}: #{data.inspect}"
  File.open(logfile, 'a+') { |file|
    file << data.to_yaml
  }
end

# append data to an existing yaml array
# TODO: this rewrites the whole file each time -- didn't see a simple way to append to a YAML array (!)
def log_yaml(data, logfile = $followed_log)
    
  content = YAML.load(File.open(logfile)) if File.exists?( logfile )
  content ||= []

  File.open(logfile, 'w+') { |yf|
    content.concat data
    YAML.dump(content, yf)
  }  
end

# shortcut to fetching & mapping all screen_name elements
def collect_screen_names(target, agent)
  puts "Collecting screen names from #{target}..."
  names = []
  (1..$pages).each { |page|
    puts "Page #{page}..."
    page = agent.get("#{target}?page=#{page}") rescue (puts "Failed: #{$!}; sleeping"; sleep 5; next)
    names += Hpricot.XML(page.body).search('screen_name').map { |s| s.innerHTML }
    sleep 2
  }
  return names
end

# get a list of all of your followers' followers
def find_people(agent)
  
  # get my followers
  # TODO need to paginate, or randomize
  method = "friends" # TODO configurable
  target = "http://twitter.com/statuses/#{method}/#{$collect_from}.xml"
  my_followers = collect_screen_names(target, agent)
  to_follow = my_followers
  puts "My followers: #{my_followers.inspect}"
  
  # find all/some of their followers
  limit = 5 # adjust for throttling...
  #to_follow = my_followers.sort_by { rand }[0..limit].map { |follower|
  #  target = "http://twitter.com/statuses/followers/#{follower}.xml"
  #  sleep 2 # be nice to twitter
  #  collect_screen_names(target, agent)
  #}.flatten
  puts "Found #{to_follow.length} people!"
  return to_follow
end



# start
agent = WWW::Mechanize.new
agent.user_agent_alias = 'Mac Safari' # sneaky
agent.basic_auth($username, $password)

# find out all the people we're following, so we don't try to follow again
friendships = []

# load our previously found list of people to follow
# or else find some people and cache that list to disk
# clear the file to start over I guess. TODO
todo_list = 'todo.yml'
targets = []
if File.exists?(todo_list)
  puts "Loading previously saved targets..."
  targets = YAML.load( File.open(todo_list) )
  # targets = targets.sort_by { rand } # shuffle; TODO ruby needs sort_by! amirite
else
  puts "Finding new targets..."
  targets = find_people(agent)
  log(targets, todo_list)
end
puts "#{targets.length} targets"

# this guy will accumulate for longer & longer as we accrue errors
buildup = 5 #seconds


# add our targets
# GOGOGOGOGOGOGOGO
targets.each { |user|
  next if friendships.include?(user)

  begin
    
    # check if we are friends already!
    url = "http://twitter.com/friendships/exists.xml?user_a=#{$username}&user_b=#{user}"
    page = agent.get(url)
    doc = Hpricot.XML(page.body)
    exists = (doc/'friends').innerHTML
    if exists == 'true' || exists == true
      log({ :user => user, :time => Time.now, :status => 'exists' })
      puts "... already following, skipping..."
      next
    end
    
    # still here? make friends
    url = "http://twitter.com/friendships/create/#{user}.xml"
    puts "\nPOST #{url} ..."
    agent.post(url) 

    # save this fresh relationship
    friendships << user
    log({ :user => user, :time => Time.now, :status => 'added' })
  rescue Net::HTTPServiceUnavailable
    time = $snooze*5
    puts "(!!) #{$!} -- snoozing for #{$time}"
    sleep time
  rescue 
    # TODO catch "already friend" vs. "you are adding too many damn people" and snooze differently
    puts "(!!) Failed to add #{user}: #{$!}"
    log({ :user => user, :time => Time.now, :status => 'failed' })

    error_snooze = $snooze+buildup
    puts "Snoozing for #{error_snooze} seconds..."
    sleep error_snooze
    buildup = buildup**2
    limit = 60*10 #TODO make semi-configurable, 10 mins for now
    buildup = limit if buildup > limit
  ensure
    puts " => #{agent.page.body.length rescue 0} bytes"
    # TODO when the response length is 0 I think that means we've been banned
  end

  # snooze
  sleep 2+(rand*$snooze).ceil
}

sleep 60*(2+(rand*10).ceil) # sleep for 10 minutes between runs
