############################################################### # Technorati Favorite Your Fans ############################################################### # # This is a program that connects to the Technorati.com service, # finds everyone who has favorited your blog and automatically # favorites them back. # # But Why Use This? # # The "Technorati Favorites" feature was collecting a lot of # dust until recently when more and more people started exchanging # favorites. "Technorati Favorites" is a pretty cool feature because # it lets you search within your favorites and view the latest # posts from your favorites like an RSS feed. It's a good way # to track blogs you don't want to subscribe to the RSS feeds for. # # If someone has gone to the trouble of adding you to their # favorites, this program will return the favor, without requiring # you to go through all of the people who have favorited you and # trying to find the ones you haven't favorites back. This is # something people already do, and this program is intended to # save them time by automating the process. # # How It Works # # It only favorites a blog once. If you log into the Technorati # website and delete a blog from your favorites they will not # be added again. I strongly suggest checking the latest posts # from your favorites and deleting any blogs that you don't like. # If any of the blogs look like spam you should report them to # Technorati. # # It doesn't always grab everyone who favorited you on the first # try because of issues with Technorati. Even if you look at the # Favorited-By list on the web you'll see duplicates and missing # names. # # The first time you use this program it will add my blog to your # list of favorites, feel free to delete it and it won't be added # again. # # Get more information here: # http://internetducttape.com/tools/technorati-favorite-your-fans/ # # Visit my blog at http://internetducttape.com # ############################################################### # # This work is licensed under the Creative Commons # Attribution-Noncommercial-No Derivative Works 2.5 License. # # To view a copy of this license, visit # http://creativecommons.org/licenses/by-nc-nd/2.5/ or # send a letter to # Creative Commons, 543 Howard Street, 5th Floor, # San Francisco, California, 94105, USA. # ############################################################### # # NOTES # # It reuses all of the GUI code I built for Tag Cloud Generator, # which is kind of overkill for this application because it # doesn't have any settings. # ############################################################### # # TESTING NOTES # # - check for updates with no updates # - no username # - no password # - bad username # - user with no claimed blogs # - proper operation # - skipping over "already favorited" blogs # ############################################################### # # Tried using WebService::Technorati but it is a piece of junk that # fails for no reason on perfectly good API calls (when tried manually). # Interpreted the API calls by hand for a while but then they # started breaking as well for no reason. # ############################################################### # MODULE IMPORTS ############################################################### # perl2exe does a very bad job of auto-detecting module imports, # so I have to be explicit to a level of complete stupidity. use strict; use warnings; use WWW::Mechanize; use Tk; use Tk::Label; use Tk::DummyEncode; use Tk::Labelframe; use Tk::ROText; use Tk::Menu; use Tk::NoteBook; use Tk::Radiobutton; use Tk::NumEntryPlain; use Tk::HexEntryPlain; # NOTE: I hacked HexEntryPlain to display 6 digit values # for RGB HTML codes require YAML::Dumper::Base; use YAML::Dumper; require YAML::Loader::Base; use YAML::Loader; use YAML::Base; use YAML::Node; use YAML qw/LoadFile DumpFile/; use Cwd; use Data::Dumper; use Getopt::Long; use FileHandle; #require Tk::ErrorDialog; #perl2exe_include utf8; #perl2exe_include "unicore/lib/gc_sc/Word.pl"; #perl2exe_include "unicore/lib/gc_sc/Digit.pl"; #perl2exe_include "unicore/lib/gc_sc/SpacePer.pl"; #perl2exe_include "unicore/lib/gc_sc/Uppercas.pl"; #perl2exe_include "unicore/To/Lower.pl"; ############################################################### # CONSTANTS ############################################################### use constant NAME_SHORT => "Technorati Favorite Your Fans"; use constant NAME_LONG => "Technorati Favorite Your Fans"; use constant RELEASE_FILE => "release_notes.txt"; use constant RELEASE_URL => "http://internetducttape.com/tools/technorati-favorite-your-fans/release-notes/"; use constant CONFIG_FILE => "technoratiff.cfg"; use constant LOG_FILE => "technoratiff.log"; use constant BUILD => "07/05/31"; # Hash Keys use constant FAVORITED_ALREADY => 'favorited_already'; use constant YOUR_BLOGS => 'your_blogs'; ############################################################### # GLOBALS - Simple ############################################################### my $mech = WWW::Mechanize->new(autocheck =>0, onerror => undef); my $yaml = new YAML; my $mw; my $dir = ""; my $nogui = 0; my $status; my $ran_once = 0; my $very_first_time = 1; my $log; my $create_notes = 0; my $username = ""; my $password = ""; ############################################################### # GLOBALS - Data Structures ############################################################### # Configuration Settings my @storage = (); my %settings = (); my %default_settings = ( 'favorited_already' => \@storage, ); my %desc = ( 'favorited_already'=>{ 'cat'=>'data','order'=>'0','type'=>'array', 'desc'=>'List of Technorati usernames that have already been favorited'}, ); my %release_notes = ( "07/04/26"=>{ 'bugs'=>'Technorati changed and this version no longer works.', 'notes'=>qq{ First version. }}, "07/05/08"=>{ 'bugs'=>'Technorati changed and this version no longer works.', 'notes'=>qq{ Technorati update. }}, "07/05/23"=>{'bugs'=>'Technorati changed and this version no longer works.', 'notes'=>qq{ Added number of blogs added to final popup. Major Technorati update -- changes login form. }}, "07/05/31"=>{'bugs'=>'none', 'notes'=>qq{ Technorati update. }} ); my %settings_fields = (); ############################################################### # MAIN PROGRAM ############################################################### # Parse command line options. GetOptions ("dir=s" => \$dir, "nogui" => \$nogui, "notes"=>\$create_notes); # --nogui run without a gui, useful for cron/scheduled jobs # --notes dump release notes as a text file so # --dir store config information and output files to this directory #shortTest(); # Call the main program main(); sub main { devChecks(); handleCmdLineArgs(); loadConfig(); if (!$nogui) { guiSettings(); MainLoop; } else { doRunFirstTime(); } if ($log) { close($log); } exit 0; } ############################################################### # FUNCTION TESTING ############################################################### sub shortTest { $nogui = 1; handleCmdLineArgs(); } ############################################################### # ARGUMENT PARSING AND DATA STRUCTURE CHECKS ############################################################### # Make sure the data structure for the settings is correctly set up. sub devChecks { my $error_count = 0; foreach my $k (sort keys %release_notes) { if (not defined $release_notes{$k}{bugs}) { print STDERR "release_notes='$k' does not have a list of bugs\n"; $error_count++; } if (not defined $release_notes{$k}{notes}) { print STDERR "release_notes='$k' does not have any notes\n"; $error_count++; } } foreach my $k (sort keys %default_settings) { if (not defined $desc{$k}) { print STDERR "default_settings='$k' does not have a description\n"; $error_count++; } } foreach my $k (sort keys %desc) { if (not defined $default_settings{$k}) { print STDERR "desc='$k' does not have a setting\n"; $error_count++; } if (not defined $desc{$k}{desc}) { print STDERR "desc='$k' does not have an description\n"; $error_count++; } if (not defined $desc{$k}{order}) { print STDERR "desc='$k' does not have an order\n"; $error_count++; } if (not defined $desc{$k}{cat}) { print STDERR "desc='$k' does not have a category\n"; $error_count++; } if (not defined $desc{$k}{type}) { print STDERR "desc='$k' does not have a type\n"; $error_count++; } if ($desc{$k}{type} eq "num") { if (not defined $desc{$k}{min}) { print STDERR "desc='$k' does not have a min value\n"; $error_count++; } if (not defined $desc{$k}{max}) { print STDERR "desc='$k' does not have a max value\n"; $error_count++; } } } if ($error_count > 0) { die "encountered $error_count errors"; } } # Test command line arguments sub handleCmdLineArgs { # Should I display the gui or run in command line mode? if (! $nogui) { $mw = new MainWindow(-height=>600); } else { print "Running without gui\n"; } # Should I use a different directory? if ($dir ne "") { if (! -d $dir) { mkdir($dir); } chdir($dir); } $log = new FileHandle(LOG_FILE, "w") || doError("Cannot open ".LOG_FILE, "Could not write to ".LOG_FILE); if ($dir ne "") { print $log "Changed directory to $dir\n"; } if ($create_notes) { createReleaseNotes(); print "Done.\n"; exit 0; } } # Connect to //engtech and see if there is a new version sub checkVersion { addStatus("Checking version"); $mech = WWW::Mechanize->new(autocheck =>0, onerror => undef); $mech->get( RELEASE_URL ); if (!$mech->success) { addStatus("... could not connect to //engtech"); return 0; } my @lines = split(/\n/, $mech->content); my $capture = 0; my $notes = ""; foreach (@lines) { if (m/<\/pre>/) { $capture = 0; } if ($capture) { $notes .= $_ . "\n"; } if (m/
/) {
      $capture = 1;
    }
  }
  if ($notes ne "") {
    print $log "... got release notes\n";
    print $log $notes . "\n\n";
  }
  my $release = YAML::Load($notes);
  %release_notes = %{$release};
  my $version = BUILD;
  $version =~ s/^(\d\d\/\d\d\/\d\d).*?$/$1/;
  my $new_v_title = "";
  my $new_v = "";
  foreach my $v (sort {$b cmp $a} keys %release_notes) {
    #    addStatus(sprintf("$version cmp $v == %d",$v cmp $version));
    if (($v cmp $version) > 0) {
      if ($new_v_title eq "") {
	$new_v_title = NAME_SHORT." $v is Available";
	addStatus($new_v_title);
	$new_v .= "What's New\n";
      }
      $new_v .= $release_notes{$v}{notes};
    }
  }
  if (defined $release_notes{$version}{bugs}) {
    if ($release_notes{$version}{bugs} ne "none") {
      addStatus("... this version has the following bugs:");
      addStatus($release_notes{$version}{bugs});
      if ($new_v_title eq "") {
	addStatus("... these bugs aren't fixed yet.");
      }
      addStatus("");
    }
  }
  if ($new_v_title ne "") {
    $new_v .= "\nGet the new version at http://internetducttape.com/tools/\n";
    $mw -> messageBox(
		      -icon=>"info",
		      -type=>"ok",
		      -title=>$new_v_title,
		      -message=>$new_v);
  } else {
    addStatus("... you have the latest version.");
  }
  addStatus("Enter settings and click 'run' to get started");
}

