Статьи

Как создать базовое приложение Twitter Analytics с помощью RestDB

Как создать базовое приложение Twitter Analytics с помощью RestDB

Эта статья была спонсирована RestDB . Спасибо за поддержку партнеров, которые делают возможным использование SitePoint.

Twitter Analytics — это полезный инструмент — он позволяет вам следить за тем, что работает, а что нет, и экспериментировать с подсчетом завидных подписчиков.

Но что, если вы хотите использовать данные из своей учетной записи для проведения анализа, который более соответствует вашим конкретным потребностям? Это не особенно легко — но это возможно. Мы будем использовать RestDB, чтобы помочь нам получать и хранить данные, а также смотреть на то, что происходит с нашими подписчиками.

Вы часто задаетесь вопросом, почему некоторые аккаунты, кажется, следуют за вами только для того, чтобы отписаться от вас через несколько секунд (или дней)? Вероятно, это не то, что вы сказали — они просто занимаются фермерством .

Последовательное фермерство — это известный хакер в социальных сетях, использующий в своих интересах людей, которые «следуют», как только кто-то за ними следует. Крупные бренды, знаменитости и знаменитости, пользующиеся этим, пользуются этим, поскольку их последователи остаются на высоком уровне, но следят за низким, что, в свою очередь, делает их популярными.

Схема логотипа твиттера

В этом посте мы создадим приложение, которое позволит вам войти через Twitter, захватить ваших подписчиков и сравнить список последних выбранных подписчиков с обновленным списком, чтобы определить новых подписчиков и рассчитать продолжительность их подписки, возможно, автоматически -идентификация фермеров.

Бутстрапирование

Как обычно, мы будем использовать Homestead Improved для качественной настройки локальной среды. Не стесняйтесь использовать свои собственные настройки вместо, если у вас есть тот, в котором вы чувствуете себя комфортно.

git clone https://github.com/swader/homestead_improved hi_followfarmers cd hi_followfarmers bin/folderfix.sh vagrant up; vagrant ssh 

Как только виртуальная машина будет подготовлена, и мы окажемся внутри нее, давайте загрузим приложение Laravel .

 composer create-project --prefer-dist laravel/laravel Code/Project cd Code/Project 

Войти через Twitter

Чтобы сделать возможным вход через Twitter, мы будем использовать пакет Socialite .

 composer require laravel/socialite 

