#!/usr/bin/perl use strict; use warnings; use Data::Dumper; use DateTime::TimeZone; use DateTime::Format::Strptime; use HTML::TreeBuilder; use HTTP::Cookies; use Getopt::Long; use LWP::UserAgent; use LWP::Simple; use Text::Unidecode; use Net::Twitter; use XML::Smart; use YAML qw(LoadFile); my $action = 'twitter'; my $config = ''; my $debug = ''; GetOptions('action=s' => \$action, 'conf=s' => \$config, 'debug' => \$debug, ); die "No config file specified" unless $config; my $conf = LoadFile($config); if ($action =~ m/^pop/) { populate(); } else { twitter(); } exit; sub populate { my $ua = LWP::UserAgent->new('requests_redirectable' => ['POST', 'GET', 'HEAD']); $ua->timeout(10); $ua->cookie_jar({ file => "$ENV{HOME}/.lwpcookies.txt" }); my ($content, $data, $output); my $path = $ENV{HOME}."/".$conf->{'path'}; open (DATA, ">$path") or die "Can't write file: $!\n"; binmode(DATA, ":utf8"); my $session = login($ua); foreach my $sat (keys %{ $conf->{'heavensabove'}{'sats'} }) { my $id = $conf->{'heavensabove'}{'sats'}{$sat}; $content = fetch($ua, "PassSummary.asp", { "Session" => $session->{"Session"}, 'satid' => $id }); $data = parse_standard($content); $output = output_iss($data, $sat); } $content = fetch($ua, "iridium.asp", { "Session" => $session->{"Session"}, 'Dur' => 7 }); $data = parse_iridium($content); $output = output_iridium($data); close (DATA); } sub twitter { my $path = $ENV{HOME}."/".$conf->{'path'}; # open data open (DATA, $path) or die "Can't find data in file $!\n"; binmode(DATA, ":utf8"); binmode(STDOUT, ":utf8"); # get times my $now = DateTime->now; my $strp = new DateTime::Format::Strptime(pattern => "%d %B %Y %H:%M:%S", on_error => 'croak',); # check data to send while () { my $line = $_; chomp $line; my ($time, $output) = split (/\|/, $line); # HACK this is going to go horribly wrong in December/January $time =~ s/(\w{3}) (\d{2})/$1 2007 $2/; # $time =~ s/$/ +0100/; my $event = $strp->parse_datetime($time); # includes timezone HACK my $offset = ($event->epoch - ($conf->{'tz'}*3600)) - $now->epoch; print "Deciding whether to send event in $offset seconds\n" if $debug; if ($offset > 15*60 && $offset < 21*60) { $output = unidecode($output); my $weather = weather_ok(); if ($weather) { print "Weather OK (code $weather), so sending:\n"; print "$output\n"; send_twitter($output); } else { print "Not sending event '$output'\n"; # TODO send private twitter to me } } } } ### populate routines sub login { my $ua = shift; # TODO oo my $auth = { UserName => $conf->{'heavensabove'}{'user'}, Password => $conf->{'heavensabove'}{'pass'}, }; my $response = $ua->post('http://www.heavens-above.com/processlogon.asp', $auth); my $session; if (!$response->is_success) { die "Couldn't log on"; } my %querydata = $response->request->uri->query_form; return \%querydata; } sub fetch { my $ua = shift; my $path = shift; my $query = shift; my $uri = URI->new("http://heavens-above.com/$path"); $uri->query_form(%{ $query }); my $response = $ua->get($uri); if (!$response->is_success) { die $response->status_line; } my $content = $response->content; return $content; } sub parse { my $content = shift; my $tree = HTML::TreeBuilder->new_from_content($content); my $datatable; foreach my $table ($tree->find("table")) { if ($table->attr("border") && $table->attr("cellpadding") == 5) { $datatable = $table; last; print "Got table data:\n".Dumper($table) if $debug; } else { print "Skipped table, no data\n" if $debug; } } if (!$datatable) { warn "No data table in content"; return undef; } my @rows = $datatable->content_list(); return \@rows; } sub parse_standard { my $content = shift; my $data; my $rows = parse($content); return unless defined $rows; my @rows = @{ $rows }; foreach my $row (@rows[2..$#rows]) { my @cells = $row->content_list(); my $rowdata = {'date' => $cells[0]->as_text, 'mag' => $cells[1]->as_text, 'start' => $cells[2]->as_text, 'sdir' => $cells[4]->as_text, 'max' => $cells[5]->as_text, 'alt' => $cells[6]->as_text, 'az' => $cells[7]->as_text, 'edir' => $cells[10]->as_text, }; # skip conditions - pretty lax for ISS as passes are rare next unless ($rowdata->{'alt'} > 20); foreach my $key (keys %{ $rowdata }) { my $value = $rowdata->{$key}; $value =~ s/^ +//; $value =~ s/ +$//; $rowdata->{$key} = $value; } push @{ $data }, $rowdata; } return $data; } sub output_iss { my $data = shift; my $name = shift; foreach my $row (@{$data}) { print DATA $row->{date}." ".$row->{start}."|$name Pass: mag ". $row->{mag}." starts at ". $row->{start}." from ".$row->{sdir}. " to ".$row->{edir}." with max elevation ".$row->{alt}. "\x{00B0} at ".$row->{'max'}." in direction ".$row->{az}."\n"; } } sub parse_iridium { my $content = shift; my $data; my $rows = parse($content); return unless defined $rows; my @rows = @{ $rows }; foreach my $row (@rows[1..$#rows]) { my @cells = $row->content_list(); my $rowdata = {'date' => $cells[0]->as_text, 'time' => $cells[1]->as_text, 'mag' => $cells[2]->as_text, 'alt' => $cells[3]->as_text, 'az' => $cells[4]->as_text, 'sat' => $cells[7]->as_text, }; # skip conditions my $alt = $rowdata->{'alt'}; $alt =~ s/\D//; next unless ($alt > 20); next unless $rowdata->{'mag'} < -3; # clean up foreach my $key (keys %{ $rowdata }) { my $value = $rowdata->{$key}; $value =~ s/^\s+//; $value =~ s/\s+(\))$/$1/; $rowdata->{$key} = $value; } push @{ $data }, $rowdata; } return $data; } sub output_iridium { my $data = shift; foreach my $row (@{$data}) { print DATA $row->{date}." ".$row->{time}."|Iridium Flare: mag ". $row->{mag}." at ".$row->{time}." with max elevation ".$row->{alt}. " in direction ".$row->{az}." (from ".$row->{sat}.")\n"; } } ### twitter routines sub send_twitter { my $output = shift; my $twit = Net::Twitter->new(username => $conf->{'twitter'}{'user'}, password => $conf->{'twitter'}{'pass'}); my $result = $twit->update($output); if ($result) { print "Twitter API replied with result ".Dumper($result)."\n"; } else { print "No Twitter reply, assume successful\n"; } } sub weather_ok { my $url = "http://weather.yahooapis.com/forecastrss?p=".$conf->{'yehoo'}{'locationcode'}; my $content = get($url); if (!$content) { print "Couldn't get weather, send anyway\n"; return 1; } my $code; my $xml = XML::Smart->new($content); my $nodeset = $xml->xpath->find('//yweather:condition[@code]'); foreach my $node ($nodeset->get_nodelist) { $code = $node->getAttribute("code"); } my @allowed = qw(21 25 29 30 31 32 33 34 36 37 44); foreach my $check (@allowed) { if ($code == $check) { return $code; } } print "Weather not good (code $code)\n"; return 0; } =head1 NAME abovestar - do all the work required for an Above * Twitter feed =head1 SYNOPSIS abovestar -c abovelondon.yaml -a populate abovestar -c abovelondon.yaml =cut