# Dump the release notes structure to a file
sub createReleaseNotes {
  print "Creating release notes.\n";
  my $fields = \%release_notes;
  DumpFile(RELEASE_FILE, $fields);
}

###############################################################
# SUBROUTINES - GUI
###############################################################

# GUI: Add status
sub addStatus {
  my ($add) = @_;
  print $log "$add\n";
  print "$add\n";
  if (defined $status) {
    $status->idletasks;
  }
}

sub addLog {
  my ($add) = @_;
  print $log "$add\n";
}

# GUI: open up the settings configuration
sub guiSettings {
  $mw->optionAdd('*BorderWidth' => 1);
  $mw->optionAdd('relief' => 'raised');
  $mw->optionAdd('geometry'=>'800x600');

  my $book = $mw->NoteBook()->pack( -fill=>'both', -expand=>1 );

  my $tab1 = $book->add( "Sheet 1", -label=>"Start");

  my $ws_l = $tab1->Label(-text => '')->pack;
  $tab1->Label(-borderwidth => 0,-text => +NAME_LONG, -font=>'bold')->pack();
  $tab1->Label(-text => "[Version: ".BUILD."]")->pack();
  $ws_l = $tab1->Label(-text => '')->pack;

  my $frame_check_step = $tab1->Labelframe(-borderwidth => 0)->pack(qw/-side top -fill x -pady 0 -padx 0/);
  $frame_check_step->Label(-text => "           ")->pack(qw/-side left -pady 5 -padx 5/);
  $frame_check_step->Button(-text => 'Check for updates', -command => sub{checkVersion()} )
    ->pack(-side => 'right', -expand => 1, -fill => 'x', -pady => 1, -padx => 1);

  my $frame_settings_step = $tab1->Labelframe(-borderwidth => 0)->pack(qw/-side top -fill x -pady 0 -padx 0/);
  $frame_settings_step->Label(-text => "Step 1:")->pack(qw/-side left -pady 5 -padx 5/);

  my $frame_settings = $frame_settings_step->Labelframe(-borderwidth => 0)->pack(qw/-side top -fill x -pady 0 -padx 0/);
  my $frame = $frame_settings->Labelframe()->pack(qw/-side top -fill x/);
  $frame = $frame_settings->Labelframe()->pack(qw/-side top -fill x/);
  my $username_l = $frame->Label(-text => 'Technorati account name (IE: engtech)')->pack(-side=>'left', -padx=>5);
  my $username_w = $frame->Entry(-width => 40)->pack(-side=>'right');
  $username_w->insert(0, $username);
  $frame = $frame_settings->Labelframe()->pack(qw/-side top -fill x/);
  my $password_l = $frame->Label(-text => 'Technorati password')->pack(-side=>'left', -padx=>5);
  my $password_w = $frame->Entry(-width => 40)->pack(-side=>'right');
  $password_w->insert(0, $password);

  my $frame_run_step = $tab1->Labelframe(-borderwidth => 0)->pack(qw/-side top -fill x -pady 0 -padx 0/);
  $frame_run_step->Label(-text => "Step 2:")->pack(qw/-side left -pady 5 -padx 5/);
  $frame_run_step->Button(-text => 'Run (takes a while)', -command => sub{doRun($username_w, $password_w)} )
    ->pack(-side => 'right', -expand => 1, -fill => 'x', -pady => 1, -padx => 1);

  my $frame_close_step = $tab1->Labelframe(-borderwidth => 0)->pack(qw/-side top -fill x -pady 0 -padx 0/);
  $frame_close_step->Label(-text => "Step 3:")->pack(qw/-side left -pady 5 -padx 5/);
  $frame_close_step->Button(-text => 'Close', -command => sub{doClose()} )
    ->pack(-side => 'right', -expand => 1, -fill => 'x', -pady => 1, -padx => 1);

  my $frame_output = $tab1->Labelframe(-borderwidth=>0)->pack(qw/-side top -fill x -pady 0 -padx 0/);
  $frame_output->Label(-text => "", -borderwidth=>0)->pack(qw/-side top -pady 5 -padx 5/);
  $status = $frame_output->ROText(
				  -fg             => '#ffffff',
				  -bg             => '#000000',
				  -relief         => 'sunken',
				  -wrap           => 'none',
				  -bd             => 2,
				  -height         => 15,
				 )->pack(qw/-side left -fill x/);
  tie (*STDOUT, ref $status, $status);
  tie (*STDERR, ref $status, $status);

  #   my $tab2 = $book->add( "Sheet 2", -label=>"General Options");
  #   guiAdvancedSettings($tab2, ('sort','tag','tag_maxmin'));

  addStatus("Enter settings and click 'run' to get started");
}

