Skip to content

Commit

Permalink
Merge pull request #21 from nrutman/IS-16
Browse files Browse the repository at this point in the history
IS-16 Begins a sync:configure command
  • Loading branch information
nrutman authored Nov 15, 2019
2 parents 5dfe95e + 26153ff commit cb4e5ae
Show file tree
Hide file tree
Showing 8 changed files with 470 additions and 137 deletions.
5 changes: 3 additions & 2 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
$googleDomain: '%google.domain%'
$varPath: '%kernel.var_dir%'

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Expand All @@ -28,8 +31,6 @@ services:
App\Client\Google\GoogleClient:
arguments:
$googleConfiguration: '%google.authentication%'
$googleDomain: '%google.domain%'
$varPath: '%kernel.var_dir%'

App\Command\RunSyncCommand:
arguments:
Expand Down
14 changes: 14 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,23 @@ Included in the `config` folder is a `parameters.yml.dist` file. Complete the fo
1. Copy this file and rename it `parameters.yml`.
2. Fill in all of the tokens with configuration for Planning Center and Google.
3. Make sure the `lists` parameter is completed with the lists to sync from Planning Center into G Suite.
4. Run `bin/console sync:configure` to get a Google G Suite token.

## Usage

### Sync:Configure
To configure the command by provisioning a token with your Google G Suite user, run the following command:
```bash
bin/console sync:configure
```
The command will provide a Google authentication URL which will require you to login with a G Suite Groups administrator and paste the provided the access token back to the command. If a valid token has already been provided, the command will exit gracefully.

| Parameter | Description |
| --------- | ----------- |
| --force | Forces the command to overwrite an existing Google token. |

**Note:** the resulting Google token is stored in the `var/google-token.json` file. If at any time you have problems with Google authentication, delete this file and rerun the `sync:configure` command (or use the `--force` parameter).