Согласно инструкциям, мы также должны зарегистрировать его в config/app.php :

 'providers' => [ // Other service providers... Laravel\Socialite\SocialiteServiceProvider::class, ], 
 'Socialite' => Laravel\Socialite\Facades\Socialite::class, 

Наконец, нам нужно зарегистрировать новое приложение Twitter на http://apps.twitter.com/app/new…

Регистрация нового приложения Twitter

… И добавьте секретные учетные данные в config/services.php :

  'twitter' => [ 'client_id' => env('TWITTER_CLIENT_ID'), 'client_secret' => env('TWITTER_CLIENT_SECRET'), 'redirect' => env('TWITTER_CALLBACK_URL'), ], 

Естественно, нам нужно добавить эти переменные окружения в файл .env в корне проекта:

 TWITTER_CLIENT_ID=keykeykeykeykeykeykeykeykey TWITTER_CLIENT_SECRET=secretsecretsecret TWITTER_CALLBACK_URL=http://homestead.app/auth/twitter/callback 

Нам нужно добавить несколько маршрутов входа в routes/web.php :

 Route::get('auth/twitter', 'Auth\LoginController@redirectToProvider'); Route::get('auth/twitter/callback', 'Auth\LoginController@handleProviderCallback'); 

Наконец, давайте добавим методы, на которые эти маршруты ссылаются, в класс LoginController внутри app/Http/Controllers/Auth :

  /** * Redirect the user to the GitHub authentication page. * * @return Response */ public function redirectToProvider() { return Socialite::driver('twitter')->redirect(); } /** * Obtain the user information from GitHub. * * @return Response */ public function handleProviderCallback() { $user = Socialite::driver('twitter')->user(); dd($user); } 

dd($user); можно ли легко проверить, прошла ли аутентификация хорошо, и, конечно же, если вы посетите /auth/twitter , вы сможете авторизовать приложение и увидеть основную информацию о вашей учетной записи на экране:

Основная информация о пользователе Twitter

Списки подписчиков

Есть много способов получить список подписчиков аккаунта, и ни один из них не приятен.

Twitter по-прежнему ненавидит разработчиков

После Великой войны Twitter с разработчиками (спойлер: с тех пор, как вышла эта статья, мало что изменилось), было полным кошмаром получить полные списки подписчиков людей. На самом деле, ограничения скорости API настолько низки, что люди прибегают к сторонним агрегаторам данных для того, чтобы фактически покупать эти данные или даже чистить страницу подписчиков. Мы пойдем по пути «белой шляпы» и пострадаем от их API, но если у вас есть другие способы получить последователей, не стесняйтесь использовать это вместо метода, описанного ниже.

API Твиттера предлагает конечную точку /followers/list , но так как он возвращает не более 20 подписчиков на вызов не более, и разрешает только 15 запросов в 15 минут, мы могли бы извлечь максимум 1200 подписчиков в час — неприемлемо. Вместо этого мы будем использовать конечную точку followers/ids для получения 5000 идентификаторов одновременно. Это подлежит тому же пределу 15 звонков в 15 минут, но дает нам гораздо больше передышки.

Важно помнить, что ID! = Ручка Twitter. Идентификаторы — это числовые значения, представляющие уникальную учетную запись во времени, даже в разных дескрипторах. Поэтому для каждого идентификатора отписавшегося нам нужно будет выполнить дополнительный вызов API, чтобы выяснить, кем они были (пригодится API Bulk Users Lookup ).

Основы API-интерфейса

Socialite полезен только для входа в систему. На самом деле общаться с API менее просто. Учитывая, что Laravel поставляется с предварительно установленной Guzzle, установка подписчика Oauth Guzzle (который позволяет нам использовать Guzzle с протоколом Oauth1) является самым простым решением:

 composer require guzzlehttp/oauth-subscriber 

Как только это LoginController::handleProviderCallback , мы можем обновить наш LoginController::handleProviderCallback чтобы проверить это:

  public function handleProviderCallback() { $user = Socialite::driver('twitter')->user(); $stack = HandlerStack::create(); $middleware = new Oauth1([ 'consumer_key' => getenv('TWITTER_CLIENT_ID'), 'consumer_secret' => getenv('TWITTER_CLIENT_SECRET'), 'token' => $user->token, 'token_secret' => $user->tokenSecret ]); $stack->push($middleware); $client = new Client([ 'base_uri' => 'https://api.twitter.com/1.1/', 'handler' => $stack, 'auth' => 'oauth' ]); $response = $client->get('followers/ids.json', [ 'query' => [ 'cursor' => '-1', 'screen_name' => $user->nickname, 'count' => 5000 ] ]); dd($response->getBody()->getContents()); } 

В приведенном выше коде мы сначала создаем стек промежуточного программного обеспечения, который будет обрабатывать запрос, извлекать его через все промежуточное программное обеспечение и выводить окончательную версию. Мы можем вставить другие промежуточные программы в этот стек, но пока нам нужен только Oauth1.

Затем мы создаем промежуточное программное обеспечение Oauth1 и передаем необходимые параметры. Первые два у нас уже есть — это ключи, которые мы определили в .env ранее. Последние два мы получили от аутентифицированного пользовательского экземпляра Twitter.

Затем мы помещаем промежуточное ПО в стек и прикрепляем стек к клиенту Guzzle. С точки зрения непрофессионала, это означает, что «когда этот клиент делает запросы, протяните запросы через все промежуточные программы в стеке, прежде чем отправлять их в конечный пункт назначения». Мы также говорим клиенту всегда аутентифицироваться с oauth.

Наконец, мы выполняем GET-вызов к конечной точке API с необходимыми параметрами запроса: страница, с которой нужно начинать (-1 — первая страница), пользователь, для которого нужно тянуть подписчиков, и сколько подписчиков нужно тянуть. В конце мы выводим этот вывод на экран, чтобы увидеть, получаем ли мы то, что нам нужно. Конечно же, вот 5000 самых последних подписчиков для моего аккаунта:

Снимок экрана с 5000 идентификаторов пользователей Twitter

Теперь, когда мы знаем, что наши вызовы API проходят, и мы можем общаться с Twitter, пришло время некоторым циклам получить полный список для текущего пользователя.

PHP сторона — получение всех последователей

Поскольку через API разрешено 15 вызовов в течение 15 минут, давайте пока ограничим размер аккаунта до 70 000 подписчиков для простоты.

  $user = Socialite::driver('twitter')->user(); if ($user->user['followers_count'] > 70000) { return view( 'home.index', ['message' => 'Sorry, we currently only support accounts with up to 70k followers'] ); } 

Примечание: home.index — это произвольный файл представления, который я создал только для этого примера и содержащий одну директиву: {{ $message }} .

Затем давайте next_cursor_string значению next_cursor_string возвращенному API, и next_cursor_string на страницы с помощью других идентификаторов.

Вау, много цифр, очень следите, вау.

Много цифр, очень следите, вау.

Если повезет, это должно выполняться очень быстро — в зависимости от отзывчивости API Twitter.

Каждый, у кого до 70 тыс. Подписчиков, теперь может получить полный список подписчиков, созданный после авторизации.

Если бы нам нужно было поддерживать большие учетные записи, было бы относительно просто заставить его повторять процесс каждые 15 минут (после сброса лимита API) для каждых 75 000 подписчиков и сшивать результаты вместе. Конечно, кому-то почти гарантировано следовать / отписываться в этом окне, учитывая количество подписчиков, поэтому было бы очень трудно оставаться точным. В этих случаях проще сосредоточиться на последних 75 000 подписчиков и анализировать только их (автоматическое упорядочение API по последним подписчикам) или найти другой способ надежного извлечения подписчиков, минуя API.

Убираться

Немного неловко иметь эту логику в контроллере LoginController, поэтому давайте перенесем это в отдельный сервис. Я создал app/Services/Followers/Followers.php для этого примера со следующим содержанием:

 <?php namespace App\Services\Followers; use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Subscriber\Oauth\Oauth1; class Followers { /** @var string */ protected $token; /** @var string */ protected $tokenSecret; /** @var string */ protected $nickname; /** @var Client */ protected $client; public function __construct(string $token, string $tokenSecret, string $nickname) { $this->token = $token; $this->tokenSecret = $tokenSecret; $this->nickname = $nickname; $stack = HandlerStack::create(); $middleware = new Oauth1( [ 'consumer_key' => getenv('TWITTER_CLIENT_ID'), 'consumer_secret' => getenv('TWITTER_CLIENT_SECRET'), 'token' => $this->token, 'token_secret' => $this->tokenSecret, ] ); $stack->push($middleware); $this->client = new Client( [ 'base_uri' => 'https://api.twitter.com/1.1/', 'handler' => $stack, 'auth' => 'oauth', ] ); } public function getClient() { return $this->client; } /** * Returns an array of follower IDs for a given optional nickname. * * If no custom nickname is provided, the one used during the construction * of this service is used, usually defaulting to the same user authing * the application. * * @param string|null $nickname * @return array */ public function getFollowerIds(string $nickname = null) { $nickname = $nickname ?? $this->nickname; $response = $this->client->get( 'followers/ids.json', [ 'query' => [ 'cursor' => '-1', 'screen_name' => $nickname, 'count' => 5000, ], ] ); $data = json_decode($response->getBody()->getContents()); $ids = $data->ids; while ($data->next_cursor_str !== "0") { $response = $this->client->get( 'followers/ids.json', [ 'query' => [ 'cursor' => $data->next_cursor_str, 'screen_name' => $nickname, 'count' => 5000, ], ] ); $data = json_decode($response->getBody()->getContents()); $ids = array_merge($ids, $data->ids); } return $ids; } } 

Затем мы можем очистить метод handleProviderCallback handleProviderCallback :

  public function handleProviderCallback() { $user = Socialite::driver('twitter')->user(); if ($user->user['followers_count'] > 70000) { return view( 'home.index', ['message' => 'Sorry, we currently only support accounts with up to 70k followers'] ); } $flwrs = new Followers( $user->token, $user->tokenSecret, $user->nickname ); dd($flwrs->getFollowerIds()); } 

Это все еще неправильный способ делать это, поэтому давайте продолжим улучшать вещи. Чтобы сохранить пользователя в системе, давайте сохраним токен, секрет и псевдоним в сеансе.

  /** * Get and store token data for authorized user. * * @param Request $request * @return Response */ public function handleProviderCallback(Request $request) { $user = Socialite::driver('twitter')->user(); if ($user->user['followers_count'] > 70000) { return view( 'home.index', ['message' => 'Sorry, we currently only support accounts with up to 70k followers'] ); } $request->session()->put('twitter_token', $user->token); $request->session()->put('twitter_secret', $user->tokenSecret); $request->session()->put('twitter_nickname', $user->nickname); $request->session()->put('twitter_id', $user->id); return redirect('/'); } 

Мы сохраняем всю информацию в сеансе, чтобы пользователь эффективно вошел в наше приложение, а затем перенаправили на домашнюю страницу.

Давайте создадим новый контроллер сейчас и дадим ему простой способ использования:

 artisan make:controller HomeController 
 <?php namespace App\Http\Controllers; use Illuminate\Http\Request; class HomeController extends Controller { public function index(Request $request) { $nick = $request->session()->get('twitter_nickname'); if (!$nick) { return view('home.loggedout'); } return view('home.index', $request->session()->all()); } } 

Просто, правда? Взгляды тоже просты:

 {{--index.blade.php--}} <h1>FollowerFarmers</h1> <h2>Hello, {{ $twitter_nickname }}! Not you? <a href="/logout">Log out!</a></h2> <p>I bet you'd like to see your follower stats, wouldn't you?</p> 
 {{--loggedout.blade.php--}} <h1>FollowerFarmers</h1> <h2>Hello, stranger!</h2> <p>You're currently logged out. How about you <a href="/auth/twitter">log in with Twitter </a> to get started?</p> 

Нам также нужно добавить несколько маршрутов в routes/web.php :

 Route::get('/', 'HomeController@index'); Route::get('/logout', 'Auth\LoginController@logout'); 

При этом мы можем проверить, вошли ли мы в систему, и мы можем легко выйти.

Обратите внимание, что для безопасности маршрут выхода из системы должен принимать только запросы POST с токенами CSRF — для простоты в процессе разработки мы используем подход GET и обновляем его позже.

Следует признать, что это не самая красивая вещь, на которую можно смотреть, но мы создаем здесь демонстрацию — реальная вещь может быть визуально отшлифована, как только логика будет завершена.

Регистрация поставщика услуг

Обычно для упрощения доступа регистрируется поставщик услуг, поэтому давайте сделаем это. Наш сервис не может быть создан без токена и секрета (т. Е. До того, как пользователь войдет в систему с помощью Twitter), поэтому нам нужно будет отложить его — другими словами, он будет создан только при необходимости, и мы сделаем конечно, нам это не нужно, пока у нас не появятся эти ценности.

 artisan make:provider FollowerServiceProvider 
 <?php namespace App\Providers; use App\Services\Followers\Followers; use Illuminate\Support\ServiceProvider; class FollowerServiceProvider extends ServiceProvider { protected $defer = true; public function register() { $this->app->singleton( Followers::class, function ($app) { return new Followers( session('twitter_token'), session('twitter_secret'), session('twitter_nickname') ); } ); } public function provides() { return [Followers::class]; } } 

Если мы добавим простое эхо подсчета в наше зарегистрированное представление:

 {{ count($ids) }} 

… И измените HomeController чтобы теперь использовать этот ServiceProvider:

 ... return view( 'home.index', array_merge( $request->session()->all(), ['ids'=> resolve(Followers::class)->getFollowerIds()] ) ); 

… а затем мы проверяем, конечно же, это работает.

Основные виды

База данных

Теперь, когда у нас есть удобный сервис для извлечения списков подписчиков, мы, вероятно, должны их где-то сохранить. Мы могли бы сохранить это в локальной базе данных MySQL или даже в простом файле, но для производительности и переносимости на этот раз я выбрал нечто иное: RestDB .

RestDB — это сервис размещаемых баз данных, работающий по принципу «подключи и работай», который легко настраивать и использовать, освобождая ваш выбор платформы хостинга. Не нуждаясь в базе данных, которая пишет в локальную файловую систему, вы можете легко отправить приложение, подобное тому, которое мы создаем, в Google Cloud Engine или Heroku. С помощью его шаблонов вы можете мгновенно настроить блог, целевую страницу, веб-форму, анализатор журналов, даже почтовую систему — черт, сервис даже поддерживает MarkDown для редактирования встроенных полей, позволяя практически иметь MarkDown блог прямо там на их сервисе.

RestDB имеет бесплатный уровень, и первый месяц практически безграничен, так что вы можете тщательно его протестировать. База данных, на которой я разрабатываю это, находится на базовом плане (любезно предоставлено командой RestDB).

Настройка RestDB

В отличие от других служб баз данных, с RestDB важно учитывать ограничения по количеству записей. Базовый план предлагает 10000 записей, которые были бы быстро исчерпаны, если бы мы сохранили подписчика каждого вошедшего в систему пользователя как отдельную запись, или даже список подписчиков для каждого пользователя в виде отдельной записи за 15-минутный период. Вот почему я выбрал следующий план:

  • каждый новый пользователь будет записан в коллекции accounts .
  • каждый новый список подписчиков будет записан в коллекции follower-lists и будет дочерней записью accounts .
  • с максимальной скоростью каждые 15 минут (или больше, если пользователю требуется больше времени, чтобы вернуться и войти в приложение), будет создан новый список по сравнению с последним, и новый список вместе с разницей в сторону последнего будут сохранены.
  • каждый пользователь сможет хранить не более 100 историй

Тем не менее, давайте создадим новую коллекцию follower-lists в соответствии с документами для быстрого запуска . Как только коллекция будет создана, давайте добавим несколько полей:

  • обязательное поле для followers . Текстовое поле поддерживает проверки регулярных выражений, и поскольку мы собираемся использовать список, разделенный запятыми, для хранения идентификаторов подписчиков, мы можем применить регулярное выражение, подобное этому, чтобы убедиться, что данные всегда действительны: ^(\d+,\s?)*(\d+)$ . Это будет соответствовать только строкам с разделенными запятыми цифрами, но без запятой. Вы можете увидеть это в действии здесь .
  • поле diff_new текстового типа, которое будет содержать список новых подписчиков со времени последней записи. Будет применено то же ограничение регулярных выражений, что и для followers , только обновленное, чтобы быть необязательным, потому что иногда не будет никакой разницы по сравнению с последней записью: (^(\d+,\s?)*(\d+)$)? ,
  • поле diff_gone текстового типа, которое будет содержать список отписавшихся со времени последней записи. Будет применено то же ограничение регулярного выражения, что и для diff_new .

Наша коллекция должна выглядеть так:

Коллекция последователей

Теперь давайте создадим родительскую коллекцию: accounts .

Примечание: вас может удивить, почему мы не просто используем встроенную коллекцию users . Это потому, что эта коллекция предназначена только для аутентификации пользователей Auth0 . Поля, которые там есть, были бы полезны для нас, но, согласно документам, у нас нет прав на запись в эту базу данных, и нам это нужно. Так почему бы просто не пойти с Auth0 для входа в систему и RestDB для данных? Не стесняйтесь использовать этот подход — лично я чувствую, что достаточно зависеть от одного стороннего сервиса, потому что для меня важна важная часть моего приложения, а два — слишком много для меня.

Нам нужны следующие поля:

  • twitter_id , идентификатор учетной записи Twitter пользователя. Обязательный номер .
  • settings , обязательное поле JSON . Это будет содержать все настройки учетной записи пользователя, такие как интервал обновления, частота отправки электронной почты и т. Д.

После их добавления давайте добавим новое поле follower_lists и определим его как дочернее отношение к нашей коллекции follower-lists . В разделе Properties мы должны выбрать «child of…». Присвоение имен немного сбивает с толку — несмотря на то, что опция говорит «потомок списков последователей», именно follower-lists являются потомками.

Вы, возможно, заметили, что мы нигде не использовали поля меток времени, например, created_at . Это потому, что RestDB автоматически создает их для каждой коллекции вместе с некоторыми другими полями. Чтобы просмотреть эти системные поля , нажмите «Показать системные поля» в правом верхнем углу таблицы настроек каждой коллекции:

Системные поля показаны

Получение этих полей в полезной нагрузке при запросе к базе данных требует от нас использования параметра ?metafields=true в URL-адресах API.

Теперь мы готовы начать объединение сторон PHP и RestDB.

Сохранение и чтение из RestDB

Чтобы иметь возможность взаимодействовать с RestDB, нам нужен ключ API. Мы можем получить это, следуя инструкциям здесь . Все параметры следует оставить со значением по умолчанию, все методы REST должны быть включены. Затем ключ должен быть сохранен в .env :

 RESTDB_KEY=keykeykey 

Идея для accounts заключается в следующем:

  • когда пользователь впервые авторизует Twitter, приложение будет считывать коллекцию учетных записей для предоставленного идентификатора Twitter, а если его нет, оно будет писать новую запись.
  • Затем пользователь перенаправляется на экран приветствия, который будет содержать сообщение с подтверждением создания учетной записи, если оно было создано, и предложение перенаправить на /dashboard .

Давайте сначала создадим сервис RestDB для общения с базой данных.

 <?php // Services/Followers/RestDB.php namespace App\Services\Followers; use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; use Psr\Http\Message\ResponseInterface; class RestDB { /** @var ClientInterface */ protected $client; /** * Sets the Guzzle client to be used * * @param ClientInterface $client * @return $this */ public function setClient(ClientInterface $client) { $this->client = $client; return $this; } /** * @return ClientInterface */ public function getClient() { return $this->client; } /** * Configures a default Guzzle client so it doesn't need to be injected * @return $this */ public function setDefaultClient() { $client = new Client([ 'base_uri' => 'https://followerfarmers-00df.restdb.io/rest/', 'headers' => [ 'x-apikey' => getenv('RESTDB_KEY'), 'content-type' => 'application/json' ] ]); $this->client = $client; return $this; } /** * Returns user's account entry if it exists. Caches result for 5 minutes * unless told to be `$fresh`. * * @param int $twitter_id * @param bool $fresh * @return bool|\stdClass */ public function userAccount(int $twitter_id, bool $fresh = false) { /** @var ResponseInterface $request */ $response = $this->client->get( 'accounts', [ 'body' => '{"twitter_id": ' . $twitter_id . ', "max": 1}', 'query' => ['metafields' => true], 'headers' => ['cache-control' => $fresh ? 'no-cache' : 'max-age:300'], ] ); $bodyString = json_decode($response->getBody()->getContents()); if (empty($bodyString)) { return false; } return $bodyString[0]; } /** * Creates a new account in RestDB. * * @param array $user * @return bool */ public function createUserAccount(array $user) { /** @var ResponseInterface $request */ $response = $this->client->post('accounts', [ 'body' => json_encode([ 'twitter_id' => $user['id'], 'settings' => array_except($user, 'id') ]), 'headers' => ['cache-control' => 'no-cache'] ]); return $response->getStatusCode() === 201; } } 

В этом сервисе мы определяем способы установки клиента Guzzle, а также ярлык для определения по умолчанию. Этот по умолчанию также включает заголовок авторизации по умолчанию и устанавливает тип контента как JSON, с которым мы общаемся. Мы также демонстрируем базовое чтение и запись от и до RestDB.

Метод userAccount непосредственно ищет идентификатор Twitter в записях accounts записей и возвращает запись, если найден, или false, если нет. Обратите внимание на использование параметра запроса метаполя — это позволяет нам извлекать _created и другие системные поля . Также обратите внимание, что мы кешируем результат в течение 5 минут, если не передан параметр $fresh , потому что информация о пользователе будет редко меняться, и нам может понадобиться это несколько раз во время сеанса. Метод createUserAccount принимает массив пользовательских данных (наиболее важным из которых является ключ id ) и создает учетную запись. Обратите внимание, что мы ищем статус 201 что означает CREATED .

Давайте также сделаем ServiceProvider и зарегистрируем сервис как одиночный.

 artisan make:provider RestdbServiceProvider 
 <?php namespace App\Providers; use App\Services\Followers\RestDB; use Illuminate\Support\ServiceProvider; class RestdbServiceProvider extends ServiceProvider { /** * Register the application services. * * @return void */ public function register() { $this->app->singleton( 'restdb', function ($app) { $r = new RestDB(); $r->setDefaultClient(); return $r; } ); } } 

Наконец, давайте обновим наш LoginController.

  // ... $request->session()->put('twitter_id', $user->id); $rest = resolve('restdb'); if (!$rest->userAccount($user->id)) { if ($rest->createUserAccount( [ 'token' => $user->token, 'secret' => $user->tokenSecret, 'nickname' => $user->nickname, 'id' => $user->id, ] )) { $request->session()->flash( 'info', 'Your account has been created! Welcome!' ); } else { $request->session()->flash( 'error', 'Failed to create your account :(' ); } } // ... return redirect('/'); 

В LoginController handleProviderCallback метода handleProviderCallback мы сначала получаем (решаем) службу, используем ее, чтобы проверить, есть ли у пользователя учетная запись, создаем ее, если нет, и переносим сообщение в сеанс, если оно прошло успешно или нет.

Давайте поместим эти флеш-сообщения в представление:

 {{--index.blade.php--}} @isset($info) <p>{{ $info }}</p> @endisset @isset($error) <p>{{ $error }}</p> @endisset ... 

Если мы проверим это, то наша новая запись будет создана:

Новая учетная запись создана

Теперь давайте предложим /dashboard . Идея заключается в следующем:

  • когда пользователь входит в систему, ему будет предоставлена ​​ссылка «Панель инструментов».
  • Перейдя по этой ссылке, вы получите:
    • захватить их последнюю запись в follower-lists подписчиков из RestDB
    • если с момента создания последней записи прошло более 15 минут или у пользователя вообще нет записи, будет получен новый список подписчиков. Новый список будет сохранен. Если это была не первая запись, генерируется разница для новых подписчиков и отписчиков.
    • если пользователь обновился за последние 15 минут, он будет просто перенаправлен на панель инструментов.
  • когда пользователь получает доступ к этой панели, все его записи RestDB-списков выбираются
  • приложения просматривают все записи diff в записях и генерируют отчеты для отписчиков, отображая информацию о том, как долго они следили за пользователем, прежде чем уйти.
  • как только эти идентификаторы для отчета были получены, их информация выбирается через конечную точку /users/lookup чтобы получить их аватары и маркеры Twitter.
  • если учетная запись отслеживалась в течение дня или менее, она помечается красным цветом, что означает высокую степень уверенности в том, что подписчик занимается фермерством. 1 — 5 дней — оранжевый, 5 — 10 дней — желтый, другие — нейтральные.

Давайте сначала обновим индексное представление и добавим новый маршрут.

 // routes/web.php Route::get('/dashboard', 'HomeController@dashboard'); 
 {{--index.blade.php--}} ... <p>I bet you'd like to see your follower stats, wouldn't you?</p> Go to <a href="/dashboard">dashboard</a>. 

Нам нужен способ получить последнюю запись follower_lists пользователя. Таким образом, в сервис RestDB мы можем добавить следующий метод:

  /** * Get the last follower_lists entry of the user in question, or false if * none exists. * * @param int $twitter_id * @return bool|\stdClass */ public function getUsersLastEntry(int $twitter_id) { $id = $this->userAccount($twitter_id)->_id; /** @var ResponseInterface $request */ $response = $this->client->get( 'accounts/' . $id . '/follower_lists', [ 'query' => [ 'metafields' => true, 'sort' => '_id', 'dir' => -1, 'max' => 1, ], 'headers' => ['cache-control' => 'no-cache'], ] ); $bodyString = json_decode($response->getBody()->getContents()); return !empty($bodyString) ? $bodyString[0] : false; } 

Мы либо возвращаем false, либо последнюю запись. Обратите внимание, что мы сортируем по _id _id, от самого нового до самого старого ( dir=-1 ), и выбираем максимум 1 запись. Эти параметры все объясняются здесь .

Теперь давайте обратим наше внимание на метод dashboard в HomeController :

  public function dashboard(Request $request) { $twitter_id = $request->session()->get('twitter_id', 0); if (!$twitter_id) { return redirect('/'); } /** @var RestDB $rest */ $rest = resolve('restdb'); $lastEntry = $rest->getUsersLastEntry($twitter_id); if ($lastEntry) { $created = Carbon::createFromTimestamp( strtotime($lastEntry->_created) ); $diff = $created->diffInMinutes(Carbon::now()); } if ((isset($diff) && $diff > 14) || !$lastEntry) { $followerIds = resolve(Followers::class)->getFollowerIds(); $rest->addFollowerList($followerIds, $lastEntry, $twitter_id); } dd("Let's show all previous lists"); } 

Хорошо, так что здесь происходит? Во-первых, мы делаем примитивную проверку, если пользователь все еще вошел в систему — twitter_id должен быть в сеансе. Если нет, мы перенаправляем на домашнюю страницу. Затем мы выбираем службу Rest, получаем последнюю запись в списках подписчиков аккаунта (которая является либо объектом, либо false ), а затем, если она существует, мы вычисляем, сколько ей лет. Если это более 14 минут, или если запись вообще не существует (то есть это самая первая запись для этой учетной записи), мы получаем новый список подписчиков и сохраняем его. Как мы можем сохранить это? Добавив новый метод addFollowerList в службу Rest.

  /** * Adds a new follower_lists entry to an account entry * * @param array $followerIds * @param \stdClass|bool $lastEntry * @param int $twitter_id * @return bool * @internal param array $newEntry */ public function addFollowerList( array $followerIds, $lastEntry, int $twitter_id ) { $account = $this->userAccount($twitter_id); $newEntry = ['followers' => implode(', ', $followerIds)]; if ($lastEntry !== false) { $lastFollowers = array_map( function ($el) { return (int)trim($el); }, explode(',', $lastEntry->followers) ); sort($lastFollowers); sort($followerIds); $newEntry['diff_gone'] = implode( ', ', array_diff($lastFollowers, $followerIds) ); $newEntry['diff_new'] = implode( ', ', array_diff($followerIds, $lastFollowers) ); } try { /** @var ResponseInterface $request */ $response = $this->client->post( 'accounts/' . $account->_id . '/follower_lists', [ 'body' => json_encode($newEntry), 'headers' => ['cache-control' => 'no-cache'], ] ); } catch (ClientException $e) { // Log the exception message or something } return $response->getStatusCode() === 201; } 

Этот первый захватывает учетную запись пользователя, чтобы найти идентификатор записи учетной записи в RestDB. Затем он инициирует переменную $newEntry с правильно отформатированной (развернутой) строкой текущих идентификаторов подписчика. Далее, если была последняя запись, мы:

  • получить эти идентификаторы в правильный массив, взорвав строку и очистив пробелы.
  • сортировка как текущих, так и прошлых массивов последователей для более эффективного сравнения.
  • получите различия и добавьте их в $newEntry .

Затем мы сохраняем запись, ориентируясь на конкретную запись учетной записи с ранее извлеченным идентификатором, и продолжаем переходить во вложенную коллекцию follower_lists .

Чтобы проверить это, мы можем подделать некоторые данные. Давайте изменим часть $followerIds HomeController::dashboard следующим образом:

  $count = rand(50, 75); $followerIds = []; while ($count--) { $flw = rand(1, 100); if (in_array($flw, $followerIds)) $count++; else $followerIds[] = $flw; } 

Это будет генерировать 50-75 случайных чисел в диапазоне от 1 до 100. Достаточно для нас, чтобы получить некоторые различия. Если мы вошли в систему по адресу url /dashboard во время входа в систему, мы должны получить нашу начальную запись.

Начальная запись

Если мы уберем 15-минутный лимит из блока if и обновим еще два раза, мы сгенерируем всего 3 записи с хорошими разностями:

3 записи с различий

Пришло время для финальной функции. Давайте проанализируем записи и определим некоторых последователей-фермеров.

Финальная растяжка

Поскольку это имеет смысл в контексте, мы поместим эту логику в службу Followers . Давайте создадим метод analyzeUnfollowers . Он будет принимать произвольное количество записей и выполнять свою логику в цикле для всех из них. Затем, если позже мы захотим предоставить более быстрый способ проверки последнего бита информации со времени последнего сеанса входа в систему, мы можем просто пропустить две последние записи вместо всех, и логика останется прежней.

  public function analyzeUnfollowers(array $entries) { ... } 

Чтобы выявить отписавшихся, мы рассмотрим самый последний diff_gone , для всех, кто пропал с момента последней проверки нашего списка подписчиков, а затем найдем их в массивах diff_new предыдущих записей. Это позволяет нам узнать, как долго они следили за нами, прежде чем уйти. При использовании записей нам также нужно превратить записи diff_gone и diff_new в массивы, чтобы их было легче искать.

  /** * Accepts an array of entries (stdObjects) ordered from newest to oldest. * The objects must have the properties: diff_gone, diff_new, and followers, * all of which are comma delimited strings of integers, or arrays of integers. * The property `_created` is also essential. * * @param array $entries * @return array */ public function analyzeUnfollowers(array $entries) { $periods = []; $entries = array_map( function ($entry) { if (is_string($entry->diff_gone)) { $entry->diff_gone = $this->intArray($entry->diff_gone); } if (is_string($entry->diff_new)) { $entry->diff_new = $this->intArray($entry->diff_new); } return $entry; }, $entries ); $latest = array_shift($entries); for ($i = 0; $i < count($entries); $i++) { $cur = $entries[$i]; $curlast = array_last($entries) === $cur; if ($curlast) { $matches = $latest->diff_gone; } else { $matches = array_intersect( $cur->diff_new, $latest->diff_gone ); } if ($matches) { $periods[] = [ 'matches' => array_values($matches), 'from' => (!$curlast) ? Carbon::createFromTimestamp(strtotime($cur->_created)) : 'forever', 'to' => Carbon::createFromTimestamp(strtotime($latest->_created)) ]; } } return $periods; } /** * Turns a string of comma separated values, spaces or no, into an array of integers * * @param string $string * @return array */ protected function intArray(string $string): array { return array_map( function ($el) { return (int)trim($el); }, explode(',', $string) ); } 

Конечно, нам нужен способ получить все записи списка подписчиков. Мы getUserEntries метод getUserEntries в сервис Rest :

  /** * Gets a twitter ID's full list of follower list entries * * @param int $twitter_id * @return array */ public function getUserEntries(int $twitter_id): array { $id = $this->userAccount($twitter_id)->_id; /** @var ResponseInterface $request */ $response = $this->client->get( 'accounts/' . $id . '/follower_lists', [ 'query' => [ 'metafields' => true, 'sort' => '_id', 'dir' => -1, 'max' => 100, ], 'headers' => ['cache-control' => 'no-cache'], ] ); $bodyString = json_decode($response->getBody()->getContents()); return !empty($bodyString) ? $bodyString : []; } 

Вполне возможно, что количество подписчиков в некоторых учетных записях приведет к большим загрузкам, что замедлит работу приложения. Поскольку нам действительно нужны только поля diff, мы можем нацеливать только те поля с параметром h , как описано в нижней части этой страницы .

Затем, если мы в целях отладки dashboard метод dashboard

  $entries = $rest->getUserEntries($twitter_id); dd($followers->analyzeUnfollowers($entries)); 

Вывод выглядит примерно так. Очевидно, что 5 наших фальшивых подписчиков следили за нами только в течение 5 секунд, в то время как остальные следили за нами еще до того, как мы подписались на эту услугу (то есть forever ).

Проанализировано

Наконец, мы можем проанализировать периоды, которые мы получили, — легко определить короткие и раскрасить их, как описано в начале этого поста. Поскольку это уже довольно значительный пост, я оставлю эту часть и часть, посвященную использованию API поиска пользователей в Twitter, чтобы превратить идентификаторы в дескрипторы пользователей в качестве домашней работы. Подсказка: если у вас заканчиваются запросы запросов для этой части, вы можете сканировать их мини-профиль с параметром user_id !

Вывод

Мы прошли процесс создания простого приложения для отслеживания количества времени, за которым данный человек следил за вами, и пометили его как фермера-последователя, и все это без использования локальной базы данных — RestDB обеспечил нам исключительную производительность, масштабируемость и независимость от местных служб.

Мы можем применить множество обновлений к этому приложению:

  • cronjob для автоматического обновления списков подписчиков за кулисами
  • интенсивное кэширование для сохранения вызовов API и увеличения скорости
  • подписка на премиум-аккаунт, которая позволит пользователям хранить больше записей
  • панель инструментов, совмещающая твиты с отписками, показывающая, что, возможно, побудило кого-то покинуть твиттерсферу
  • поддержка нескольких учетных записей
  • Поддержка Instagram

О каких других обновлениях этой системы вы можете подумать? Не стесняйтесь вносить свой вклад в приложение на Github !