# GUI: Sort the settings from least complicated to most complicated
sub sortSettings {
  my ($a, $b) = @_;
  my $cat_a = "";
  my $cat_b = "";
  my $order_a = -1;
  my $order_b = -1;
  if (defined $desc{$a}) {
    if (defined $desc{$a}{order}) {
      $order_a = $desc{$a}{order};
    }
    if (defined $desc{$a}{cat}) {
      $cat_a = $desc{$a}{cat};
    }
  }
  if (defined $desc{$b}) {
    if (defined $desc{$b}{order}) {
      $order_b = $desc{$b}{order};
    }
    if (defined $desc{$b}{cat}) {
      $cat_b = $desc{$b}{cat};
    }
  }
  if ($cat_a ne $cat_b) {
    return($cat_a cmp $cat_b);
  } else {
    return($order_a <=> $order_b);
  }
}

# GUI: Create a dialog for editting the advanced settings
sub guiAdvancedSettings {
  my ($tab, @cats) = @_;
  my $last_cat = '';
  my @keys = ();

  # Build a list of the keys that will be displayed on this page.
  foreach my $k (sort {sortSettings($a, $b)} keys %settings) {
    foreach my $c (@cats) {
      if ($desc{$k}{cat} eq $c) {
	push(@keys, $k);
      }
    }
  }

  my $frame = $tab->Labelframe(-borderwidth => 0)->pack(qw/-side top -fill x -fill y/);
  foreach my $k (@keys) {
    if ($last_cat ne $desc{$k}{cat}) {
      $last_cat = $desc{$k}{cat};
      $frame->Label(-text => '', -borderwidth => 0)->pack(qw/-side top -fill x -pady 0 -padx 0/);
    }
    if ($k =~ m/^bool/) {
      guiRadioEntry($frame, $k);
    } else {
      guiEditEntry($frame, $k);
    }
  }
  #  my $frame = $tab->Labelframe()->pack(qw/-side top -fill x/);
  #  $frame->Label(-text => "Debug options (you probably don't want to use these)")->pack(-side=>'left', -padx=>5, -pady=>5);
  #  $frame = $tab->Labelframe()->pack(qw/-side top -fill x/);
}

# Settings: Get a human readable list of the settings.
sub getSettings {
  my $text = "";
  $text .= "$username\n";
  foreach my $k (sort {sortSettings($a, $b)} keys %settings) {
    if ($settings{$k} eq $default_settings{$k}) {
      next;
    }
    my $label = $k;
    if (defined $desc{$k}{desc}) {
      $label = $desc{$k}{desc};
    }
    my $text_on = "on";
    if (defined $desc{$k}{on}) {
      $text_on = $desc{$k}{on};
    }
    my $text_off = "off";
    if (defined $desc{$k}{off}) {
      $text_off = $desc{$k}{off};
    }
    if ($k =~ m/^bool/) {
      if ($settings{$k}) {
	$text .= "$label = $text_on\n";
      } else {
	$text .= "$label = $text_off\n";
      }
    } else {
      $text .= "$label = $settings{$k}\n";
    }
  }
  return ($text);
}

# GUI: Create an edit box setting
sub guiEditEntry {
  my ($parent, $key) = @_;
  my $top = $parent->Labelframe(qw/-borderwidth 2/)->pack(qw/-side top -fill x/);
  my $label = $key;
  if (defined $desc{$key}{desc}) {
    $label = $desc{$key}{desc};
  }
  my $frame = $top->Labelframe(qw/-borderwidth 0/)->pack(qw/-side top -fill x/);
  my $edit_l = $frame->Label(-text => $label)->pack(-side=>'left', -padx=>5);
  if (defined $desc{$key}{example}) {
    $frame = $top->Labelframe(qw/-borderwidth 0/)->pack(qw/-side top -fill x/);
    $frame->Label(-text => "\t$desc{$key}{example}")->pack(-side=>'left', -padx=>5);
  }
  if (defined $desc{$key}{example2}) {
    $frame = $top->Labelframe(qw/-borderwidth 0/)->pack(qw/-side top -fill x/);
    $frame->Label(-text => "\t$desc{$key}{example2}")->pack(-side=>'left', -padx=>5);
  }
  $frame = $top->Labelframe(qw/-borderwidth 0/)->pack(qw/-side top -fill x/);
  my $edit_w;
  if ($desc{$key}{type} eq "num") {
    $edit_w = $frame->NumEntryPlain(-width => 40, -minvalue=>$desc{$key}{min}, -maxvalue=>$desc{$key}{max}, -value=>$settings{$key})->pack(-side=>'right');
  } elsif ($desc{$key}{type} eq "rgb") {
    $edit_w = $frame->HexEntryPlain(-width => 40, -minvalue=>0x0, -maxvalue=>0xFFFFFF,-value=>$settings{$key})->pack(-side=>'right');
  } else {
    $edit_w = $frame->Entry(-width => 40, -value=>$settings{$key})->pack(-side=>'right');
  }
  $settings_fields{$key} = $edit_w;
}

