├── .editorconfig ├── .github └── workflows │ └── php.yml ├── Command ├── LoginCommand.php └── ShieldCommand.php ├── DependencyInjection └── FacebookExtension.php ├── FacebookBundle.php ├── Resources ├── bin │ └── console └── config │ └── services.yaml ├── Tool.php ├── composer.json └── psalm.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Validate composer.json and composer.lock 18 | run: composer validate 19 | 20 | - name: Cache Composer packages 21 | id: composer-cache 22 | uses: actions/cache@v2 23 | with: 24 | path: vendor 25 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-php- 28 | 29 | - name: Install dependencies 30 | if: steps.composer-cache.outputs.cache-hit != 'true' 31 | run: composer install --prefer-dist --no-progress --no-suggest 32 | 33 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 34 | # Docs: https://getcomposer.org/doc/articles/scripts.md 35 | 36 | # - name: Run test suite 37 | # run: composer run-script test 38 | -------------------------------------------------------------------------------- /Command/LoginCommand.php: -------------------------------------------------------------------------------- 1 | client = new Client(); 26 | 27 | parent::__construct(); 28 | } 29 | 30 | protected function configure(): void 31 | { 32 | $this->setDescription('Login to facebook') 33 | ->setHelp('This command allows you to login to a facebook account...'); 34 | } 35 | 36 | /** 37 | * Sign in to your account. 38 | */ 39 | protected function login(InputInterface $input, OutputInterface $output): void 40 | { 41 | /** @var QuestionHelper */ 42 | $helper = $this->getHelper('question'); 43 | 44 | $question = new Question('Enter email: '); 45 | 46 | $email = (string) $helper->ask($input, $output, $question); 47 | 48 | $question = new Question('Enter password: '); 49 | $question->setHidden(true); 50 | $question->setHiddenFallback(false); 51 | 52 | $password = (string) $helper->ask($input, $output, $question); 53 | 54 | $this->client->request('GET', self::BASE_URL); 55 | 56 | $this->client->submitForm('login', [ 57 | 'email' => $email, 58 | 'pass' => $password, 59 | ]); 60 | 61 | if ($this->filter('#checkpoint_title')) { 62 | if ($this->filter('#approvals_code')) { 63 | $this->nextStep($input, $output); 64 | 65 | return; 66 | } 67 | throw new \RuntimeException('You must enable 2-factor authentication.'); 68 | } 69 | 70 | $output->writeln('Login failed, please re-enter'); 71 | // Re-login 72 | $this->login($input, $output); 73 | } 74 | 75 | /** 76 | * Two-step verification. 77 | */ 78 | protected function nextStep(InputInterface $input, OutputInterface $output, int $failed = 0): void 79 | { 80 | /** @var QuestionHelper */ 81 | $helper = $this->getHelper('question'); 82 | $question = new Question('Enter 2-FA code: '); 83 | 84 | $code = (string) $helper->ask($input, $output, $question); 85 | 86 | if (strlen($code) >= 32) { 87 | $code = (new GoogleAuthenticator())->getCode($code); 88 | } 89 | 90 | $this->client->submitForm('submit[Submit Code]', [ 91 | 'approvals_code' => $code, 92 | ]); 93 | 94 | if ($this->filter('#approvals_code')) { 95 | if (++$failed > 1) { 96 | throw new \RuntimeException('Login failed, you entered incorrectly too many times.'); 97 | } 98 | 99 | $output->writeln('The recovery code is not correct, please re-enter it'); 100 | // Re-enter the recovery code if it is not correct 101 | $this->nextStep($input, $output, $failed); 102 | 103 | return; 104 | } 105 | 106 | $this->endStep(); 107 | } 108 | 109 | /** 110 | * Complete login. 111 | */ 112 | protected function endStep(): void 113 | { 114 | try { 115 | $this->dontSave(); 116 | $this->continue(); 117 | $this->dontSave(); 118 | } catch (\InvalidArgumentException) { 119 | // Finish without checking the browser 120 | } 121 | } 122 | 123 | protected function continue(): void 124 | { 125 | $this->client->submitForm('submit[Continue]'); 126 | $this->client->submitForm('submit[This was me]'); 127 | } 128 | 129 | protected function dontSave(): void 130 | { 131 | $this->client->submitForm('submit[Continue]', [ 132 | 'name_action_selected' => 'dont_save', 133 | ]); 134 | } 135 | 136 | protected function filter(string $input): bool 137 | { 138 | $crawler = $this->client->getCrawler()->filter($input); 139 | 140 | return $crawler->count() > 0; 141 | } 142 | 143 | protected function execute(InputInterface $input, OutputInterface $output): int 144 | { 145 | try { 146 | $this->login($input, $output); 147 | } catch (\RuntimeException $e) { 148 | $output->writeln(''.$e->getMessage().''); 149 | 150 | return Command::FAILURE; 151 | } 152 | 153 | $cookies = $this->client->getCookieJar()->all(); 154 | $cookies = \array_map('strval', $cookies); 155 | $cookies = \json_encode($cookies); 156 | 157 | \file_put_contents(getcwd().DIRECTORY_SEPARATOR.'cookies.json', $cookies); 158 | $output->writeln('Logged in successfully'); 159 | 160 | return Command::SUCCESS; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Command/ShieldCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Activate avatar protection') 25 | ->addArgument('token', InputArgument::REQUIRED, 'Access token') 26 | ->addOption('off', null, InputOption::VALUE_NONE, 'Turn off'); 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output): int 30 | { 31 | try { 32 | /** @var FacebookSession $session */ 33 | $session = new FacebookSession((string) $input->getArgument('token')); 34 | 35 | /** @var GraphObject */ 36 | $graphObj = (new FacebookRequest($session, 'GET', '/me')) 37 | ->execute() 38 | ->getGraphObject(); 39 | $user = $graphObj->asArray(); 40 | } catch (\Throwable $e) { 41 | $output->writeln(''.$e->getMessage().''); 42 | 43 | return Command::FAILURE; 44 | } 45 | 46 | $client = HttpClient::create(); 47 | 48 | $mode = $input->getOption('off') ? false : true; 49 | 50 | $data = [ 51 | [ 52 | 'is_shielded' => $mode, 53 | 'actor_id' => $user['id'], 54 | 'client_mutation_id' => 'b0316dd6-3fd6-4beb-aed4-bb29c5dc64b0', 55 | ], 56 | ]; 57 | 58 | $client->request('POST', 'https://graph.facebook.com/graphql', [ 59 | 'headers' => [ 60 | 'Authorization' => 'OAuth '.(string) $input->getArgument('token'), 61 | ], 62 | 'body' => [ 63 | 'variables' => json_encode($data), 64 | 'doc_id' => '1477043292367183', 65 | ], 66 | ]); 67 | 68 | $output->writeln('Shielded successfully turned '.($mode ? 'on' : 'off').''); 69 | 70 | return Command::SUCCESS; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DependencyInjection/FacebookExtension.php: -------------------------------------------------------------------------------- 1 | load('services.yaml'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /FacebookBundle.php: -------------------------------------------------------------------------------- 1 | add(new LoginCommand()); 25 | $application->add(new ShieldCommand()); 26 | 27 | $application->run(); 28 | -------------------------------------------------------------------------------- /Resources/config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | _instanceof: 4 | Symfony\Component\Console\Command\Command: 5 | tags: ['console.command'] 6 | public: true 7 | 8 | Gingdev\Facebook\Command\: 9 | resource: ../../Command 10 | -------------------------------------------------------------------------------- /Tool.php: -------------------------------------------------------------------------------- 1 | '124024574287414', 18 | 'redirect_uri' => 'fbconnect://success', 19 | 'scope' => 'email,read_insights,read_audience_network_insights,rsvp_event,offline_access,publish_video,openid,catalog_management,user_managed_groups,groups_show_list,pages_manage_cta,pages_manage_instant_articles,pages_show_list,pages_messaging,pages_messaging_phone_number,pages_messaging_subscriptions,read_page_mailboxes,ads_management,ads_read,business_management,instagram_basic,instagram_manage_comments,instagram_manage_insights,instagram_content_publish,publish_to_groups,groups_access_member_info,leads_retrieval,whatsapp_business_management,attribution_read,pages_read_engagement,pages_manage_metadata,pages_read_user_content,pages_manage_ads,pages_manage_posts,pages_manage_engagement,audience_network_placement_management,public_profile', 20 | 'response_type' => 'token', 21 | ]; 22 | 23 | protected string $accessToken = ''; 24 | 25 | protected Client $browser; 26 | 27 | public function __construct(string $filename) 28 | { 29 | $json = \file_get_contents($filename); 30 | if (false === $json) { 31 | throw new \RuntimeException("Unable to load file {$filename}"); 32 | } 33 | 34 | $cookies = \json_decode($json, true); 35 | if (\JSON_ERROR_NONE !== \json_last_error()) { 36 | throw new \InvalidArgumentException('json_decode error: '.\json_last_error_msg()); 37 | } 38 | 39 | if (!\is_array($cookies)) { 40 | throw new \RuntimeException("Invalid cookie file: {$filename}"); 41 | } 42 | 43 | /** @var string[] $cookies */ 44 | $cookieJar = new CookieJar(); 45 | $cookieJar->updateFromSetCookie($cookies); 46 | 47 | $this->browser = new Client(cookieJar: $cookieJar); 48 | } 49 | 50 | public function requestAccessToken(): void 51 | { 52 | $this->browser->request('GET', self::TOKEN_URL.'?'.http_build_query($this->queryData)); 53 | $this->browser->followRedirects(false); 54 | 55 | try { 56 | $form = $this->browser 57 | ->getCrawler() 58 | ->filter('form') 59 | ->form(); 60 | } catch (\InvalidArgumentException) { 61 | throw new \LogicException('Cookies have expired or are not valid.'); 62 | } 63 | 64 | $this->browser->submit($form); 65 | 66 | /** @var Response */ 67 | $resp = $this->browser->getResponse(); 68 | 69 | /** @var string */ 70 | $location = $resp->getHeader('location'); 71 | 72 | parse_str( 73 | parse_url($location, PHP_URL_FRAGMENT), 74 | $data 75 | ); 76 | 77 | $this->accessToken = (string) $data['access_token']; 78 | } 79 | 80 | public function getAccessToken(): string 81 | { 82 | return $this->accessToken; 83 | } 84 | 85 | public function getBrowser(): Client 86 | { 87 | return $this->browser; 88 | } 89 | 90 | public function likePost(string $id, int $mode = 0): bool 91 | { 92 | $crawler = $this->browser->request('GET', 'https://mbasic.facebook.com/reactions/picker/?ft_id='.$id); 93 | 94 | try { 95 | $link = $crawler->filter('a[style="display:block"]')->eq($mode)->link(); 96 | $this->browser->click($link); 97 | } catch (\InvalidArgumentException) { 98 | return false; 99 | } 100 | 101 | return true; 102 | } 103 | 104 | public function likePage(string $id): bool 105 | { 106 | $crawler = $this->browser->request('GET', 'https://mbasic.facebook.com/'.$id.'/about'); 107 | 108 | try { 109 | $link = $crawler->filterXPath('//a[contains(@href, "/a/profile.php")]')->link(); 110 | $this->browser->click($link); 111 | } catch (\InvalidArgumentException) { 112 | return false; 113 | } 114 | 115 | return true; 116 | } 117 | 118 | public function followPage(string $id): bool 119 | { 120 | $crawler = $this->browser->request('GET', 'https://mbasic.facebook.com/'.$id.'/about'); 121 | 122 | try { 123 | $link = $crawler->filter('a[id="pages_follow_action_id"]')->link(); 124 | $this->browser->click($link); 125 | } catch (\InvalidArgumentException) { 126 | return false; 127 | } 128 | 129 | return true; 130 | } 131 | 132 | public function followUser(string $id): bool 133 | { 134 | $crawler = $this->browser->request('GET', 'https://mbasic.facebook.com/'.$id.'/about'); 135 | 136 | try { 137 | $link = $crawler->filterXPath('//a[contains(@href, "/a/subscribe.php")]')->link(); 138 | $this->browser->click($link); 139 | } catch (\InvalidArgumentException) { 140 | return false; 141 | } 142 | 143 | return true; 144 | } 145 | 146 | public function commentPost(string $id, string $message): bool 147 | { 148 | $crawler = $this->browser->request('GET', 'https://mbasic.facebook.com/mbasic/comment/advanced/?target_id='.$id.'&at=compose'); 149 | 150 | try { 151 | $form = $crawler->filter('form')->form(); 152 | $form->remove('photo'); // watch out for this damn thing 153 | $this->browser->submit($form, ['comment_text' => $message]); 154 | } catch (\InvalidArgumentException) { 155 | return false; 156 | } 157 | 158 | return true; 159 | } 160 | 161 | public function sharePost(string $id): bool 162 | { 163 | // Nah, I hope there will be a quicker solution 164 | $crawler = $this->browser->request('GET', 'https://mbasic.facebook.com/'.$id); 165 | 166 | try { 167 | $link = $crawler->filterXPath('//a[contains(@href, "/composer/mbasic")]')->link(); 168 | $this->browser->click($link); 169 | $this->browser->submitForm('view_post'); 170 | } catch (\InvalidArgumentException) { 171 | return false; 172 | } 173 | 174 | return true; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ging-dev/facebook-tool", 3 | "description": "Facebook Tool for PHP", 4 | "type": "symfony-bundle", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "ging-dev", 9 | "email": "gingdz11012001@gmail.com" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "require": { 14 | "php": "^8.0", 15 | "fabpot/goutte": "^4.0", 16 | "symfony/console": "^5.2", 17 | "facebook/graph-sdk": "^4.0", 18 | "sonata-project/google-authenticator": "^2.3" 19 | }, 20 | "require-dev": { 21 | "symfony/http-kernel": "^5.2", 22 | "symfony/dependency-injection": "^5.2", 23 | "symfony/config": "^5.2", 24 | "phpstan/phpstan": "^1.1", 25 | "vimeo/psalm": "^4.18" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Gingdev\\Facebook\\": "" 30 | } 31 | }, 32 | "bin": ["Resources/bin/console"], 33 | "scripts": { 34 | "phpstan": "phpstan analyse --level 6 Command" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------