### Sync:Run
To sync contacts between lists, simply run the following command:
```bash
Expand Down
142 changes: 91 additions & 51 deletions src/Client/Google/GoogleClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
use App\Client\ReadableListClientInterface;
use App\Client\WriteableListClientInterface;
use App\Contact\Contact;
use App\File\FileProvider;
use Google_Client;
use Google_Exception;
use Google_Service_Directory;
use Google_Service_Directory_Member;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;

class GoogleClient implements ReadableListClientInterface, WriteableListClientInterface
{
Expand All @@ -21,92 +24,105 @@ class GoogleClient implements ReadableListClientInterface, WriteableListClientIn
/** @var array */
protected $configuration;

/** @var string */
protected $domain;

/** @var FileProvider */
protected $fileProvider;

/** @var Google_Service_Directory */
protected $service;

/** @var string */
protected $varPath;

/**
* @param Google_Client $client
* @param GoogleServiceFactory $googleServiceFactory
* @param FileProvider $fileProvider
* @param array $googleConfiguration
* @param string $googleDomain
* @param string $varPath
*
* @throws Google_Exception
*/
public function __construct(
Google_Client $client,
GoogleServiceFactory $googleServiceFactory,
FileProvider $fileProvider,
array $googleConfiguration,
string $googleDomain,
string $varPath
) {
$this->client = self::initializeClient($client, $googleConfiguration, $googleDomain, $varPath);
$this->configuration = $googleConfiguration;
$this->client = $client;
$this->service = $googleServiceFactory->create($this->client);
$this->fileProvider = $fileProvider;
$this->configuration = $googleConfiguration;
$this->domain = $googleDomain;
$this->varPath = $varPath;
}

/**
* Initializes a Google Client based on configuration.
*
* @see https://developers.google.com/admin-sdk/directory/v1/quickstart/php
*
* @param Google_Client $client
* @param array $configuration
* @param string $domain
* @param string $tempPath
*
* @return Google_Client
* @return GoogleClient
*
* @throws FileNotFoundException
* @throws Google_Exception
*/
protected static function initializeClient(Google_Client $client, array $configuration, string $domain, string $tempPath): Google_Client
public function initialize(): self
{
$client->setApplicationName('Contacts Sync');
$client->setScopes([
$this->client->setApplicationName('Contacts Sync');
$this->client->setScopes([
Google_Service_Directory::ADMIN_DIRECTORY_GROUP,
Google_Service_Directory::ADMIN_DIRECTORY_GROUP_MEMBER,
]);
$client->setAuthConfig($configuration);
$client->setAccessType('offline');
$client->setPrompt('select_account consent');
$client->setHostedDomain($domain);

// Load previously authorized token from a file, if it exists.
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
$tokenPath = sprintf('%s/%s', $tempPath, self::TOKEN_FILENAME);
if (file_exists($tokenPath)) {
$accessToken = \GuzzleHttp\json_decode(file_get_contents($tokenPath), true);
$client->setAccessToken($accessToken);
$this->client->setAuthConfig($this->configuration);
$this->client->setAccessType('offline');
$this->client->setPrompt('select_account consent');
$this->client->setHostedDomain($this->domain);

// try to load the token from the saved file
try {
$this->client->setAccessToken($this->getToken());
} catch (InvalidArgumentException $invalidArgumentException) {
throw new InvalidGoogleTokenException($invalidArgumentException);
}

// If there is no previous token or it's expired.
if ($client->isAccessTokenExpired()) {
// Refresh the token if possible, else fetch a new one.
if ($client->getRefreshToken()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
} else {
// Request authorization from the user.
$authUrl = $client->createAuthUrl();
printf("Open the following link in your browser:\n%s\n", $authUrl);
echo 'Enter verification code: ';
$authCode = trim(fgets(STDIN));

// Exchange authorization code for an access token.
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
$client->setAccessToken($accessToken);

// Check to see if there was an error.
if (array_key_exists('error', $accessToken)) {
throw new RuntimeException(implode(', ', $accessToken));
}
if ($this->client->isAccessTokenExpired()) {
if (!$this->client->getRefreshToken()) {
throw new InvalidGoogleTokenException();
}
// Save the token to a file.
file_put_contents($tokenPath, \GuzzleHttp\json_encode($client->getAccessToken()));
$this->client->fetchAccessTokenWithRefreshToken($this->client->getRefreshToken());
$this->saveToken();
}

return $this;
}

/**
* @return string
*/
public function createAuthUrl(): string
{
return $this->client->createAuthUrl();
}

/**
* @param string $authCode
*/
public function setAuthCode(string $authCode)
{
$accessToken = $this->client->fetchAccessTokenWithAuthCode($authCode);
$this->client->setAccessToken($accessToken);

// Check to see if there was an error.
if (array_key_exists('error', $accessToken)) {
$exception = new RuntimeException(implode(', ', $accessToken));
throw new InvalidGoogleTokenException($exception);
}

return $client;
$this->saveToken();
}

/**
Expand All @@ -131,8 +147,7 @@ public function addContact(string $list, Contact $contact): void
*/
public function removeContact(string $list, Contact $contact): void
{
$member = self::contactToMember($contact);
$this->service->members->delete($list, $member->getEmail());
$this->service->members->delete($list, $contact->email);
}

/**
Expand All @@ -151,6 +166,8 @@ private static function contactToMember(Contact $contact): Google_Service_Direct
/**
* @param Google_Service_Directory_Member $member
*
* @see getContacts
*
* @return Contact
*/
private static function memberToContact(Google_Service_Directory_Member $member): Contact
Expand All @@ -160,4 +177,27 @@ private static function memberToContact(Google_Service_Directory_Member $member)

return $contact;
}

/**
* @return array
*
* @throws FileNotFoundException
*/
private function getToken(): array
{
return \GuzzleHttp\json_decode($this->fileProvider->getContents($this->getTokenPath()), true);
}

/**
* @return string
*/
private function getTokenPath(): string
{
return sprintf('%s/%s', $this->varPath, self::TOKEN_FILENAME);
}

private function saveToken(): void
{
$this->fileProvider->saveContents($this->getTokenPath(), \GuzzleHttp\json_encode($this->client->getAccessToken()));
}
}
14 changes: 14 additions & 0 deletions src/Client/Google/InvalidGoogleTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace App\Client\Google;

use RuntimeException;
use Throwable;

class InvalidGoogleTokenException extends RuntimeException
{
public function __construct(Throwable $previous = null)
{
parent::__construct('The required Google token was not found.', 0, $previous);
}
}
85 changes: 85 additions & 0 deletions src/Command/ConfigureSyncCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace App\Command;

use App\Client\Google\GoogleClient;
use App\Client\Google\InvalidGoogleTokenException;
use Google_Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;

class ConfigureSyncCommand extends Command
{
/** @var string */
public static $defaultName = 'sync:configure';

/** @var GoogleClient */
private $googleClient;

/** @var string */
private $googleDomain;

public function __construct(
GoogleClient $googleClient,
string $googleDomain
) {
$this->googleClient = $googleClient;
$this->googleDomain = $googleDomain;
parent::__construct();
}

public function configure()
{
$this->setDescription('Configures the utility so it can sync member lists');
$this->setHelp(sprintf('An interactive command that prompts for all configuration needed to sync member lists via the %s command.', RunSyncCommand::getDefaultName()));

$this->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Forces the configuration prompts whether or not values are currently set.'
);
}

public function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);

if (!$this->isGoogleClientConfigured() || $input->getOption('force')) {
$io->block(sprintf('This utility requires a valid token for authentication with your Google account (%s). Please visit the following URL in a web browser and paste the provided authentication code into the prompt below.', $this->googleDomain));
$io->writeln($this->googleClient->createAuthUrl().PHP_EOL);

$authCode = trim($io->ask('Paste the auth code here:'));

if (!$authCode) {
$io->error('A Google authentication code must be provided.');

return;
}

$this->googleClient->setAuthCode($authCode);
}
}

/**
* Determines whether the Google Client can be initialized without error.
*
* @return bool
*
* @throws Google_Exception
*/
private function isGoogleClientConfigured(): bool
{
try {
$this->googleClient->initialize();
} catch (InvalidGoogleTokenException | FileNotFoundException $e) {
return false;
}

return true;
}
}
Loading

0 comments on commit cb4e5ae

Please sign in to comment.