Incorporate HTTP endpoint logic so we can now do websockets or HTTP endpoints

This commit is contained in:
Deon George 2022-02-22 11:52:55 +11:00
parent b0c3897e45
commit 6b16d07d80
16 changed files with 264 additions and 40 deletions

View File

@ -6,6 +6,11 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Models\{Channel,Enterprise,Team,User}; use Slack\Models\{Channel,Enterprise,Team,User};
use App\Models\Channel as AppChannel;
use App\Models\Enterprise as AppEnterprise;
use App\Models\Team as AppTeam;
use App\Models\User as AppUser;
/** /**
* Class Base - is a Base to all incoming Slack POST requests * Class Base - is a Base to all incoming Slack POST requests
* *
@ -43,7 +48,9 @@ abstract class Base
*/ */
final public function channel(bool $create=FALSE): ?Channel final public function channel(bool $create=FALSE): ?Channel
{ {
$o = Channel::firstOrNew( $class = class_exists(AppChannel::class) ? AppChannel::class : Channel::class;
$o = $class::firstOrNew(
[ [
'channel_id'=>$this->channel_id, 'channel_id'=>$this->channel_id,
]); ]);
@ -59,7 +66,9 @@ abstract class Base
final public function enterprise(): Enterprise final public function enterprise(): Enterprise
{ {
return Enterprise::firstOrNew( $class = class_exists(AppEnterprise::class) ? AppEnterprise::class : Enterprise::class;
return $class::firstOrNew(
[ [
'enterprise_id'=>$this->enterprise_id 'enterprise_id'=>$this->enterprise_id
]); ]);
@ -73,7 +82,9 @@ abstract class Base
*/ */
final public function team(bool $any=FALSE): ?Team final public function team(bool $any=FALSE): ?Team
{ {
$o = Team::firstOrNew( $class = class_exists(AppTeam::class) ? AppTeam::class : Team::class;
$o = $class::firstOrNew(
[ [
'team_id'=>$this->team_id 'team_id'=>$this->team_id
]); ]);
@ -94,7 +105,9 @@ abstract class Base
*/ */
final public function user(): User final public function user(): User
{ {
$o = User::firstOrNew( $class = class_exists(AppUser::class) ? AppUser::class : User::class;
$o = $class::firstOrNew(
[ [
'user_id'=>$this->user_id, 'user_id'=>$this->user_id,
]); ]);

View File

@ -17,9 +17,9 @@ class Payload implements \ArrayAccess, \JsonSerializable
* *
* @param array $data The payload data. * @param array $data The payload data.
*/ */
public function __construct(array $data) public function __construct(array $data,bool $key=FALSE)
{ {
$this->data = $data; $this->data = $key ? ['payload'=>$data ] : $data;
} }
/** /**

View File

@ -15,14 +15,14 @@ class Factory {
* @var array event type to event class mapping * @var array event type to event class mapping
*/ */
public const map = [ public const map = [
'app_home_opened'=>AppHomeOpened::class, 'app_home_opened' => AppHomeOpened::class,
'member_joined_channel'=>MemberJoinedChannel::class, 'member_joined_channel' => MemberJoinedChannel::class,
'channel_left'=>ChannelLeft::class, 'channel_left' => ChannelLeft::class,
'group_left'=>GroupLeft::class, 'group_left' => GroupLeft::class,
'message'=>Message::class, 'message' => Message::class,
'reaction_added'=>ReactionAdded::class, 'reaction_added' => ReactionAdded::class,
'pin_added'=>PinAdded::class, 'pin_added' => PinAdded::class,
'pin_removed'=>PinRemoved::class, 'pin_removed' => PinRemoved::class,
]; ];
/** /**

View File

@ -0,0 +1,30 @@
<?php
namespace Slack\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Slack\Client\Payload;
use Slack\Event\Factory as SlackEventFactory;
use App\Http\Controllers\Controller;
class EventsController extends Controller
{
private const LOGKEY = 'CEC';
/**
* Fire slack event
*
* @param Request $request
* @return \Illuminate\Http\Response|\Laravel\Lumen\Http\ResponseFactory
*/
public function fire(Request $request)
{
$event = SlackEventFactory::make(new Payload($request->all(),TRUE));
Log::info(sprintf('%s:Dispatching Event [%s]',static::LOGKEY,get_class($event)));
event($event);
return response('Event Processed',200);
}
}

View File

@ -14,7 +14,7 @@ use Slack\Models\{Enterprise,Team,Token,User};
class SlackAppController extends Controller class SlackAppController extends Controller
{ {
private const LOGKEY = 'CSA'; protected const LOGKEY = 'CSA';
private const slack_authorise_url = 'https://slack.com/oauth/v2/authorize'; private const slack_authorise_url = 'https://slack.com/oauth/v2/authorize';
private const slack_oauth_url = 'https://slack.com/api/oauth.v2.access'; private const slack_oauth_url = 'https://slack.com/api/oauth.v2.access';
@ -39,7 +39,7 @@ class SlackAppController extends Controller
public function home() public function home()
{ {
return sprintf('Hi, for instructions on how to install me, please reach out to <strong>@deon.</strong>'); return sprintf('Hi, for instructions on how to install me, please reach out to <strong>%s</strong>.',config('slack.app_admin','Your slack admin'));
} }
public function setup() public function setup()
@ -54,7 +54,7 @@ class SlackAppController extends Controller
* @return string * @return string
* @throws \GuzzleHttp\Exception\GuzzleException * @throws \GuzzleHttp\Exception\GuzzleException
*/ */
public function install(Request $request) public function install(Request $request,bool $oauth=FALSE)
{ {
if (! config('slack.client_id') OR ! config('slack.client_secret')) if (! config('slack.client_id') OR ! config('slack.client_secret'))
abort(403,'Slack ClientID or Secret not set'); abort(403,'Slack ClientID or Secret not set');
@ -158,7 +158,7 @@ class SlackAppController extends Controller
$so->admin_id = $uo->id; $so->admin_id = $uo->id;
$so->save(); $so->save();
return sprintf('All set up! Head back to your slack instance <strong>%s</strong>.',$so->description); return $oauth ? $output : sprintf('All set up! Head back to your slack instance <strong>%s</strong>.',$so->description);
} }
/** /**

View File

@ -0,0 +1,81 @@
<?php
namespace Slack\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Slack\Client\Payload;
use Slack\Event\Factory as EventFactory;
use App\Slack\Interactive\Factory as InteractiveFactory;
use App\Slack\Options\Factory as OptionsFactory;
class CheckRequest
{
private const LOGKEY = 'MCR';
/**
* Ensure that we have the right token before proceeding.
* We should only have 1 message (since the token is an object in the message.)
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request,Closure $next)
{
Log::info(sprintf('%s:Incoming request to [%s]',static::LOGKEY,$request->path()),['m'=>__METHOD__]);
// For app installs, we have nothing to check.
if (in_array($request->path(),config('slack.bypass_routes')))
return $next($request);
switch ($request->path()) {
// For slashcmd full validation is done in the controller
case 'api/slashcmd':
return $next($request);
case 'api/event':
// URL Verification
if ($request->input('type') === 'url_verification') {
Log::debug(sprintf('%s:Responding directly to URL Verification',static::LOGKEY),['m'=>__METHOD__,'r'=>$request->all()]);
return response($request->input('challenge'),200);
}
$event = EventFactory::make(new Payload($request->all(),TRUE));
break;
case 'api/imsgopt':
$event = OptionsFactory::make($request);
break;
case 'api/imsg':
$event = InteractiveFactory::make($request);
break;
default:
// Quietly die if we got here.
return response('',444);
}
// Ignore events for inactive workspaces
if ($event->enterprise_id AND (! $event->enterprise()->active)) {
Log::notice(sprintf('%s:IGNORING post, Enterprise INACTIVE [%s]',static::LOGKEY,$event->enterprise_id),['m'=>__METHOD__]);
// Quietly die if the team is not active
return response('',200);
} elseif ((! $event->enterprise_id) AND ((! $event->team()) OR (! $event->team()->active))) {
Log::notice(sprintf('%s:IGNORING post, Team INACTIVE [%s]',static::LOGKEY,$event->team_id),['m'=>__METHOD__]);
// Quietly die if the team is not active
return response('',200);
} else {
Log::debug(sprintf('%s:Incoming Request Allowed',static::LOGKEY),['m'=>__METHOD__,'e'=>$event->enterprise_id,'t'=>$event->team_id,'eo'=>$event->enterprise()->id,'to'=>$event->team()]);
return $next($request);
}
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Slack\Http\Middleware;
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Slack\Base;
class CheckSignature
{
private const LOGKEY = 'MCS';
/**
* Validate a slack request
* by the slack signing secret (not the token)
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request,Closure $next)
{
// Make sure we are not an installation call
if (! in_array($request->path(),config('slack.bypass_routes'))) {
// get the remote sign
$remote_signature = $request->header('X-Slack-Signature');
Log::info(sprintf('%s:Incoming request - check slack SIGNATURE [%s]',static::LOGKEY,$remote_signature),['m'=>__METHOD__]);
// Load the secret, you also can load it from env(YOUR_OWN_SLACK_SECRET)
$secret = config('slack.signing_secret');
$body = $request->getContent();
// Compare timestamp with the local time, according to the slack official documents
// the gap should under 5 minutes
// @codeCoverageIgnoreStart
if (! $timestamp = $request->header('X-Slack-Request-Timestamp')) {
Log::alert(sprintf('%s:No slack timestamp - aborting...',static::LOGKEY),['m'=>__METHOD__]);
return response('',444);
}
if (($x=Carbon::now()->diffInMinutes(Carbon::createFromTimestamp($timestamp))) > 5) {
Log::alert(sprintf('%s:Invalid slack timestamp [%d]',static::LOGKEY,$x),['m'=>__METHOD__]);
return response('',444);
}
// @codeCoverageIgnoreEnd
// generate the string base
$sig_basestring = sprintf('%s:%s:%s',Base::signature_version,$timestamp,$body);
// generate the local sign
$hash = hash_hmac('sha256',$sig_basestring,$secret);
$local_signature = sprintf('%s=%s',Base::signature_version,$hash);
// check two signs, if not match, throw an error
if ($remote_signature !== $local_signature) {
Log::alert(sprintf('%s:Invalid slack signature [%s]',static::LOGKEY,$remote_signature),['m'=>__METHOD__]);
return response('',444);
}
}
return $next($request);
}
}

View File

@ -4,14 +4,13 @@ namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Leenooks\Traits\ScopeActive; use Slack\Traits\ScopeActive;
class Channel extends Model class Channel extends Model
{ {
use ScopeActive; use ScopeActive;
protected $fillable = ['team_id','channel_id','name','active']; protected $fillable = ['team_id','channel_id','name','active'];
protected $table = 'slack_channels';
/* RELATIONS */ /* RELATIONS */

View File

@ -3,14 +3,13 @@
namespace Slack\Models; namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Leenooks\Traits\ScopeActive; use Slack\Traits\ScopeActive;
class Enterprise extends Model class Enterprise extends Model
{ {
use ScopeActive; use ScopeActive;
protected $fillable = ['enterprise_id']; protected $fillable = ['enterprise_id'];
protected $table = 'slack_enterprises';
/* RELATIONS */ /* RELATIONS */

View File

@ -3,15 +3,14 @@
namespace Slack\Models; namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Leenooks\Traits\ScopeActive;
use Slack\API; use Slack\API;
use Slack\Traits\ScopeActive;
class Team extends Model class Team extends Model
{ {
use ScopeActive; use ScopeActive;
protected $fillable = ['team_id']; protected $fillable = ['team_id'];
protected $table = 'slack_teams';
/* RELATIONS */ /* RELATIONS */

View File

@ -4,14 +4,12 @@ namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Leenooks\Traits\ScopeActive; use Slack\Traits\ScopeActive;
class Token extends Model class Token extends Model
{ {
use ScopeActive; use ScopeActive;
protected $table = 'slack_tokens';
/* RELATIONS */ /* RELATIONS */
public function team() public function team()

View File

@ -4,16 +4,15 @@ namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Leenooks\Traits\ScopeActive; use Slack\Traits\ScopeActive;
class User extends Model class User extends Model
{ {
use ScopeActive; use ScopeActive;
private const LOGKEY = '-MU'; protected const LOGKEY = '-MU';
protected $fillable = ['user_id']; protected $fillable = ['user_id'];
protected $table = 'slack_users';
/* RELATIONS */ /* RELATIONS */

View File

@ -9,6 +9,9 @@ use Slack\API;
use Slack\Channels\SlackBotChannel; use Slack\Channels\SlackBotChannel;
use Slack\Console\Commands\SlackSocketClient; use Slack\Console\Commands\SlackSocketClient;
/**
* @todo For Lumen installs, this service provider is not required?
*/
class SlackServiceProvider extends ServiceProvider class SlackServiceProvider extends ServiceProvider
{ {
/** /**

View File

@ -0,0 +1,17 @@
<?php
/**
* Add a ScopeActive to an Eloquent Model
*/
namespace Slack\Traits;
trait ScopeActive
{
/**
* Only query active records
*/
public function scopeActive($query)
{
return $query->where($this->getTable().'.active',TRUE);
}
}

View File

@ -6,4 +6,7 @@ return [
'client_secret' => env('SLACK_CLIENT_SECRET',NULL), 'client_secret' => env('SLACK_CLIENT_SECRET',NULL),
'signing_secret' => env('SLACK_SIGNING_SECRET',NULL), 'signing_secret' => env('SLACK_SIGNING_SECRET',NULL),
'register_notification' => env('SLACK_REGISTER_NOTIFICATION',TRUE), 'register_notification' => env('SLACK_REGISTER_NOTIFICATION',TRUE),
// Our routes that we dont check for signatures
'bypass_routes' => ['/','slack-install-button','slack-install'],
]; ];

View File

@ -4,7 +4,8 @@ $routeConfig = [
'namespace' => 'Slack\Http\Controllers', 'namespace' => 'Slack\Http\Controllers',
]; ];
app('router')->group($routeConfig, function ($router) { app('router')
->group($routeConfig, function ($router) {
$router->get('slack-install-button', [ $router->get('slack-install-button', [
'uses' => 'SlackAppController@button', 'uses' => 'SlackAppController@button',
'as' => 'slack-install-button', 'as' => 'slack-install-button',
@ -14,4 +15,17 @@ app('router')->group($routeConfig, function ($router) {
'uses' => 'SlackAppController@install', 'uses' => 'SlackAppController@install',
'as' => 'slack-install', 'as' => 'slack-install',
]); ]);
});
$router->get('', [
'uses' => 'SlackAppController@home',
'as' => 'home',
]);
});
app('router')
->group(array_merge($routeConfig,['prefix'=>'api']), function ($router) {
$router->post('event', [
'uses' => 'EventsController@fire',
'as' => 'event',
]);
});