# GUI: Create a radio box setting
sub guiRadioEntry {
  my ($parent, $key) = @_;
  my $top = $parent->Labelframe(qw/-borderwidth 2/)->pack(qw/-side top -fill x/);
  my $label = $key;
  if (defined $desc{$key}{desc}) {
    $label = $desc{$key}{desc};
  }
  my $text_on = "on";
  if (defined $desc{$key}{on}) {
    $text_on = $desc{$key}{on};
  }
  my $text_off = "off";
  if (defined $desc{$key}{off}) {
    $text_off = $desc{$key}{off};
  }
  my $frame = $top->Labelframe(qw/-borderwidth 0/)->pack(qw/-side top -fill x/);
  my $edit_l = $frame->Label(-text => $label)->pack(-side=>'left', -padx=>5);
  $frame = $top->Labelframe(qw/-borderwidth 0/)->pack(qw/-side top -fill x/);
  my $edit_w_off = $frame->Radiobutton(-variable => \$settings{$key}, -value => 0, -text => $text_off)->pack(-side=>'right', -expand=>0);
  my $edit_w_on = $frame->Radiobutton(-variable => \$settings{$key}, -value => 1, -text => $text_on)->pack(-side=>'right', -expand=>0);
}

# GUI: Wrapper for when the RUN button is pressed.
sub doRun {
  my ($username_w, $password_w) = @_;

  my $username_tmp = $username_w->get;
  my $password_tmp = $password_w->get;

  addStatus("username is '$username_tmp'");

  # Clean up user input because it can never be trusted.
  chomp($username_tmp);
  chomp($password_tmp);
  $username_tmp =~ s/\r//;
  $password_tmp =~ s/\r//;
  $username_tmp =~ s/^\s+//;
  $password_tmp =~ s/^\s+//;
  $username_tmp =~ s/\s+$//;
  $password_tmp =~ s/\s+$//;

  # Test that users entered the fields
  if (0 == length($username_tmp)) {
    doError("Please enter your username", qq{
The username field is still empty.

Please enter your Technorati username.
});
    return;
  } elsif (0 == length($password_tmp)) {
    doError("Please enter your password", qq{
The password field is still empty.

Please your Technorati password.
});
    return;
  }

  # Have the settings changed? If so we need to recheck them.
  if ($username ne $username_tmp) {
    $ran_once = 0;
    $username = $username_tmp;
  }
  if ($password ne $password_tmp) {
    $ran_once = 0;
    $password = $password_tmp;
  }

  if (not $ran_once) {
    doRunFirstTime();
  } else {
    doRunAgain();
  }
}

