Skip to content

Commit

Permalink
Improvements and additions to AppServers and Deferrers
Browse files Browse the repository at this point in the history
- Improved `AppServer` implementations:
    - Added overrideable `init()` method to assist in initialization since constructor is final.
    - Added support for setting AppCredentials on the base AppServer.
- Improved `Deferrer` implementations:
    - Moved `PreAckDeferrer` to new `Deferral` namespace.
    - Added `ShellExecDeferrer` to allow deferring via a background `shell_exec` call.
    - Added `DeferredContextCliServer` for processing deferred `Context`s via CLI.
    - Fixed initialization bug in `Ack` listener.
- Improved `MultiTenantHttpServer` implementation:
    - Added support for more lenient/intuitive app registrations.
    - Added `Coerce::application()` helper.
- Improved `Auth` components:
    - Fixed bug in `AuthMiddleware` that validated credentials too early.
    - Updated `AppCredentials::supports*()` methods.
    - Added missing `App::withBotToken()` method.
  • Loading branch information
jeremeamia committed Apr 22, 2021
1 parent 6310342 commit a650d04
Show file tree
Hide file tree
Showing 19 changed files with 473 additions and 152 deletions.
15 changes: 15 additions & 0 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,21 @@ public function withAppToken(string $appToken): self
return $this;
}

/**
* Explicitly sets the both token to use for Auth.
*
* You can also set this via the environment variable: SLACK_BOT_TOKEN.
*
* @param string $botToken
* @return $this
*/
public function withBotToken(string $botToken): self
{
$this->config->withBotToken($botToken);

return $this;
}

/**
* Sets the app credentials for the app.
*
Expand Down
4 changes: 4 additions & 0 deletions src/AppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ public function getTokenStore(): TokenStore
}

/**
* Explicitly sets the both token to use for Auth.
*
* You can also set this via the environment variable: SLACK_BOT_TOKEN.
*
* @param string $botToken
* @return $this
*/
Expand Down
67 changes: 62 additions & 5 deletions src/AppServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
namespace SlackPhp\Framework;

use Psr\Log\{LoggerInterface, NullLogger};
use SlackPhp\Framework\Auth\{AppCredentials, AppCredentialsStore};

/**
* An AppServer is a protocol-specific and/or framework-specific app runner.
*
* Its main responsibilities include:
* 1. Receiving an incoming Slack request via the specific protocol/framework.
* 2. Authenticating the Slack request.
* 2. Authentication, including incoming Slack requests or outgoing connections.
* 3. Parsing the Slack request and payload into a Slack `Context`.
* 4. Using the app to process the Slack Context.
* 5. Providing a protocol-specific way for the app to "ack" back to Slack.
Expand All @@ -20,19 +21,27 @@
abstract class AppServer
{
private ?Application $app;
private ?AppCredentialsStore $appCredentialsStore;
private ?LoggerInterface $logger;

/**
* Creates a new instance of the server for fluent configuration.
*
* @return static
*/
public static function new(): self
{
return new static();
}

/**
* Creates the server.
*
* Cannot override. If initialization logic is needed, override the `init()` method.
*/
final public function __construct()
{
// Do nothing. App and Logger are initialized lazily.
$this->init();
}

/**
Expand All @@ -52,7 +61,7 @@ public function withApp($app): self
}

/**
* Gets the logger for the Server
* Gets the application being run by the Server
*
* @return Application
*/
Expand All @@ -70,6 +79,39 @@ protected function getApp(): Application
return $this->app;
}

/**
* Sets the app credentials store for the Server.
*
* @param AppCredentialsStore $appCredentialsStore
* @return $this
*/
public function withAppCredentialsStore(AppCredentialsStore $appCredentialsStore): self
{
$this->appCredentialsStore = $appCredentialsStore;

return $this;
}

/**
* Gets the app credentials to use for authenticating the app being run by the Server.
*
* If app credentials are not provided in the AppConfig, the app credentials store will be used to fetch them.
*
* @return AppCredentials
*/
protected function getAppCredentials(): AppCredentials
{
$config = $this->getApp()->getConfig();
$credentials = $config->getAppCredentials();

if (!$credentials->supportsAnyAuth() && isset($this->appCredentialsStore)) {
$credentials = $this->appCredentialsStore->getAppCredentials($config->getId());
$config->withAppCredentials($credentials);
}

return $credentials;
}

/**
* Sets the logger for the Server.
*
Expand All @@ -84,6 +126,8 @@ public function withLogger(LoggerInterface $logger): self
}

/**
* Gets the logger for the Server.
*
* @return LoggerInterface
*/
protected function getLogger(): LoggerInterface
Expand All @@ -95,6 +139,16 @@ protected function getLogger(): LoggerInterface
: $this->logger;
}

