changeset 36:02576525bddb default tip

Added the \!isk command, had to change some stuff at 2 files of WebService::EveOnline (cpan: WebService-EveOnline-0.62) added those in a seperate folder. (Tried checking the http://theantipop.org/eve/trunk/WebService-EveOnline/ svn repo, but that seems to be in diff with the one offered via cpan.)
author df@i1600
date Thu, 03 Dec 2009 18:47:45 +0100
parents b0436bc63f9e
children
files WebService-EveOnline_changes/API/Character.pm WebService-EveOnline_changes/Base.pm skillbot.pl
diffstat 3 files changed, 710 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebService-EveOnline_changes/API/Character.pm	Thu Dec 03 18:47:45 2009 +0100
@@ -0,0 +1,302 @@
+package WebService::EveOnline::API::Character;
+
+use base qw/ WebService::EveOnline::Base /;
+
+our $VERSION = "0.61";
+
+=head4 new
+
+This is called under the hood when an $eve->character object is requested.
+
+It returns an array of character objects for each characters available via
+your API key. You probably won't need to call this directly.
+
+=cut
+
+sub new {
+    my ($self, $c, $name) = @_;
+
+    my $character_data = $self->call_api('character', {}, $c);
+
+    # XML::Simple is a big pile of shit:
+    $character_data = ($character_data->{name}) ? { $character_data->{name} => $character_data } : $character_data;
+
+    my $characters = [];
+    foreach my $character (sort keys %{$character_data}) {
+        next if $character =~ /^_/; # skip meta keys
+
+        my $char_obj = bless({ 
+                 _character_name => $character, 
+                 _corporation_name => $character_data->{$character}->{corporationName},
+                 _corporation_id => $character_data->{$character}->{corporationID},
+                 _character_id => $character_data->{$character}->{characterID},
+                 _api_key => $c->{_api_key},
+                 _user_id => $c->{_user_id},
+                 _evecache => $c->{_evecache},
+           }, __PACKAGE__ );
+
+        if ($name) {
+            return $char_obj if $char_obj->name eq $name;
+        } else {
+            push(@{$characters}, $char_obj);
+        }
+    }
+    
+    return @{$characters};
+}
+
+=head2 $character->hashref
+
+Returns a character hashref on a character object containing the following keys:
+
+   character_id
+   character_name
+   character_race
+   character_gender
+   character_bloodline
+   corporation_name
+   corporation_id
+
+=cut
+
+sub hashref {
+    my ($self) = @_;
+    return {
+        character_name => $self->{_character_name},
+        corporation_name => $self->{_corporation_name},
+        character_id => $self->{_character_id},
+        corporation_id => $self->{_corporation_id},
+        character_race => $self->character_race,
+        character_gender => $self->character_gender,
+        character_bloodline => $self->character_bloodline,
+    };
+}
+
+=head2 assets
+
+Placeholder
+
+=cut
+
+sub assets {
+    my ($self, $params) = @_;
+    my $assets = $self->call_api('assets', { characterID => $self->{_character_id} }, $self);
+    return $assets;   
+}
+
+=head2 assets
+
+Shows the char his balance (limited api key).
+
+=cut
+
+sub balance {
+    my ($self) = @_;
+    my $balance = $self->call_api('balance', { characterID => $self->{_character_id} }, $self);
+    return $balance->{balance};
+}
+
+=head2 kills
+
+Placeholder
+
+=cut
+
+sub kills {
+    my ($self, $params) = @_;
+    my $kills = $self->call_api('kills', { characterID => $self->{_character_id} }, $self);
+    return $kills;   
+}
+
+=head2 orders
+
+Placeholder
+
+=cut
+
+sub orders {
+    my ($self, $params) = @_;
+    my $orders = $self->call_api('orders', { characterID => $self->{_character_id} }, $self);
+    return $orders;   
+}
+
+=head2 $character->name
+
+Returns the name of the current character based on the character object.
+
+=cut
+
+sub name {
+    my ($self) = @_;
+    return $self->{_character_name};
+}
+
+=head2 $character->id
+
+Returns a character object based on the character id you provide, assuming
+your API key allows it.
+
+=cut
+
+sub id {
+    my ($self) = @_;
+    return $self->{_character_id};      
+}
+
+=head2 $character->race
+
+The race of the selected character.
+
+=cut
+
+sub race {
+    my ($self, $params) = @_;
+    my $race = $self->call_api('race', { characterID => $self->{_character_id} }, $self);
+    return $race->{race};   
+}
+
+=head2 $character->bloodline
+
+The bloodline of the selected character.
+
+=cut
+
+sub bloodline {
+    my ($self, $params) = @_;
+    my $bloodline = $self->call_api('bloodline', { characterID => $self->{_character_id} }, $self);
+    return $bloodline->{bloodLine}; 
+}
+
+=head2 $character->gender, sex
+
+The gender of the selected character.
+
+=cut
+
+sub gender {
+    my ($self, $params) = @_;
+    my $gender = $self->call_api('gender', { characterID => $self->{_character_id} }, $self);
+    return $gender->{gender};   
+}
+
+sub sex {
+    my ($self, $params) = @_;
+    my $gender = $self->call_api('gender', { characterID => $self->{_character_id} }, $self);
+    return $gender->{gender};   
+}
+
+=head2 $character->attributes
+
+Sets the base attributes held by the selected character.
+
+=cut
+
+sub attributes {
+    my ($self, $params) = @_;
+    my $attributes = $self->call_api('attributes', { characterID => $self->{_character_id} }, $self);
+
+    $self->{_attributes} = {
+        _memory => $attributes->{memory},
+        _intelligence => $attributes->{intelligence},
+        _charisma => $attributes->{charisma},
+        _perception => $attributes->{perception},
+        _willpower => $attributes->{willpower},
+    };
+
+    return bless($self, __PACKAGE__);    
+}
+
+=head2 $character->attributes->memory, $attributes->memory
+
+Returns the base memory attribute of the current character
+
+=cut
+
+sub memory {
+    my ($self) = @_;
+    return $self->{_attributes}->{_memory};
+}
+
+=head2 $character->attributes->intelligence, $attributes->intelligence
+
+Returns the base intelligence attribute of the current character
+
+=cut
+
+sub intelligence {
+    my ($self) = @_;
+    return $self->{_attributes}->{_intelligence};
+}
+
+=head2 $character->attributes->charisma, $attributes->charisma
+
+Returns the base charisma attribute of the current character
+
+=cut
+
+sub charisma {
+    my ($self) = @_;
+    return $self->{_attributes}->{_charisma};
+}
+
+=head2 $character->attributes->perception, $attributes->perception
+
+Returns the base perception attribute of the current character
+
+=cut
+
+sub perception {
+    my ($self) = @_;
+    return $self->{_attributes}->{_perception};
+}
+
+=head2 $character->attributes->willpower, $attributes->willpower
+
+Returns the base willpower attribute of the current character
+
+=cut
+
+sub willpower {
+    my ($self) = @_;
+    return $self->{_attributes}->{_willpower};
+}
+
+=head2 $character->attributes->attr_hashref, $attributes->attr_hashref
+
+Returns a hashref containing the base attributes of the
+current character with the following keys:
+    
+    memory
+    intelligence
+    charisma
+    perception
+    willpower
+
+=cut
+
+sub attr_hashref {
+    my ($self) = @_;
+    return {
+        memory => $self->{_attributes}->{_memory},  
+        intelligence => $self->{_attributes}->{_intelligence},  
+        charisma => $self->{_attributes}->{_charisma},  
+        perception => $self->{_attributes}->{_perception},  
+        willpower => $self->{_attributes}->{_willpower},  
+    };
+}
+
+=head2 $character->attribute_enhancers
+
+Returns a hash of hashes of the attribute enhancers held by the selected character.
+The interface to this is highly likely to change to be more consistent with the rest of the
+interface, so use with caution.
+
+=cut
+
+sub attribute_enhancers {
+    my ($self, $params) = @_;
+    my $enhancers = $self->call_api('enhancers', { characterID => $self->{_character_id} }, $self);
+    return $enhancers;  
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebService-EveOnline_changes/Base.pm	Thu Dec 03 18:47:45 2009 +0100
@@ -0,0 +1,395 @@
+package WebService::EveOnline::Base;
+
+our $VERSION = "0.62";
+
+use LWP::UserAgent;
+use HTTP::Request;
+use XML::Simple;
+
+use WebService::EveOnline::Cache;
+
+use WebService::EveOnline::API::Character;
+use WebService::EveOnline::API::Corporation;
+use WebService::EveOnline::API::Skills;
+use WebService::EveOnline::API::Transactions;
+use WebService::EveOnline::API::Journal;
+use WebService::EveOnline::API::Account;
+use WebService::EveOnline::API::Map;
+
+# U.G.L.Y. You ain't got no alibi (this is where we set up the API mappings, sort out the internal symbol conversion and set max cache times)
+# max_cache overrides the cache time set in the default EVE webservice response XML (e.g. shorter for wallet, longer for bloodline which
+# probably won't update every hour...)
+
+our $API_MAP = {
+    # Character
+    skills            => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 900     },
+    balance           => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 60      },
+    race              => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 604800  },
+    bloodline         => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 604800  },
+    attributes        => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ],                      },
+    enhancers         => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ],                      },
+    gender            => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 604800  },
+    training          => { endpoint => 'char/SkillInTraining',    params => [ [ 'character_id',    'characterID'   ] ],                      },
+    accounts          => { endpoint => 'char/AccountBalance',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 60      },
+    transactions      => { endpoint => 'char/WalletTransactions', params => [ 
+                                                                              [ 'character_id',    'characterID'   ], 
+                                                                              [ 'before_trans_id', 'beforeTransID' ], 
+                                                                              [ 'account_key',     'accountKey'    ],  
+                                                                                                                     ], max_cache => 3600    },
+    kills             => { endpoint => 'char/Killlog',            params => [ [ 'character_id',    'characterID'   ] ],                      },
+    orders            => { endpoint => 'char/MarketOrders',       params => [ [ 'character_id',    'characterID'   ] ],                      },
+    assets            => { endpoint => 'char/AssetList',          params => [ [ 'character_id',    'characterID'   ] ],                      },
+
+    # Corporation
+    corp_accounts     => { endpoint => 'corp/AccountBalance',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 60      },
+    corp_members      => { endpoint => 'corp/MemberTracking',     params => [ [ 'character_id',    'characterID'   ] ],                      },
+    corp_assets       => { endpoint => 'corp/AssetList',          params => [ [ 'character_id',    'characterID'   ] ],                      },
+    corp_sheet        => { endpoint => 'corp/CorporationSheet',   params => [ [ 'character_id',    'characterID'   ] ],                      },
+    corp_transactions => { endpoint => 'corp/WalletTransactions', params => [ 
+                                                                              [ 'character_id',    'characterID'   ], 
+                                                                              [ 'before_trans_id', 'beforeTransID' ], 
+                                                                              [ 'account_key',     'accountKey'    ],  
+                                                                                                                     ], max_cache => 3600    },
+    corp_kills        => { endpoint => 'corp/Killlog',            params => [ [ 'character_id',    'characterID'   ] ],                      },
+    corp_orders       => { endpoint => 'corp/MarketOrders',       params => [ [ 'character_id',    'characterID'   ] ],                      },
+    corp_baselist     => { endpoint => 'corp/StarbaseList',       params => [ [ 'character_id',    'characterID'   ] ],                      },
+    corp_base         => { endpoint => 'corp/StarbaseDetail',     params => [ [ 'character_id',    'characterID'   ] ],                      },
+
+    # Map
+    map_jumps         => { endpoint => 'map/Jumps',               params => [ [ 'character_id',    'characterID'   ] ],                      },
+    map_kills         => { endpoint => 'map/Kills',               params => [ [ 'character_id',    'characterID'   ] ],                      },
+    map               => { endpoint => 'map/Sovereignty',         params => undef                                     ,                      },
+
+    # Global/Misc
+    character         => { endpoint => 'account/Characters',      params => undef,                                      max_cache => 3600    },
+    all_skills        => { endpoint => 'eve/SkillTree',           params => undef,                                      max_cache => 86400   },
+    all_reftypes      => { endpoint => 'eve/RefTypes',            params => undef,                                                           },
+};
+
+=head2 new
+
+Called by WebService::EveOnline->new -- sets things up at the backend without cluttering things up.
+Doesn't die if not passed an api_key/user_id combination, unlike the latter.
+
+=cut
+
+sub new {
+    my ($class, $params) = @_;
+
+    $params ||= {};
+    $params->{cache_type}   ||= "SQLite";
+    $params->{cache_user}   ||= "";
+    $params->{cache_pass}   ||= "";
+    $params->{cache_dbname} ||= ($^O =~ /MSWin/) ? "c:/windows/temp/webservice_eveonline.db" : "/tmp/webservice_eveonline.db";
+    $params->{cache_init}   ||= "yes";
+    $params->{cache_maxage} ||= (86400 * 7 * 4); # time (s) between cache rebuilds. 28 days, for now.
+    
+    my $evecache = WebService::EveOnline::Cache->new( { eve_user_id => $params->{user_id}, cache_type => $params->{cache_type}, cache_dbname => $params->{cache_dbname} } ) if $params->{cache_init} eq "yes";
+    if ($evecache && $evecache->cache_age >= $params->{cache_maxage}) {
+        $evecache->repopulate( { skills => call_api('all_skills'), map => call_api('map') } );
+    } else {
+        $evecache ||= WebService::EveOnline::Cache->new( { cache_type => "no_cache" } );
+    }
+    
+    return bless({ _user_id => $params->{user_id}, _api_key => $params->{api_key}, _evecache => $evecache }, $class);
+}
+
+=head2 character, characters
+
+Pull back character objects based on your API key -- see examples/show_characters 
+
+Singlar and plural are provided so as to allow grammatically correct usage given
+the appropriate context (they both do exactly the same thing under the hood and
+can be used interchangeably -- handy for contractors... ;-) )
+
+=cut
+
+sub characters {
+    return WebService::EveOnline::API::Character->new(@_);
+}
+
+sub character {
+    return WebService::EveOnline::API::Character->new(@_);
+}
+
+=head2 corporation
+
+Pull back a corporation information object -- use on a character object for best effect.
+See examples/show_corporation
+
+=cut
+
+sub corporation {
+    return WebService::EveOnline::API::Corporation->new(@_);
+}
+
+=head2 skill, skills
+
+Pull back skill objects on a character. See examples/skills_overview for more
+details.
+
+Singlar and plural are provided so as to allow grammatically correct usage given
+the appropriate context (they both do exactly the same thing under the hood and
+can be used interchangeably).
+
+=cut
+
+sub skill {
+    return WebService::EveOnline::API::Skills->new(@_);
+}
+
+sub skills {
+    return WebService::EveOnline::API::Skills->new(@_);
+}
+
+=head2 transaction, transactions
+
+Returns transaction objects for a particular character/corporation. Singular/plural as above;
+See examples/show_transactions for more details.
+
+=cut
+
+sub transaction {
+    return WebService::EveOnline::API::Transactions->new(@_);
+}
+
+sub transactions {
+    return WebService::EveOnline::API::Transactions->new(@_);
+}
+
+=head2 journal
+
+Placeholder, for the moment. 
+
+=cut
+
+sub journal {
+    return WebService::EveOnline::API::Journal->new(@_);
+}
+
+=head2 account, accounts
+
+Return detailed account objects for a particular character, including corporate
+account info. The first member of the array ALWAYS returns the selected character's 
+personal account object -- subsequent accounts are from the corporation the
+character belongs to. See examples/show_character for an example of how to use this.
+
+=cut
+
+sub account {
+    return WebService::EveOnline::API::Account->new(@_);    
+}
+
+sub accounts {
+    return WebService::EveOnline::API::Account->new(@_);    
+}
+
+=head2 map
+
+Another placeholder.
+
+=cut
+
+sub map {
+    return WebService::EveOnline::API::Map->new(@_);
+}
+
+
+=head2 $eve->user_id
+
+Returns the current user_id.
+
+=cut
+
+sub user_id {
+    my ($self, $user_id) = @_;
+    $self->{_user_id} = $user_id if $user_id;
+    return $self->{_user_id};
+}
+
+=head2 $eve->api_key
+
+Returns the current api_key.
+
+=cut
+
+sub api_key {
+    my ($self, $api_key) = @_;
+    $self->{_api_key} = $api_key if $api_key;
+    return $self->{_api_key};
+}
+
+=head2 $eve->call_api(<command>, <params>)
+
+Call the Eve API and retrieve the results. Look in the cache first. Cache results according to API map settings.
+
+=cut
+
+sub call_api {
+    my ($self, $command, $params, $base) = @_;
+
+    my $auth = { user_id => "", api_key => "" };
+
+    if (ref($base)) {
+        $auth = { user_id => $base->user_id, api_key => $base->api_key };
+    } else {
+        $command = $self;
+    }
+
+    if ( defined($API_MAP->{$command}) ) {
+        my $cache = ref($self) ? $self->{_evecache} : $base->{_evecache};
+        
+        my $gen_params = _gen_params($self, $API_MAP->{$command}->{params}, $params);
+
+        my $cached_response = $cache->retrieve( { command => "$command", params => $gen_params } ) if ref($cache);
+        return $cached_response if $cached_response;
+        
+        my $ua = LWP::UserAgent->new;
+        $ua->agent("$WebService::EveOnline::AGENT/$WebService::EveOnline::VERSION");
+
+        my $req = HTTP::Request->new( POST => $WebService::EveOnline::EVE_API . $API_MAP->{$command}->{endpoint} . '.xml.aspx' );
+        $req->content_type("application/x-www-form-urlencoded");
+
+        my $content = 'userid=' . $auth->{user_id} . '&apikey=' . $auth->{api_key} . $gen_params;
+
+        $req->content($content) ;
+    
+        my $res = $ua->request($req);
+
+        if ($res->is_success) {
+            my $xs = XML::Simple->new();
+            my $xml = $res->content;
+
+            warn "RAW XML is:\n$xml\n" if $ENV{EVE_DEBUG} =~ m/xml/i;
+
+            my $pre = $xs->XMLin($xml);
+            my $data = {};
+            my $in_error_state = undef;
+
+            # print out any error content if it's set.
+            if ($pre->{error}) {
+                # error 206 is returned on characters without corp permissions. ignore. FIXME: nasty hack
+                if ($pre->{error}->{code} != "206") {
+                    $in_error_state = 1;
+                    $data->{error} = "EVE API Error: " . $pre->{error}->{content} . " (" . $pre->{error}->{code} . ")";
+                }
+            }
+
+            # at the moment, we deal in hashrefs. one day, these will be objects (like everything else will be ;-P)
+            unless ($in_error_state) {
+                if ($command eq "character") {
+                    $data = $pre->{result}->{rowset}->{row};
+                } elsif ($command eq "skills") {
+                    $data->{skills} = $pre->{result}->{rowset}->{skills}->{row} if $pre->{result}->{rowset}->{skills}->{row};
+                } elsif ($command eq "attributes") {
+                    $data = $pre->{result}->{attributes};
+                } elsif ($command eq "enhancers") {
+                    $data = $pre->{result}->{attributeEnhancers};
+                } elsif ($command eq "gender") {
+                    $data = $pre->{result};
+                } elsif ($command eq "race") {
+                    $data = $pre->{result};
+                } elsif ($command eq "bloodline") {
+                    $data = $pre->{result};
+                } elsif ($command eq "balance") {
+                    $data = $pre->{result};
+                } elsif ($command eq "training") {
+                    $data = $pre->{result};             
+                } elsif ($command eq "kills") {
+                    $data = $pre->{result}->{rowset}->{row};             
+                } elsif ($command eq "orders") {
+                    $data = $pre->{result}->{rowset}->{row};             
+                } elsif ($command eq "corp_kills") {
+                    $data = $pre->{result}->{rowset}->{row};             
+                } elsif ($command eq "corp_members") {
+                    $data = $pre->{result}->{rowset}->{row};             
+                } elsif ($command eq "corp_orders") {
+                    $data = $pre->{result}->{rowset}->{row};             
+                } elsif ($command eq "assets") {
+                    $data = $pre->{result}->{rowset}->{row};             
+                } elsif ($command eq "transactions") {
+                    $data->{transactions} = $pre->{result}->{rowset}->{row} if $pre->{result}->{rowset}->{row};
+                } elsif ($command =~ /accounts/) {
+                    my $acc = $pre->{result}->{rowset}->{row};
+                    $data->{accounts} = ref($acc) eq "HASH" ? [ $acc ] : $acc; 
+                } else {
+                    $data = $pre;
+                    return $data;
+                }
+            }
+
+            $data->{_status} ||= "ok";
+            $data->{_xml} = $xml;
+            $data->{_parsed_as} = $pre;
+
+            my $stripped_data = undef;
+            
+            unless ($WebService::EveOnline::DEBUG_MODE) {
+                $stripped_data = {};
+                foreach my $strip_debug (keys %{$data}) {
+                    next if $strip_debug =~ /^_/; # skip meta keys
+                    $stripped_data->{$strip_debug} = $data->{$strip_debug};
+                }
+            }
+
+
+            if ($cache && ($stripped_data || $data) && !$in_error_state) {
+                # error results are not cached
+                return $cache->store( { command => $command, obj => $self, data => $stripped_data || $data, params => $gen_params, cache_until => $pre->{cachedUntil}, max_cache => $API_MAP->{$command}->{max_cache}  } );
+            } elsif ($in_error_state) {
+                warn $data->{error} . "\n";
+                return undef; # better error handling is required...;
+            } else {
+                return $stripped_data || $data;
+            }
+        } else {
+            warn "Error code received: " . $res->status_line . "\n" if $ENV{EVE_DEBUG};
+            return { _status => "error", message => $res->status_line, _raw => undef };     
+        }
+    } else {
+        return { _status => "error", message => "Bad command", _raw => undef };     
+    }
+    
+}
+
+=head2 $character->before_trans_id
+
+Set to return transactions older than a particular trans id for character/corp transactions.
+
+=cut
+
+sub before_trans_id {
+    my ($self, $before_trans_id) = @_;
+    $self->{_before_trans_id} = $before_trans_id if $before_trans_id;
+    return $self->{_before_trans_id} || undef;
+}
+
+=head2 id
+
+This will not return anything useful on the base class; call id on characters, accounts, transactions, etc.
+where appropriate.
+
+=cut
+
+sub id {
+    return undef;
+}
+
+sub _gen_params {
+    my ($self, $keys, $passed) = @_;
+    return "" unless defined $keys;
+
+    my @kvp = ();
+    foreach my $param (@{$keys}) {
+        my ($intkey, $evekey) = @{$param};
+        if ($self->can($intkey)) {
+            push(@kvp, "$evekey=" . ($self->$intkey || $passed->{$intkey})) if ($self->$intkey || $passed->{$intkey});
+        } else {
+            push(@kvp, "$evekey=" . ($passed->{$evekey} || $passed->{$intkey})) if ($passed->{$evekey} || $passed->{$intkey});
+        }
+    }
+
+    return '&' . (join('&', @kvp));
+}
+
+1;
--- a/skillbot.pl	Sun Dec 07 11:18:23 2008 +0000
+++ b/skillbot.pl	Thu Dec 03 18:47:45 2009 +0100
@@ -13,6 +13,7 @@
 
 Copyright ©2008 Andy Smith <andy+twitfolk.pl@bitfolk.com>
 Portions copyright ©2008 Dominic Cleal <dominic@computerkb.co.uk>
+Small changes made by Bjorn Hamels <bjorn@hamels.nl>
 
 Artistic license same as Perl.
 =cut
@@ -225,6 +226,17 @@
 	my ($self, $event) = @_;
 	my ($their_nick, $msg) = ($event->nick, $event->args);
 
+	# lists the isk amounts per character
+	if ($msg =~ /^\!isk/i) {
+		my $text = "Isk: ";
+		for my $f (sort { $b->{char}->balance <=> $a->{char}->balance }
+							grep(defined $_->{char}->balance, @friends)) {
+			my $isk = int($f->{char}->balance/1000000);
+			$text .= $f->{char}->name . " (" . $isk . "M) | " if ($isk>0);
+		}
+		$self->privmsg('#' . $config{channel}, $text);
+	}
+	
 	# lists the current chars in training of the caller
 	if ($msg =~ /^\!eta(\s+(.+))?/i) {
 		my $found = 0;
@@ -403,7 +415,7 @@
 
 			if (! $frienduids{$uid}) {
 				my $api = WebService::EveOnline->new( { user_id => $uid,
-														api_key => $key } );
+									api_key => $key } );
 
 				my $loaded = 0;
 				foreach my $character ($api->characters) {