# GUI: This is what happens when the RUN button is pressed the first time.
sub doRunFirstTime {
  # Log in
  if (login()) {
    generateFavorites();
    $ran_once = 1;
  }
}

# GUI: This is what happens when the RUN button is pressed AFTER the first time.
sub doRunAgain {
  generateFavorites();
}

# GUI: This is what happens when the CLOSE button is pressed.
sub doClose {
  saveConfig();
  addStatus("Goodbye");
  exit 0;
}

# GUI: Display an error message
sub doError {
  my ($title, $error) = @_;
  if (!$nogui) {
    $mw -> messageBox(
		      -icon=>"error",
		      -type=>"ok",
		      -title=>$title,
		      -message=>$error);
  } else {
    print "\n$title\n";
    print "$error\n";
  }
}

# GUI: Display an internal error message (these should never happen)
sub doInternalError {
  my ($error, $ln) = @_;
  doError("Internal Error", "engtech screwed up and you're seeing an error message you should never see.\nError is from line $ln in version: ".BUILD."\n$error");
}


###############################################################
# SUBROUTINES - Functionality
###############################################################

# Test login
sub login {
  my $login_url = "http://technorati.com";
  addStatus("Connecting to $login_url");

  # Reset cookies
  $mech = WWW::Mechanize->new(autocheck =>0, onerror => undef);
  $mech->get( $login_url );
  if (!$mech->success) {
    addStatus("-> Login failed");
    doError("Error Logging In to Technorati", qq{
Could not connect to $login_url with your username and password.

Troubleshooting:

 1. Do you have internet access?
    Try connecting to technorati.com with your web browser.

 2. Do you have a firewall running?
    Check that the firewall isn't blocking this program.

 3. Do you have the correct username/password?
    Try logging in to technorati.com with your web browser.

 4. Did Technorati change their page structure?
    Click on the "Check for Updates" button to see if there
    is a new version of this program available.
});
    return 0;
  }
  # 2007/05/23 - Technorati changed the order of this form.
  $mech->submit_form(
		     form_number => 2,
		     fields      => {
				     username    => $username,
				     password    => $password,
				    }
		    );
  my $failed = 1;
  if ($mech->success) {
    # FIXME: This will give false passes if they change the user screen.
    if ($mech->content !~ m/signinform/) {
      $failed = 0;
    }
  }
  if ($failed) {
    addStatus("-> Login failed");
    doError("Error Logging In to Technorati", qq{
Could not log in to Technorati.

Troubleshooting:

 1. Do you have the correct username/password?
    Try logging in to technorati.com with your web browser.

 2. Did Technorati change their page structure?
    Click on the "Check for Updates" button to see if there
    is a new version of this program available.
});
    return 0;
  }
  addStatus("-> Login success");
  return 1;
}


