Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save marcaube/f8c199d206959d36ef76339f87e904df to your computer and use it in GitHub Desktop.
Save marcaube/f8c199d206959d36ef76339f87e904df to your computer and use it in GitHub Desktop.
<?php
//
// AUTO KEYWORD-BASED FOLLOWER CURATION BOT (by @levelsio)
//
// https://gist.github.com/levelsio/6ee6c47283ee414ef3aace1d81986717
//
// File: twitterFollowerCuratorBot.php
//
// Created: May 2021
// License: MIT
//
// Dependency: https://github.com/abraham/twitteroauth via Composer
//
// Installation:
//
// Configure $consumer_key, $consumer_secret, $user_token and $user_token_secret with your Twitter API credentials.
// Configure $keywords with your own unfollow keywords.
//
// Execute:
//
// $> composer require abraham/twitteroauth
// $> php twitterFollowerCuratorBot.php
//
// Schedule/cron: @hourly
//
// 1) This script goes through your new followers and curates them to pre-emptively remove trolls
// 2) It checks their name, bio, location etc. for specific keywords
// 3) If it matches, it blocks them, then quickly unblocks them, which results in them automatically unfollowing you
// 4) This means they won't see your tweets anymore in their timeline
// 5) It also mutes them so you won't see their tweets anymore either
// 6) Extra features are unfollowing based
// a) low follower count (<10)
// b) no profile image
// c) account created in last 30 days
// 7) It avoids removing big followers (>10,000 followers)
// 8) Tip: you can also curate entire cities, for ex I auto remove followers from San Francisco because I think they live in a bubble and their tweets are usually annoying to me
// 9) It saves the amount of people removed or approved in ./blockedCount.txt and ./approvedCount.txt, and saves cache in ./approvedUserIdsCache.json to avoid re-checking the same followers
//
// <config>
// <Twitter app credentials>
$consumer_key = '';
$consumer_secret = '';
$user_token = '';
$user_token_secret = '';
// </Twitter app credentials>
// <keywords to match>
// change this with your own keywords you want to match users on to unfollow
$keywords = [
'biden',
'trump',
'san francisco',
];
// </keywords to match>
// </config>
error_reporting(E_ALL);
require(__DIR__.'/vendor/autoload.php');
use Abraham\TwitterOAuth\TwitterOAuth;
if (!($connection = new TwitterOAuth($consumer_key, $consumer_secret, $user_token, $user_token_secret))) {
die('(!) Cannot connect to Twitter');
}
if (!($response = $connection->get('followers/ids', ['count' => 100]))) {
die('Error: '.json_encode($response));
}
$newlyFollowingUserIds = $response->ids;
if (empty($newlyFollowingUserIds)) {
die("Bad response empty ids: ".json_encode($response));
}
if (!is_file(__DIR__.'/approvedCount.txt')) {
file_put_contents(__DIR__.'/approvedCount.txt', '0');
}
if (!is_file(__DIR__.'/blockedCount.txt')) {
file_put_contents(__DIR__.'/blockedCount.txt', '0');
}
if (!is_file(__DIR__.'/approvedUserIdsCache.json')) {
file_put_contents(__DIR__.'/approvedUserIdsCache.json', '[]');
}
$approvedUserIds = (array)json_decode(file_get_contents(__DIR__.'/approvedUserIdsCache.json'), true);
foreach ($newlyFollowingUserIds as $userId) {
if (in_array($userId, $approvedUserIds)) {
echo $userId." already approved\n";
continue;
}
echo "😴 Sleeping for 5 seconds...\n";
sleep(5);
if (!($user = $connection->get('users/show', ['user_id' => $userId]))) {
die('Error');
}
echo "\n==========================\n";
echo "#".$userId."\n";
echo "name=".$user->name."\n";
echo "screen_name=".$user->screen_name."\n";
echo "description=".$user->description."\n";
echo "location=".$user->location."\n";
echo "followers_count=".$user->followers_count."\n";
echo "default_profile_image=".$user->default_profile_image."\n\n";
$suspiciousUserFlags = 0;
if ($user->default_profile_image) {
$suspiciousUserFlags++;
$blockReason = "they have no profile image set";
}
if ($user->followers_count < 10) {
$suspiciousUserFlags++;
$blockReason = "they have a low follower count <10";
}
if ($user->protected) {
$suspiciousUserFlags++;
$blockReason = "they have a private account";
}
if (strtotime($user->created_at) > strtotime('-30 days')) {
$suspiciousUserFlags++;
$blockReason = "they have an account created in the last 30 days";
}
$userKeywordMatches = [];
foreach ($keywords as $keyword) {
if (
(stripos(str_replace(' ', '', $user->name), $keyword) !== false) ||
(stripos(str_replace(' ', '', $user->screen_name), $keyword) !== false) ||
(stripos(str_replace(' ', '', $user->description), $keyword) !== false) ||
(stripos(str_replace(' ', '', $user->location), $keyword) !== false)
) {
$userKeywordMatches[] = $keyword;
$blockReason = "their name, bio or location has a blocked keyword match";
}
}
// <if big follower, ignore suspicious and keywords>
if ($user->followers_count > 10000) {
$userKeywordMatches = 0;
$suspiciousUserFlags = 0;
}
// </if big follower, ignore suspicious and keywords>
if (!$userKeywordMatches && !$suspiciousUserFlags) {
echo "✅ Approved, adding to approved cache\n";
$approvedUserIds[] = $userId;
file_put_contents(__DIR__.'/approvedCount.txt', (int)file_get_contents(__DIR__.'/approvedCount.txt') + 1);
file_put_contents(__DIR__.'/approvedUserIdsCache.json', json_encode($approvedUserIds));
echo "😴 Sleeping for 5 seconds...\n";
sleep(5);
continue;
}
if ($userKeywordMatches) {
echo "❌ Flagged new follower @".$user->screen_name.", matching on: ".implode(', ', $userKeywordMatches)."\n";
}
if ($suspiciousUserFlags && $user->default_profile_image) {
echo "❌ Flagged new follower @".$user->screen_name.", looks suspect due to no profile image\n";
}
if ($suspiciousUserFlags && $user->followers_count<10) {
echo "❌ Flagged new follower @".$user->screen_name.", looks suspect due to low follower count\n";
}
// <blunble>
echo "❌ Blocking new follower @".$user->screen_name." to make them unfollow\n";
echo json_encode($connection->post('blocks/create', ['user_id' => $userId]))."\n";
echo "😴 Sleeping for 5 seconds...\n";
sleep(5);
echo "❌ Unblocking new follower @".$user->screen_name." to make them unfollow\n";
echo json_encode($connection->post('blocks/destroy', ['user_id' => $userId]))."\n";
// </blunble>
// <mute>
echo "❌ Muting new follower @".$user->screen_name."\n";
echo json_encode($connection->post('mutes/users/create', ['user_id' => $userId]))."\n";
// </mute>
file_put_contents(__DIR__.'/blockedCount.txt', (int)file_get_contents(__DIR__.'/blockedCount.txt') + 1);
echo(
"🧨 Removed new follower https://twitter.com/".$user->screen_name." because ".$blockReason."\n".
"Keyword matches: ".implode(', ', $userKeywordMatches)."\n".
"Follower count: ".$user->followers_count."\n".
"No profile image: ".$user->default_profile_image."\n".
"Blocked count :".number_format(file_get_contents(__DIR__.'/blockedCount.txt'))."\n".
"Approved count :".number_format(file_get_contents(__DIR__.'/approvedCount.txt'))."\n"
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment