#!usr/bin/perl use feature qw(say); use JSON::XS qw(encode_json decode_json); use Selenium::Firefox::Profile; use Data::Dumper; use HTML::TreeBuilder::XPath; use File::Slurp qw(read_file write_file); use Selenium::Remote::WDKeys; use Selenium::Firefox; use Cwd; use Getopt::Long qw(GetOptions); use File::Basename; use List::Util 'shuffle'; use Selenium::Waiter qw/wait_until/; my (%config, $driver_profile, $driver); sub Init { %config = ( 'xpath' => { 'input_username_classic' => '//*[@id="id_username"]', 'input_password_classic' => '//*[@id="id_enc_password"]', 'button_login_classic' => '//*[@id="login-form"]/p[3]/input', 'button_addtohomescreen' => '/html/body/div[4]/div/div/div/div[3]/button[1][text()="Add to Home screen"]', 'button_addtohomescreen_cancel' => '/html/body/div[4]/div/div/div/div[3]/button[2][text()="Cancel"]', 'button_newpost' => '//div[contains(@data-testid, "new-post-button")]', 'input_newpostupload' => '//*[@id="react-root"]/section/nav[2]/div/div/form/input', 'button_expand' => '//span[text()="Expand"]', 'button_next' => '//button[text()="Next"]', 'button_share' => '//button[text()="Share"]', 'textarea_caption' => '//textarea[contains(@placeholder,"Write a caption")]', }, 'profile' => { 'adobebestprices' => { 'DBFilepath' => cwd() . '/profiles/adobebestprices/db/db_adobebestprices.dat', 'imageDir' => cwd() . '/profiles/adobebestprices/images', 'filename_as_title' => 1, 'tags' => ['#Beats', '#FLStudio20', '#Producer', '#Ableton', '#Beatmaker', '#Studio', '#ProTools', '#Music', '#DAW', '#LogicPro', '#VST', '#VSTplugins', '#NativeInstruments', '#Drums', '#MIDI', '#Omnisphere', '#Keyscape', '#Trilian', '#Logic', '#Waves', '#Antares', '#AutoTune', '#Kontakt', '#SoundToys', '#FABFilter', '#Arturia', '#Windows', '#MAC'], }, }, 'dumpToFile' => cwd() . '/get_page_source', 'pyautogui_cmd' => 'python3 ' . cwd() . '/autogui.py', 'profile_selected' => undef, 'wipe_after' => 0, ); &CheckParameter(); &UndumpFromFile(); &SelectImage(); $driver_profile = Selenium::Firefox::Profile->new; $driver_profile->set_preference( 'intl.accept_languages' => 'en-US', # 'general.useragent.override' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15', 'general.useragent.override' => 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16', ); # $driver_profile->set_boolean_preference("options.headless" => 1); $driver = Selenium::Remote::Driver->new( 'browser_name' => 'firefox', 'platform' => 'LINUX', 'remote_server_addr' => 'localhost', 'port' => 4444, 'firefox_profile' => $driver_profile, 'extra_capabilities' => { 'moz:firefoxOptions' => { #"args" => [ "--headless" ], }, }, ); # $driver = Selenium::Firefox->new( 'browser_name' => 'firefox', # 'platform' => 'LINUX', # 'binary' => '/usr/local/bin/geckodriver', # 'firefox_profile' => $driver_profile, # 'extra_capabilities' => { # 'moz:firefoxOptions' => { # #"args" => [ "--headless" ], # }, # }, # ); $driver->set_implicit_wait_timeout(5000); $driver->set_window_size(812, 375); say 'starting...'; say $driver->get_title(); say $driver->get_user_agent(); } &Init(); &Login(); &NewPost(); &Finish(); sub RndHashtagString { my $i = shift; my $RndArr = &RndArrItems($config{profile}{'adobebestprices'}{tags}, $i); my $hastags = join(' ', @$RndArr[0..$#{$RndArr}]); return $hastags; } sub RndArrItems { my ($arr_ref, $num_picks) = @_; # Shuffled list of indexes into @deck my @shuffled_indexes = shuffle(0..$#{$arr_ref}); # Get just N of them. my @pick_indexes = @shuffled_indexes[ 0 .. $num_picks - 1 ]; # Pick cards from @deck my @picks; for my $index (@pick_indexes) { push(@picks, $$arr_ref[$index]); } return \@picks; } sub SelectImage { &DirectoryListing(); &FindNewDataset(); $config{image_backslash} = $config{image_selected}; # $config{image_backslash} =~ s/\//\\/g; print Dumper \%config; } sub Login { say 'Logging in...'; $driver->get('https://instagram.com/accounts/login/?force_classic_login'); my $tree = HTML::TreeBuilder::XPath->new_from_content($driver->get_page_source()); &Rndwait(1,3); if( $tree->exists($config{xpath}{input_username_classic}) && $tree->exists($config{xpath}{input_password_classic}) ) { say 'Found: Login elements.'; # Username & password $driver->find_element($config{xpath}{input_username_classic})->click; $driver->send_keys_to_active_element('adobebestprices'); &Rndwait(1,3); $driver->find_element($config{xpath}{input_password_classic})->click(); $driver->send_keys_to_active_element('vst#1337'); &Rndwait(1,3); # Click login button $driver->find_element($config{xpath}{button_login_classic})->click(); } else { die 'Not found: Login elements.'; } } sub NewPost { &Rndwait(5,10); # We see the home screen my $tree = HTML::TreeBuilder::XPath->new_from_content($driver->get_page_source()); # add to homescreen popup? &ClickXpath($config{xpath}{button_addtohomescreen}, $tree, 0); # click cancel button &ClickXpath($config{xpath}{button_newpost}, $tree); # click new post button &Rndwait(5,10); # Bypass file dialog using pyautogui my $cmd = $config{pyautogui_cmd} . ' "' . $config{image_backslash} . '"'; # image path needs to use backslash for python my $ret = system($cmd) or die "system $cmd failed: $?"; # print `$cmd`; # system($cmd); # &Rndwait(30,45); &FillNewPost(); } sub FillNewPost { # Wait for page reload after selecting an image wait_until { $driver->find_element($config{xpath}{button_next}) }; my $tree = HTML::TreeBuilder::XPath->new_from_content($driver->get_page_source()); # Expand button &ClickXpath($config{xpath}{button_expand}, $tree, 0); # click expand button to show image in full size &Rndwait(2,4); &ClickXpath($config{xpath}{button_next}, $tree); # click next button &Rndwait(5,10); # Caption my $tree2 = HTML::TreeBuilder::XPath->new_from_content($driver->get_page_source()); my $profile = $config{profile_selected}; my $caption; my $hastags = &RndHashtagString(9); if ($config{profile}{$profile}{'filename_as_title'}) { my $filename = basename($config{image_selected}); $filename =~ s/(NO INSTALL)|(SymLink Installer)//g; $filename =~ s/( , )|(\.[^.]+$)//g; $caption = "$filename\n\n" . $hastags; } &ClickXpath($config{xpath}{textarea_caption}, $tree2); &Rndwait(5,6); $driver->send_keys_to_active_element($caption); &Rndwait(5,10); # Share &ClickXpath($config{xpath}{button_share}, $tree2); &Rndwait(20,40); } sub Finish { say 'Sleep and then quit'; &Rndwait(5,5); &AddImageToDB(); &WipeImage() if $config{wipe_after}; $driver->quit(); } sub ClickXpath { my $die = 1; my ($xpath, $tree, $die) = @_; if( $tree->exists($xpath) ) { say "Found: $xpath"; $driver->find_element($xpath)->click(); } else { die "Not found: $xpath" if $die; warn "Not found: $xpath" if !$die; } } sub Rndwait { my ($minimum, $maximum) = @_; # $seconds = $minimum + int(rand($maximum - $minimum)); $seconds = $minimum + rand($maximum - $minimum); $milliseconds = int($seconds * 1000); say "Sleeping for ${milliseconds}ms now..."; $driver->pause($milliseconds); } sub dumpToFile { # &Delimiter((caller(0))[3]); my ($filepath, $ref) = @_; # DATA DUMPER UTF8 HACK # no warnings 'redefine'; local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; local $Data::Dumper::Useperl = 1; open my $FILE, '>:encoding(UTF-8)', $filepath; print $FILE Dumper $ref; close $FILE; } sub Delimiter { my $SubName = shift; print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n"; } sub CheckParameter { &Delimiter((caller(0))[3]); GetOptions ('profile=s' => \$config{'profile_selected'}) or die &PrintUsage(); die &PrintUsage() if !$config{'profile_selected'}; my $profile = $config{'profile_selected'}; if (!exists $config{'profile'}{$profile}) { print "Profile '$config{'profile'}' does not exist.\n"; &PrintUsage(); die; } } sub UndumpFromFile { &Delimiter((caller(0))[3]); my $profile = $config{profile_selected}; if (-e $config{profile}{$profile}{'DBFilepath'}) { my $json = read_file($config{profile}{$profile}{'DBFilepath'}, { binmode => ':raw' }); if (!$json) { warn "DB file $config{'profile'}{$profile}{'DBFilepath'} is empty.\n"; return; } %{$config{db}} = %{ decode_json $json }; print "INFO: $config{'profile'}{$profile}{'DBFilepath'} has " . scalar(keys(%{$config{db}})) . " keys.\n"; } elsif ( !-e $config{'profile'}{$profile}{'DBFilepath'} ) { print "INFO: NO DB file found at $config{'profile'}{$profile}{'DBFilepath'}. Creating now... "; write_file( $config{'profile'}{$profile}{'DBFilepath'}, '' ); &UndumpFromFile(); } } sub PrintUsage { print "Usage: $0 --profile *name*\n"; print "Following profiles are available:\n"; print "* $_\n" for keys( %{$config{'profile'}} ); } sub DirectoryListing { &Delimiter((caller(0))[3]); my $profile = $config{profile_selected}; my @files = glob ( "$config{profile}{$profile}{imageDir}/*" ); %{$config{images_available}} = map { $_ => { 'FILEPATH' => "$_" } } @files; } sub FindNewDataset { &Delimiter((caller(0))[3]); my $i = 0; for my $key (keys %{$config{images_available}}) { if (exists $config{db}{$key}) { print "OLD: $key\n"; } elsif (!exists $config{db}{$key}) { print "NEW: $key\n"; $config{image_selected} = $key; last; } $i++; } if ($i == scalar(keys(%{$config{images_available}}))) { die "\nNO NEW FILES AVAILABLE.\n"; } } sub AddImageToDB { &Delimiter((caller(0))[3]); my $profile = $config{profile_selected}; my $selected = $config{image_selected}; $config{'images_available'}{$selected}{'TIMESTAMP_UPLOADED'} = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec); $config{db}{$selected} = $config{images_available}{$selected}; my $json = encode_json(\%{$config{db}}); write_file($config{profile}{$profile}{'DBFilepath'}, { binmode => ':raw' }, $json); } sub WipeImage { &Delimiter((caller(0))[3]); my $image = $config{image_selected}; print "Deleting $image..."; unlink($image) or die "Could not delete $image!\n"; print " done.\n"; } # SERVER: # java -Dwebdriver.gecko.driver=C:\Users\vstbestprices\Seafile\INSTAFEED_SYNC\selenium\geckodriver.exe -jar C:\Users\vstbestprices\Seafile\INSTAFEED_SYNC\selenium\selenium-server-standalone-3.141.59.jar # java -jar /home/pi/projects/instafeed/selenium/selenium-server-standalone-3.141.59.jar