# Save the settings to a configuration file
sub saveConfig {
  foreach my $k (sort keys %settings_fields) {
    $settings{$k} = $settings_fields{$k}->get;
  }
  # FIXME: I should use contants instead of text for the settings keys to avoid
  # typos.
  # But until it's fixed just make sure no weird settings were created.
  foreach my $k (keys %settings) {
    if (not defined $default_settings{$k}) {
      doInternalError("Somehow the setting '$k' was created?", __LINE__);
    }
  }
  my $fields = {
		'username'=>$username,
		'password'=>rot13($password),
		'settings'=>\%settings
	       };
  DumpFile(CONFIG_FILE, $fields);
  addStatus("Saved configuration");
#  my $s = getSettings();
#  foreach my $line (split(/\n/, $s)) {
#    print $log "\t$line\n";
#  }
}

# Poor man's password encryption
sub rot13 {
  my ($text) = @_;
  $text =~ tr/[a-zA-Z]/[n-za-mN-ZA-M]/;
  return($text);
}

# Load the settings from the configuration file
sub loadConfig {
  addStatus("Loading configuration");
  # Copy default settings.
  foreach my $k (keys %default_settings) {
    $settings{$k} = $default_settings{$k};
  }
  if (-f CONFIG_FILE) {
    my $fields = LoadFile(CONFIG_FILE);
    $username = $fields->{username};
    $password = rot13($fields->{password});
    my %tmp = %{$fields->{settings}};
    foreach my $k (keys %tmp) {
      # Lose any settings that the program doesn't understand.
      if (defined $default_settings{$k}) {
	$settings{$k} = $tmp{$k};
      }
    }
  }
  if (defined $settings{+FAVORITED_ALREADY}) {
    addStatus("-> Loaded ".scalar @{$settings{+FAVORITED_ALREADY}}. " Technorati users already favorited.");
  }
}

# Successfully ran
sub generateSuccess {
  my ($added_blogs) = @_;
  my $title = "Favorited Your Technorati Fans";
  my $dir = getcwd;
  addStatus("-> Finished favoriting your Technorati fans");
  my $message = $title.qq{

Added another $added_blogs to your Technorati Favorites.

Everyone who has favorited you on Technorati has been added
as one of your favorites!

You should go to http://technorati.com/faves and browse through
the latest posts in your favorite blogs. If you do not like the
blog then clicking on the red X to remove it permanently.

It is a good idea to check your Technorati faves and remove
spammers/adult content/hateful content.
};
  if (!$nogui) {
    $mw -> messageBox(
		      -icon=>"info",
		      -type=>"ok",
		      -title=>$title,
		      -message=>$message);
  } else {
    print "\n$title\n";
    print "$message\n";
  }
}

# Main program routine
sub generateFavorites {
  saveConfig();
  my ($error, @blogs) = findBlogs($username);
  if (($error) || (0 == (scalar @blogs))) {
    doError("Could not find your blogs", qq{
Could not find your list of blogs on Technorati.
http://technorati.com/profile/$username

Troubleshooting:

 1. Did Technorati change their page structure?
    Click on the "Check for Updates" button to see if there
    is a new version of this program available.
});
    return;
  }
  my %skip_list = ();
  findAlreadyFaved(\%skip_list);
  my $added_blogs = 0;
  foreach my $b (@blogs) {
    my @favs = findFavs($b);
    $added_blogs += addFavs($b, \@favs, \%skip_list);
  }
  addStatus("\nDone.");
  generateSuccess($added_blogs);
}

sub findAlreadyFaved {
  my ($skip_ref) = @_;
  addStatus("Getting list of blogs you have already favorited (this can take a while)");
  my $fav_url = "http://technorati.com/faves/$username?show=blogs";
  $mech->get( $fav_url );
  if (!$mech->success) {
    addStatus("-> could not get already fav'd for '$username'");
    addLog("could not connected to:\n".$fav_url);
  }
  my $count = 0;
  foreach my $link ($mech->links()) {
    if ($link->url() =~ m/\/blogs\/http%3A%2F%2F(.*?)$/) {
      $skip_ref->{"http://$1"} = 1;
      $count++;
    }
  }
  addStatus("-> You already have $count favorites");
}