/**
* Initializes a server. Called at the time of construction.
*
* Implementations MAY override.
*/
protected function init(): void
{
// Do nothing by default.
}

/**
* Starts receiving and processing requests from Slack.
*/
Expand All @@ -103,7 +157,10 @@ abstract public function start(): void;
/**
* Stops receiving requests from Slack.
*
* Depending on the implementation, `stop()` may not need to actually do anything.
* Implementations MAY override.
*/
abstract public function stop(): void;
public function stop(): void
{
// Do nothing by default.
}
}
31 changes: 12 additions & 19 deletions src/Auth/AppCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use SlackPhp\Framework\Env;

/**
* Contains credentials required for all types of app authentication.
*/
class AppCredentials
{
/** @var array<string, mixed> */
Expand Down Expand Up @@ -155,30 +158,29 @@ public function supportsApiAuth(): bool
return isset($this->defaultBotToken);
}

public function supportsInstallationOAuth(): bool
public function supportsInstallAuth(): bool
{
return isset($this->clientId, $this->clientSecret);
}

/**
* @return string|null
*/
public function supportsAnyAuth(): bool
{
return $this->supportsHttpAuth()
|| $this->supportsApiAuth()
|| $this->supportsInstallAuth()
|| $this->supportsSocketAuth();
}

public function getAppToken(): ?string
{
return $this->appToken;
}

/**
* @return string|null
*/
public function getClientId(): ?string
{
return $this->clientId;
}

/**
* @return string|null
*/
public function getClientSecret(): ?string
{
return $this->clientSecret;
Expand All @@ -192,25 +194,16 @@ public function getCustomSecrets(): array
return $this->customSecrets;
}

/**
* @return string|null
*/
public function getDefaultBotToken(): ?string
{
return $this->defaultBotToken;
}

/**
* @return string
*/
public function getSigningKey(): ?string
{
return $this->signingKey;
}

/**
* @return string
*/
public function getStateSecret(): ?string
{
return $this->stateSecret;
Expand Down
95 changes: 95 additions & 0 deletions src/Deferral/DeferredContextCliServer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace SlackPhp\Framework\Deferral;

use Closure;
use SlackPhp\Framework\{AppServer, Context, Exception};
use Throwable;

/**
* Server implementation meant to be run from the CLI to process a deferred context.
*/
class DeferredContextCliServer extends AppServer
{
/** @var string[] */
private array $args;
private ?Closure $deserializeCallback;
private int $exitCode = 0;

/**
* @param string[] $args
* @return $this
*/
public function withArgs(array $args): self
{
$this->args = $args;

return $this;
}

/**
* @param callable(string): Context $deserializeCallback
* @return $this
*/
public function withDeserializeCallback(callable $deserializeCallback): self
{
$this->deserializeCallback = Closure::fromCallable($deserializeCallback);

return $this;
}

protected function init(): void
{
global $argv;
$this->args = $argv ?? [];
}

public function start(): void
{
try {
$this->getLogger()->debug('Started processing of deferred context');
$context = $this->deserializeContext($this->args[1] ?? '');
$this->getApp()->handle($context);
$this->getLogger()->debug('Completed processing of deferred context');
} catch (Throwable $exception) {
$this->getLogger()->error('Error occurred during processing of deferred context', compact('exception'));
$this->exitCode = 1;
}

$this->stop();
}

public function stop(): void
{
if (isset($this->args[2]) && $this->args[2] === '--soft-exit') {
return;
}

exit($this->exitCode);
}

private function deserializeContext(string $serializedContext): Context
{
$fn = $this->deserializeCallback ?? function (string $serializedContext): Context {
if (strlen($serializedContext) === 0) {
throw new Exception('No context provided');
}

$data = json_decode(base64_decode($serializedContext), true);
if (empty($data)) {
throw new Exception('Invalid context data');
}

$context = Context::fromArray($data);
if (!($context->isAcknowledged() && $context->isDeferred())) {
throw new Exception('Context was not deferred');
}

return $context;
};

return $fn($serializedContext);
}
}
4 changes: 3 additions & 1 deletion src/PreAckDeferrer.php → src/Deferral/PreAckDeferrer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

declare(strict_types=1);

namespace SlackPhp\Framework;
namespace SlackPhp\Framework\Deferral;

use SlackPhp\Framework\{Context, Deferrer, Listener};

/**
* A synchronous implementation of Deferrer, that does the additional processing prior to the "ack" HTTP response.
Expand Down
Loading

0 comments on commit a650d04

Please sign in to comment.