#!/usr/bin/env perl package pcurse; use strict; use DateTime; use IO::Socket::SSL; use Getopt::Long; use Archive::Extract; use Thread::Pool; use JSON; use LWP::UserAgent; use HTML::HTML5::Parser; use feature ':5.10'; sub merge_opts { my $opts = shift; my $conf = shift; if(defined($opts->{'debug'})) { print 'We got opts:'."\n"; print Dumper $opts; print 'We got conf:'."\n"; print Dumper $conf; } foreach my $k(keys %{$opts}) { next unless(defined($opts->{$k})); $conf->{$k} = $opts->{$k}; } return $conf; } sub parse_arguments { my $toret; Getopt::Long::GetOptions ( "verbose" => \$toret->{'verbose'}, "wowpath=s" => \$toret->{'wowpath'}, "baseuri=s" => \$toret->{'baseuri'}, "config=s" => \$toret->{'config'}, "test" => \$toret->{'test'}, "workers=i" => \$toret->{'workers'}, "debug" => \$toret->{'debug'}, "add=s" => \$toret->{'add'}, "name=s" => \$toret->{'name'}, ); return $toret; } sub load_config { my $file = shift; my $toret; unless(-e $file) { my @p = split(/\//, $file); my $file = pop(@p); my $path = join('/', @p); unless(-d $path) { print 'Will create path: '.$path."\n"; system("mkdir","-p","$path"); } } else { $toret = pcurse::import_json($file); } $toret = pcurse::sane_defaults($toret); return $toret; } sub check_config { my $conf = shift; unless($conf->{'wowpath'}) { print 'Where is your addons installed? (complete path, including AddOns on the end): '; while(my $line = <>) { chomp($line); if(-e $line) { $conf->{'wowpath'} = $line; last; } print 'You sure? Cannot read that path. Try again: '; } } return $conf; } sub sane_defaults { my $in = shift; $in->{'addons'} = $ENV{'HOME'}.'/.pcurse/addons.json' unless(exists($in->{'addons'})); $in->{'workers'} = "4" unless(exists($in->{'workers'})); return $in; } sub load_addons { my $addons_file = shift; unless(-e $addons_file) { my @parts = split(/\//, $addons_file); my $f = pop(@parts); my $d = join('/', @parts); unless(-e $d) { system("mkdir","-p",$d); print 'Created '.$d."\n"; } if(-e $ENV{'HOME'}.'/.lcurse/addons.json') { print 'There seems to be an addons.json from lcurse around, and we have no list ourself yet. Stealing it:)'."\n"; my $json = &import_json($ENV{'HOME'}.'/.lcurse/addons.json'); $json = $json->{'addons'}; $json = pcurse::add_baseuri_to_addon($json); return $json; } } else { my $json = &import_json($addons_file); $json = pcurse::add_baseuri_to_addon($json); return $json; } return 0; } sub add_baseuri_to_addon { my $json = shift; foreach my $addon(@{$json}) { unless(exists($addon->{'host'})) { my $str = $addon->{'uri'}; my (undef,$h,undef,$u) = split(/(http(|s)\:\/\/[a-zA-Z0-9.]+)(\/.+)/,$str,2); $addon->{'uri'} = $u; $addon->{'host'} = $h; } #$addon->{'uri'} = 'https://www.curseforge.com'.$addon->{'uri'} unless($addon->{'uri'} =~ m/^http/); #$addon->{'baseuri'} = } return $json; } sub save_config { my $json = JSON->new; $json->convert_blessed; $json->allow_nonref; $json->allow_tags; $json->allow_blessed; my $file = shift; my $json_data = shift; if(ref $json_data eq 'ARRAY' or ref $json_data eq 'HASH') { my $text = $json->pretty->encode($json_data); open my $fh, ">", $file or return (0,'Could not open '.$file.' for writing: '.$!); print $fh $text; close $fh; return (1,$file.' saved successfully'); } else { say 'Invalid format on passed data (not a HASH or ARRAY ref)'; } } sub import_json { my $file = shift; my $json = JSON->new; my $json_data = do { local $/ = undef; open my $fh, "<", $file or die 'Could not read file '.$file.': '.$!; <$fh>; }; my $toret = $json->decode($json_data); return $toret; } sub html_parse { my $parser = HTML::HTML5::Parser->new(); my $html = shift; my %parseopts = (); my $doc = $parser->parse_string($html,\%parseopts); my $doctype = $parser->dtd_element($doc); return $html unless(defined($doctype)); return $doc; } sub http_get { my $uri = shift; my $ua = LWP::UserAgent->new(timeout => 10); $ua->agent('Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/43.0.2357.65 Mobile Safari/537.36'); my $response = $ua->get($uri); return $response if($response->is_success); return undef; } sub html_get { my $uri = shift; my $response = pcurse::http_get($uri); return undef unless($response); my $html = pcurse::html_parse($response->decoded_content); return $html; } sub get_latest_file_id { my $html = shift; my $uri = shift; return 'elvui' if($uri =~ m/tukui/); $uri .= '/download/'; my $retstr = pcurse::find_in_html('dlstring',$html,$uri); return $retstr; } sub get_product_version { my $html = shift; my $uri = shift; my $fileid = shift; if($fileid eq 'elvui') { $uri = '/downloads/'; } else { $uri .= '/files/'.$fileid; } my $retstr = pcurse::find_in_html('vstring',$html,$uri); return $retstr; } sub find_in_html { my $mode = shift; my $html = shift; my $sstring = shift; unless($sstring =~ m/\/downloads\//) { #ElvUI hacks my @sstringa = split /\//, $sstring,4; $sstring = pop(@sstringa); $sstring = '/'.$sstring; } my $ref = ref $html; my $retstr; if($ref) { #This means the http parser knows what to do and we're working on a parseable document my $results = $html->getElementsByTagName('a'); my @nodes = $results->get_nodelist; foreach my $context(@nodes) { my $href = $context->getAttribute('href'); next unless($href); if($href =~ m/$sstring/) { if($mode eq 'dlstring') { $retstr = (split(/$sstring/, $href,2))[1]; } elsif($mode eq 'vstring') { if($sstring =~ m/downloads/) { #hack for elvui $retstr = $context->getAttribute('href'); $retstr =~ s/$sstring//g; } else { $retstr = $context->getAttribute('data-name'); } } return $retstr if($retstr); } } } else { #This means we're on our own = whatever we're getting here is a html document as a string, unparsed. my $parser = HTML::HTML5::Parser->new(); if(defined($html)) { my @file = split(/\n/, $html); foreach my $line(@file) { if($line =~ m/$sstring/) { my $parsed = $parser->parse_balanced_chunk($line); my @nodes = $parsed->nonBlankChildNodes(); foreach my $node(@nodes) { my @atr = $node->attributes(); if($mode eq 'dlstring') { my $href = $node->getAttribute('href'); $retstr = (split(/$sstring/, $href,2))[1]; } elsif($mode eq 'vstring') { if($sstring =~ m/downloads/) { #ElvUI hacks $retstr = $node->getAttribute('href'); $retstr =~ s/$sstring//g; } else { $retstr = $node->getAttribute('data-name'); } } return $retstr if($retstr); } } } } else { return undef; } } return undef; } sub download_update { my $uri = shift; my $fileid = shift; my ($ret,$filename,$file) = pcurse::download($uri); return (1,$filename,$file) if($ret); return (0,undef,undef); } sub update { my $filename = shift; my $file = shift; my $targetpath = shift; $filename = '/tmp/'.$filename; unless(-e "$filename") { open my $fh, '>', "$filename" or return 0; print $fh $file; close $fh; } if(-e "$filename") { my $ae = Archive::Extract->new(archive => "$filename"); if($ae->extract(to=>$targetpath)) { system("rm","-v","$filename"); return 1; } } return 0; } sub download { my $uri = shift; my $file = pcurse::http_get($uri); if(defined($file)) { my $filename = $file->filename; my $content = $file->decoded_content; return (1,$filename,$content); } else { return (0,$uri,undef); } } sub init_pool { my $w = shift; my $p = Thread::Pool->new( { workers => $w, do => sub { my $todo = shift; if($todo eq 'check') { my $addon = shift; my $conf = shift; my $html = pcurse::html_get($addon->{'host'}.$addon->{'uri'}); my $fileid = pcurse::get_latest_file_id($html,$addon->{'host'}.$addon->{'uri'}); if($fileid) { $addon->{'fileid'} = $fileid; my $version = pcurse::get_product_version($html,$addon->{'uri'},$fileid); if($version && ($version ne $addon->{'version'})) { unless($conf->{'test'}) { if($fileid eq 'elvui') { $addon->{'downloaduri'} = $addon->{'host'}.'/downloads/'.$version; } else { $addon->{'downloaduri'} = $addon->{'host'}.$addon->{'uri'}.'/download/'.$fileid.'/file'; } $addon->{'targetversion'} = $version; return { retval => 1, did => 'check', addon => $addon }; } } elsif (! defined($version)) { return { retval => 0, did => 'check', result => 'Could not find version number', addon => $addon }; } else { return { retval => 0, did => 'check', result => 'No need to update', addon => $addon }; } } else { return { retval => 0, did => 'check', result => 'Could not find file id for '.$addon->{'name'} }; } } elsif($todo eq 'download') { my $uri = shift; my $fileid = shift; my ($ret,$filename,$file) = pcurse::download_update($uri,$fileid); return { retval => $ret, did => 'download', filename => $filename, filecontent => $file } if($ret); return { retval => 0, did => 'download', filename => undef, filecontent => undef, uri => $uri }; } else { return { retval => 0, result => 'Unknown task' }; } }, }); return $p; } sub updatelog { my $now = DateTime->now->iso8601(); my $addon_name = shift; my $addon_oldv = shift; my $addon_newv = shift; my $filename = $ENV{'HOME'}.'/.pcurse/update.log'; open my $fh, '>>', $filename, or return { retval => 0, message => 'Could not open '.$filename.' for appending' }; print $fh $now.': '.$addon_name.': '.$addon_oldv.' => '.$addon_newv."\n"; close $fh; return { retval => 1 } } 1;