# Find all of the blogs belonging to a username. Not using the Technorati API
# because it breaks down *all* the time.
sub findBlogs {
  my ($t_user) = @_;
  my @result = ();
  addStatus("Finding blogs for $t_user");
  my $profile_url = "http://technorati.com/people/technorati/$t_user";
  $mech->get( $profile_url );
  if (!$mech->success) {
    addStatus("-> could not get blogs for '$t_user");
    addLog("could not connected to:\n".$profile_url);
    return(1, @result);
  }
#  foreach my $link ($mech->links()) {
#    if ($link->url() =~ m/\/blogs\/(http%3A%2F%2F|)(.*?)$/) {
#      push(@result, "http://$2");
#    }
#  }
  # Need to parse the page because they got rid of the ?fans argument.
  my @list = split(/<\/li>/, $mech->content);
  foreach (@list) {
    if (m/li class="blog">(.*)/is) {
      my $blog = $1;
#      print "\n\ntrying to match on:\n$blog\n";
      if ($blog =~ m//) {
	push(@result, $1);
      }
    }
  }
  addStatus(" -> found ".scalar @result." blogs");
  if (0 == scalar @result) {
    addStatus($profile_url);
  }
  return(0, @result);
}

# Technorati doesn't have an API call for Favorites, so
# we have to do it by brute force scraping.
sub findFavs {
  my ($blog_url) = @_;
  my $fav_url = "http://technorati.com/blogs/${blog_url}?show=faves";
  $mech->get( $fav_url );
  my $fav_count = 0;
  if ($mech->content =~ m/(\d+) fans/) {
    addStatus("Favorited by $1 fans: $blog_url");
    $fav_count = int($1 / 10);
  }
  my @result = ();
  for (my $i=0; $i <= $fav_count; $i++) {
    foreach my $link ($mech->links()) {
      if ($link->url() =~ m/\/people\/technorati\/(.*?)$/) {
	#         print "matched ".$link->url()."\n";
	push(@result, $1);
      }
    }
    #      print "debug: page $i gives ".scalar @result." favorited-bys\n";
    $mech->get ( $fav_url."&fans&page=".($i + 1) );
  }
  # The results may have a lot of dupes. This is a normal
  # issue that you'd see even when you view the Technorati
  # favorites using a web browser.
  return(@result);
}

# Transform the Technorati favorited-by into a list of real
# usernames to favorite.
sub usersFromFavoritedBy {
  my ($favs_ref) = @_;
  my %to_check = ('engtech'=>1);
  foreach my $f (@{$favs_ref}) {
    # Don't favorite yourself.
    if ($f eq $username) {
      next;
    }
    $to_check{$f} = 1;
  }
  # Filter out the results that have already been saved.
  foreach my $f (@{$settings{+FAVORITED_ALREADY}}) {
    if (defined $to_check{$f}) {
      addStatus("Skipping $f because already favorited");
      delete($to_check{$f});
    }
  }
  return(keys %to_check);
}

# Go through everyone who favorited you and favorite them in return.
sub addFavs {
  my ($blog_url, $favs_ref, $skip_ref) = @_;
  addStatus("\nAdding people who favorited $blog_url");
  my @users = usersFromFavoritedBy($favs_ref);

  my $added_blogs = 0;

  # Find and favorite blogs for all users
  my $user_count = 0;
  foreach my $u (@users) {
    $user_count++;
    my ($error, @blogs) = findBlogs($u);
    if ($error) {
      # Skipping this user for now, will try them another time.
      next;
    }
    my $success = 1;
    foreach my $b (@blogs) {
      if (defined $skip_ref->{$b}) {
	addStatus(" -> skipping $b (already fav'd)");
	next;
      }
      my $fav_url = "http://technorati.com/faves/?add=$b";
      $mech->get( $fav_url );
      if ($mech->success) {
	$added_blogs++;
      } else {
	$success = 0;
      }
    }
    if ($success) {
      # Add username to the list of favorited blogs
#      push( @{$settings{+FAVORITED_ALREADY}}, $u);
#      saveConfig();
      addStatus( sprintf("%d%% complete (%d/%d)", 
			 100 * $user_count / scalar(@users),
			$user_count, scalar(@users)) );
    }
  }
  addStatus("Done with $blog_url");
  return($added_blogs);
}