Enhancements

This commit is contained in:
Deon George 2021-08-10 13:48:59 +10:00
parent 9d66b5ed91
commit 4533b1d0ed
No known key found for this signature in database
GPG Key ID: 7670E8DC27415254
46 changed files with 1334 additions and 548 deletions

View File

@ -55,7 +55,7 @@ class API
{
$this->_token = $o->token;
Log::debug(sprintf('%s:Slack API with token [%s]',static::LOGKEY,$this->_token->token_hidden),['m'=>__METHOD__]);
Log::debug(sprintf('%s:Slack API with token [%s]',static::LOGKEY,$this->_token ? $this->_token->token_hidden : NULL),['m'=>__METHOD__]);
}
public function authTest(): Test

View File

@ -19,9 +19,9 @@ abstract class Base
public const signature_version = 'v0';
public function __construct(Request $request)
public function __construct(array $request)
{
$this->_data = json_decode(json_encode($request->all()));
$this->_data = json_decode(json_encode($request));
if (get_class($this) == self::class)
Log::debug(sprintf('SB-:Received from Slack [%s]',get_class($this)),['m'=>__METHOD__]);
@ -50,6 +50,7 @@ abstract class Base
if (! $o->exists and $create) {
$o->team_id = $this->team()->id;
$o->active = FALSE;
$o->save();
}

View File

@ -18,87 +18,13 @@ class BlockKit implements \JsonSerializable
$this->_data = collect();
}
public function count()
{
return $this->_data->count();
}
public function jsonSerialize()
{
return $this->_data;
}
/**
* Render a BlockKit Button
*
* @param string $label
* @param string $value
* @param string|null $action_id
* @return \Illuminate\Support\Collection
* @throws \Exception
*/
public function button(string $label,string $value,string $action_id=NULL): Collection
{
$x = collect();
$x->put('type','button');
$x->put('text',$this->text($label,'plain_text'));
$x->put('value',$value);
if ($action_id)
$x->put('action_id',$action_id);
return $x;
}
/**
* Render the input dialog
*
* @param string $label
* @param string $action
* @param int $minlength
* @param string $placeholder
* @param bool $multiline
* @param string $hint
* @param string $initial
* @return $this
* @throws \Exception
*/
protected function input(string $label,string $action,int $minlength,string $placeholder='',bool $multiline=FALSE,string $hint='',string $initial='')
{
$this->_data->put('type','input');
$this->_data->put('element',[
'type'=>'plain_text_input',
'action_id'=>$action,
'placeholder'=>$this->text($placeholder ?: ' ','plain_text'),
'multiline'=>$multiline,
'min_length'=>$minlength,
'initial_value'=>$initial,
]);
$this->_data->put('label',[
'type'=>'plain_text',
'text'=>$label,
'emoji'=>true,
]);
$this->_data->put('optional',$minlength ? FALSE : TRUE);
if ($hint)
$this->_data->put('hint',$this->text($hint,'plain_text'));
return $this;
}
/**
* Returns a BlockKit Text item
*
* @param string $text
* @param string $type
* @return array
* @throws \Exception
*/
public function text(string $text,string $type='mrkdwn'): array
{
// Quick Validation
if (! in_array($type,['mrkdwn','plain_text']))
throw new \Exception('Invalid text type: '.$type);
return [
'type'=>$type,
'text'=>$text,
];
}
}

View File

@ -1,256 +0,0 @@
<?php
namespace Slack\Blockkit;
use Illuminate\Support\Collection;
use Slack\BlockKit;
/**
* Class Blockkit Block
* Represents a Block used in Blockkit
*
* @package Slack\Blockkit
*/
class Block extends BlockKit
{
/**
* Add Actions Block
*
* @param Collection $elements
* @return $this
*/
public function addAction(Collection $elements): self
{
// Initialise
$this->_data = collect();
$this->_data->put('type','actions');
$this->_data->put('elements',$elements);
return $this;
}
/**
* A context block
*
* @param Collection $elements
* @return $this
*/
public function addContext(Collection $elements): self
{
// Initialise
$this->_data = collect();
$this->_data->put('type','context');
$this->_data->put('elements',$elements);
return $this;
}
/**
* Add a bock divider
*/
public function addDivider(): self
{
$this->_data->put('type','divider');
return $this;
}
/**
* Add a block header
*
* @param string $text
* @param string $type
* @return Block
* @throws \Exception
*/
public function addHeader(string $text,string $type='plain_text'): self
{
$this->_data->put('type','header');
$this->_data->put('text',$this->text($text,$type));
return $this;
}
/**
* Generates a multiselect that queries back to the server for values
*
* @param string $label
* @param string $action
* @return $this
* @throws \Exception
*/
public function addMultiSelectInput(string $label,string $action): self
{
$this->_data->put('type','section');
$this->_data->put('text',$this->text('mrkdwn',$label));
$this->_data->put('accessory',[
'action_id'=>$action,
'type'=>'multi_external_select',
]);
return $this;
}
/**
* @param string $label
* @param string $action
* @param Collection $options
* @param Collection|null $selected
* @param int|null $maximum
* @return $this
* @throws \Exception
*/
public function addMultiSelectStaticInput(string $label,string $action,Collection $options,Collection $selected=NULL,int $maximum=NULL): self
{
$this->_data->put('type','section');
$this->_data->put('text',$this->text($label,'mrkdwn'));
$x = collect();
$x->put('action_id',$action);
$x->put('type','multi_static_select');
$x->put('options',$options->transform(function ($item) {
return ['text'=>$this->text($item->name,'plain_text'),'value'=>(string)$item->id];
}));
if ($selected and $selected->count())
$x->put('initial_options',$selected->transform(function ($item) {
return ['text'=>$this->text($item->name,'plain_text'),'value'=>(string)$item->id];
}));
if ($maximum)
$x->put('max_selected_items',$maximum);
$this->_data->put('accessory',$x);
return $this;
}
/**
* @param Collection $options
* @param string $action
* @return Collection
*/
public function addOverflow(Collection $options,string $action): Collection
{
return collect([
'type'=>'overflow',
'options'=>$options,
'action_id'=>$action,
]);
}
/**
* A section block
*
* @param string $text
* @param string $type
* @param Collection|null $accessories
* @param string|null $block_id
* @return $this
* @throws \Exception
*/
public function addSection(string $text,string $type='mrkdwn',Collection $accessories=NULL,string $block_id=NULL): self
{
// Initialise
$this->_data = collect();
$this->_data->put('type','section');
$this->_data->put('text',$this->text($text,$type));
if ($block_id)
$this->_data->put('block_id',$block_id);
if ($accessories AND $accessories->count())
$this->_data->put('accessory',$accessories);
return $this;
}
/**
* @param string $label
* @param string $action
* @param Collection $options
* @param string|null $default
* @return $this
* @throws \Exception
*/
public function addSelect(string $label,string $action,Collection $options,string $default=NULL): self
{
$this->_data->put('type','section');
$this->_data->put('text',$this->text($label,'mrkdwn'));
// Accessories
$x = collect();
$x->put('action_id',$action);
$x->put('type','static_select');
$x->put('options',$options->map(function ($item) {
if (is_array($item))
$item = (object)$item;
return [
'text'=>[
'type'=>'plain_text',
'text'=>(string)$item->name,
],
'value'=>(string)($item->value ?: $item->id)
];
}));
if ($default) {
$choice = $options->filter(function($item) use ($default) {
if (is_array($item))
$item = (object)$item;
return ($item->value == $default) ? $item : NULL;
})->filter()->pop();
if ($choice) {
$x->put('initial_option',[
'text'=>$this->text($choice['name'],'plain_text'),
'value'=>(string)$choice['value']
]);
}
}
$this->_data->put('accessory',$x);
return $this;
}
/**
* Generates a single-line input dialog
*
* @param string $label
* @param string $action
* @param string $placeholder
* @param int $minlength
* @param string $hint
* @param string $initial
* @return $this
* @throws \Exception
*/
public function addSingleLineInput(string $label,string $action,string $placeholder='',int $minlength=5,string $hint='',string $initial=''): self
{
return $this->input($label,$action,$minlength,$placeholder,FALSE,$hint,$initial);
}
/**
* Generates a multi-line input dialog
*
* @param string $label
* @param string $action
* @param string $placeholder
* @param int $minlength
* @param string $hint
* @param string $initial
* @return $this
* @throws \Exception
*/
public function addMultiLineInput(string $label,string $action,string $placeholder='',int $minlength=20,string $hint='',string $initial=''): self
{
return $this->input($label,$action,$minlength,$placeholder,TRUE,$hint,$initial);
}
}

View File

@ -3,9 +3,11 @@
namespace Slack\Blockkit;
use Slack\BlockKit;
use Slack\Blockkit\Blocks\TextEmoji;
/**
* This class creates a slack actions used in BlockKit Actions
* @todo Still needed?
*/
class BlockAction extends BlockKit
{
@ -18,15 +20,16 @@ class BlockAction extends BlockKit
* @param string $style
* @return BlockAction
* @throws \Exception
* @deprecated Move to Blocks/Button?
*/
public function addButton(string $text,string $action,string $value,string $style=''): self
public function addButton(TextEmoji $text,string $action,string $value,string $style=''): self
{
if ($style AND ! in_array($style,['primary','danger']))
abort('Invalid style: '.$style);
$this->_data->put('type','button');
$this->_data->put('action_id',$action);
$this->_data->put('text',$this->text($text,'plain_text'));
$this->_data->put('text',$text);
$this->_data->put('value',$value);
if ($style)

341
src/Blockkit/Blocks.php Normal file
View File

@ -0,0 +1,341 @@
<?php
namespace Slack\Blockkit;
use Illuminate\Support\Collection;
use Slack\BlockKit;
use Slack\Exceptions\SlackException;
use Slack\Blockkit\Blocks\{Button,Text,TextEmoji};
/**
* Class Blockkit Block
* Represents a list of Blockit blocks
*
* @package Slack\Blockkit
*/
class Blocks extends BlockKit
{
/**
* Actions
*
* @param array $items
* @return Blocks
*/
private function actions(array $items): self
{
$this->_data->push(array_merge(['type'=>'actions'],$items));
return $this;
}
/**
* Add actions block
*
* @param Collection $elements
* @return Blocks
*/
public function addActionElements(Collection $elements): self
{
$this->actions(['elements'=>$elements]);
return $this;
}
/**
* Add context items
*
* @param Collection $items
* @return Blocks
*/
public function addContextElements(Collection $items): self
{
return $this->context(['elements'=>$items]);
}
/**
* Add a bock divider
*
* @returns Blocks
*/
public function addDivider(): self
{
$this->_data->push(['type'=>'divider']);
return $this;
}
/**
* Add a block header
*
* @param string $text
* @return Blocks
*/
public function addHeader(string $text): self
{
$this->_data->push(['type'=>'header','text'=>TextEmoji::item($text,TRUE)]);
return $this;
}
/**
* @param Collection $options
* @param string $action_id
* @return Collection
* @todo To Check
*/
public function addOverflow(Collection $options,string $action_id): Collection
{
return collect([
'type'=>'overflow',
'options'=>$options,
'action_id'=>$action_id,
]);
}
/**
* Add a section with accessories
*
* @param Text $text
* @param Button $accessory
* @return Blocks
*/
public function addSectionAccessoryButton(Text $text,Button $accessory): self
{
return $this->section([
'text'=>$text,
'accessory'=>$accessory,
]);
}
/**
* @param Text $label
* @param string $action
* @param Collection $options
* @param string|null $default
* @return Blocks
* @deprecated
* @todo Look at addSectionAccessory
*/
public function addSelect(Text $label,string $action,Collection $options,string $default=NULL): self
{
$this->_data->put('type','section');
$this->_data->put('text',$label);
// Accessories
$x = collect();
$x->put('action_id',$action);
$x->put('type','static_select');
$x->put('options',$options->map(function ($item) {
if (is_array($item))
$item = (object)$item;
return [
'text'=>[
'type'=>'plain_text',
'text'=>(string)$item->name,
],
'value'=>(string)($item->value ?: $item->id)
];
}));
if ($default) {
$choice = $options->filter(function($item) use ($default) {
if (is_array($item))
$item = (object)$item;
return ($item->value == $default) ? $item : NULL;
})->filter()->pop();
if ($choice) {
$x->put('initial_option',[
'text'=>TextEmoji::item($choice['name']),
'value'=>(string)$choice['value']
]);
}
}
$this->_data->put('accessory',$x);
return $this;
}
/**
* Add a section with fields
* @param Collection $items
* @return Blocks
*/
public function addSectionFields(Collection $items): self
{
return $this->section(['fields'=>$items]);
}
/**
* Generates a multiselect that queries back to the server for values
*
* @param Text $label
* @param string $action
* @return Blocks
* @todo To Change - and rename addSectionMultiSelectInput()
* @deprecated
*/
public function addMultiSelectInput(Text $label,string $action): self
{
$this->_data->put('type','section');
$this->_data->put('text',$label);
$this->_data->put('accessory',[
'action_id'=>$action,
'type'=>'multi_external_select',
]);
return $this;
}
/**
* Add a section with a multi select list
*
* @param Text $label
* @param string $action
* @param Collection $options
* @param Collection|null $selected
* @param int|null $maximum
* @return Blocks
* @throws \Exception
* @note Slack only allows 100 items
*/
public function addSectionMultiSelectStaticInput(Text $label,string $action,Collection $options,Collection $selected=NULL,int $maximum=NULL): self
{
if ($options->count() > 100)
throw new SlackException('Selection list cannot have more than 100 items.');
$x = collect();
$x->put('action_id',$action);
$x->put('type','multi_static_select');
$x->put('options',$options->transform(function ($item) {
return ['text'=>TextEmoji::item($item->name),'value'=>(string)$item->id];
}));
if ($selected and $selected->count())
$x->put('initial_options',$selected->transform(function ($item) {
return ['text'=>TextEmoji::item($item->name),'value'=>(string)$item->id];
}));
if ($maximum)
$x->put('max_selected_items',$maximum);
return $this->section([
'text' => $label,
'accessory' => $x
]);
}
/**
* Add a section with just text
*
* @param Text $text
* @return Blocks
*/
public function addSectionText(Text $text): self
{
return $this->section(['text'=>$text]);
}
/**
* A context block
*
* @param array $items
* @return Blocks
*/
private function context(array $items): self
{
$this->_data->push(array_merge(['type'=>'context'],$items));
return $this;
}
/**
* Render the input dialog
*
* @param string $label
* @param string $action
* @param int $minlength
* @param string $placeholder
* @param bool $multiline
* @param string $hint
* @param string $initial
* @return $this
* @throws \Exception
* @deprecated - to optimize
*/
protected function input(string $label,string $action,int $minlength,string $placeholder='',bool $multiline=FALSE,string $hint='',string $initial='')
{
$this->_data->put('type','input');
$this->_data->put('element',[
'type'=>'plain_text_input',
'action_id'=>$action,
'placeholder'=>$this->text($placeholder ?: ' ','plain_text'),
'multiline'=>$multiline,
'min_length'=>$minlength,
'initial_value'=>$initial,
]);
$this->_data->put('label',[
'type'=>'plain_text',
'text'=>$label,
'emoji'=>true,
]);
$this->_data->put('optional',$minlength ? FALSE : TRUE);
if ($hint)
$this->_data->put('hint',$this->text($hint,'plain_text'));
return $this;
}
/**
* A section block
*
* @param array $items
* @return Blocks
*/
private function section(array $items): self
{
$this->_data->push(array_merge(['type'=>'section'],$items));
return $this;
}
/**
* Generates a single-line input dialog
*
* @param string $label
* @param string $action
* @param string $placeholder
* @param int $minlength
* @param string $hint
* @param string $initial
* @return Blocks
* @throws \Exception
* @deprecated - to optimize
*/
public function addSingleLineInput(string $label,string $action,string $placeholder='',int $minlength=5,string $hint='',string $initial=''): self
{
return $this->input($label,$action,$minlength,$placeholder,FALSE,$hint,$initial);
}
/**
* Generates a multi-line input dialog
*
* @param string $label
* @param string $action
* @param string $placeholder
* @param int $minlength
* @param string $hint
* @param string $initial
* @return Blocks
* @throws \Exception
* @deprecated - to optimize
*/
public function addMultiLineInput(string $label,string $action,string $placeholder='',int $minlength=20,string $hint='',string $initial=''): self
{
return $this->input($label,$action,$minlength,$placeholder,TRUE,$hint,$initial);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Slack\Blockkit\Blocks;
final class Button
{
public string $type;
public TextEmoji $text;
public string $value;
public string $url;
public string $action_id;
public string $style;
public function __construct(TextEmoji $text,string $value,string $action_id,string $url=NULL,string $style=NULL)
{
$this->type = 'button';
$this->text = $text;
$this->value = $value ?: '-';
$this->action_id = $action_id;
if ($url)
$this->url = $url;
if ($style)
$this->style = $style;
}
public static function item(TextEmoji $text,string $value,string $action_id,string $url=NULL,string $style=NULL): self
{
return new self($text,$value,$action_id,$url,$style);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Slack\Blockkit\Blocks;
use Slack\Blockkit\Input\Element;
final class Input
{
public string $type;
public Element $element;
public TextEmoji $label;
public function __construct(Element $element,TextEmoji $label)
{
$this->type = 'input';
$this->element = $element;
$this->label = $label;
}
public static function item(Element $element,TextEmoji $label): self
{
return new self($element,$label);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Slack\Blockkit\Blocks;
final class Text
{
public string $type;
public string $text;
public function __construct(string $text,string $type='mrkdwn')
{
$this->type = $type;
$this->text = $text;
}
public static function item(string $text,string $type='mrkdwn'): self
{
return new self($text,$type);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Slack\Blockkit\Blocks;
final class TextEmoji
{
public bool $emoji;
public string $text;
public string $type;
public function __construct(string $text,bool $emoji=TRUE)
{
$this->emoji = $emoji;
$this->text = $text;
$this->type = 'plain_text';
}
public static function item(string $text,bool $emoji=TRUE): self
{
return new self($text,$emoji);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Slack\Blockkit\Input;
final class Element
{
public string $type;
public string $action_id;
public bool $multiline;
public function __construct(string $type,string $action_id,bool $multiline=FALSE)
{
$this->type = $type;
$this->action_id = $action_id;
$this->multiline = $multiline;
}
public static function item(string $type,string $action_id,bool $multiline=FALSE): self
{
return new self($type,$action_id,$multiline);
}
}

View File

@ -2,40 +2,36 @@
namespace Slack\Blockkit;
use Illuminate\Support\Str;
use Slack\BlockKit;
use Slack\Blockkit\Blocks\TextEmoji;
/**
* This class creates a slack Modal Response
*/
class Modal extends BlockKit
{
protected $blocks;
private $action = NULL;
public function __construct(string $title)
public function __construct(TextEmoji $title)
{
parent::__construct();
$this->blocks = collect();
$this->_data->put('type','modal');
$this->_data->put('title',$this->text(Str::limit($title,24),'plain_text'));
$this->_data->put('title',$title);
}
/*
public function action(string $action)
{
$this->action = $action;
}
*/
/**
* The data that will be returned when converted to JSON.
*/
public function jsonSerialize()
{
if ($this->blocks->count())
$this->_data->put('blocks',$this->blocks);
switch ($this->action) {
case 'clear':
return ['response_action'=>'clear'];
@ -51,23 +47,24 @@ class Modal extends BlockKit
/**
* Add a block to the modal
*
* @param Block $block
* @param Blocks $blocks
* @return $this
*/
public function addBlock(Block $block): self
public function setBlocks(Blocks $blocks): self
{
$this->blocks->push($block);
$this->_data->put('blocks',$blocks);
return $this;
}
public function callback(string $id): self
public function setCallback(string $id): self
{
$this->_data->put('callback_id',$id);
return $this;
}
/*
public function close(string $text='Cancel'): self
{
$this->_data->put('close',
@ -112,4 +109,5 @@ class Modal extends BlockKit
return $this;
}
*/
}

View File

@ -0,0 +1,20 @@
<?php
namespace Slack\Channels;
use Illuminate\Notifications\Notification;
class SlackBotChannel
{
public function send($notifiable,Notification $notification)
{
if (! $co = $notifiable->routeNotificationFor('slackapp',$notification)) {
return;
}
$o = $notification->toSlack($notifiable);
$o->setChannel($co);
return $o->post();
}
}

View File

@ -60,9 +60,9 @@ abstract class API
* @param array $args An associative array of arguments to pass to the
* method call.
* @param bool $multipart Whether to send as a multipart request. Default to false
* @param bool $callDeferred Wether to call the API asynchronous or not.
* @param bool $callDeferred Whether to call the API asynchronous or not.
*
* @return \React\Promise\PromiseInterface A promise for an API response.
* @return PromiseInterface A promise for an API response.
*/
public function apiCall(string $method,array $args=[],bool $multipart=FALSE,bool $callDeferred=TRUE): PromiseInterface
{
@ -84,7 +84,6 @@ abstract class API
]
]);
//dump(['m'=>__METHOD__,'l'=>__LINE__,'promise'=>$promise]);
// Add requests to the event loop to be handled at a later date.
if ($callDeferred) {
$this->loop->futureTick(function () use ($promise) {

View File

@ -11,11 +11,15 @@ use Illuminate\Support\Facades\Log;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
use React\Promise;
use Slack\Command\Factory;
class SocketMode extends API
{
use EventEmitterTrait;
private const LOGKEY = 'ASM';
private bool $debug_reconnect = FALSE;
private bool $connected;
private WebSocket $websocket;
@ -37,7 +41,7 @@ class SocketMode extends API
*
* @return \React\Promise\PromiseInterface
*/
public function connect()
public function connect(): Promise\PromiseInterface
{
$deferred = new Deferred;
@ -81,9 +85,9 @@ class SocketMode extends API
// initiate the websocket connection
// write PHPWS things to the existing logger
$this->websocket = new WebSocket($response['url'].'&debug_reconnects=true', $this->loop, $this->logger);
$this->websocket = new WebSocket($response['url'].($this->debug_reconnect ? '&debug_reconnects=true' : ''),$this->loop,$this->logger);
$this->websocket->on('message', function ($message) {
Log::debug('Calling onMessage for',['m'=>serialize($message)]);
Log::debug(sprintf('%s:- Calling onMessage ...',self::LOGKEY),['m'=>__METHOD__]);
$this->onMessage($message);
});
@ -124,6 +128,8 @@ class SocketMode extends API
$this->websocket->close();
$this->connected = FALSE;
return TRUE;
}
/**
@ -139,23 +145,37 @@ class SocketMode extends API
/**
* Handles incoming websocket messages, parses them, and emits them as remote events.
*
* @param WebSocketMessageInterface $messageRaw A websocket message.
* @param WebSocketMessageInterface $message
*/
private function onMessage(WebSocketMessageInterface $message)
{
Log::debug('+ Start',['m'=>__METHOD__]);
Log::debug(sprintf('%s:+ Start',self::LOGKEY),['m'=>__METHOD__]);
// parse the message and get the event name
$payload = Payload::fromJson($message->getData());
$emitted = FALSE;
if (isset($payload['type'])) {
$this->emit('_internal_message', [$payload['type'], $payload]);
switch ($payload['type']) {
case 'hello':
$this->connected = TRUE;
break;
/*
case 'disconnect':
Log::debug(sprintf('%s:- Disconnect Received, Re-Connecting...',self::LOGKEY),['m'=>__METHOD__]);
$this->disconnect();
$this->connect();
break;
// We got an event, we'll handle it later in SlackSocketClient::class (after ACKing it).
case 'events_api':
case 'interactive':
case 'slash_commands':
break;
/*
case 'team_rename':
$this->team->data['name'] = $payload['name'];
break;
@ -234,19 +254,47 @@ class SocketMode extends API
$user = new User($this, $payload['user']);
$this->users[$user->getId()] = $user;
break;
*/
default:
Log::debug(sprintf('Unhandled type [%s]',$payload['type']),['m'=>__METHOD__,'p'=>$payload]);
}
*/
// emit an event with the attached json
$this->emit($payload['type'], [$payload]);
default:
Log::debug(sprintf('%s:- Unhandled type [%s]',self::LOGKEY,$payload['type']),['m'=>__METHOD__,'p'=>$payload]);
}
}
if (isset($payload['envelope_id'])) {
// @acknowledge the event
$this->websocket->send(json_encode(['envelope_id'=>$payload['envelope_id']]));
Log::debug(sprintf('Responded to event [%s] for (%s)',$payload['envelope_id'],$payload['type']),['m'=>__METHOD__]);
if (isset($payload['accepts_response_payload']) && $payload['accepts_response_payload']) {
switch ($payload['type']) {
case 'slash_commands':
$command = Factory::make($payload);
if (! method_exists($command,'respond')) {
Log::alert(sprintf('%s:! Cant respond to Command [%s], no respond method',static::LOGKEY,get_class($command)),['m'=>__METHOD__]);
abort(500,'No respond method() for '.get_class($command));
}
$response = ($x=$command->respond())->isEmpty() ? NULL : $x;
$this->websocket->send(json_encode(['envelope_id'=>$payload['envelope_id'],'payload'=>$response]));
$emitted = TRUE;
break;
default:
Log::debug(sprintf('%s:- Unhandled type [%s] for accepts_response_payload',self::LOGKEY,$payload['type']),['m'=>__METHOD__]);
$this->websocket->send(json_encode(['envelope_id'=>$payload['envelope_id']]));
}
} else {
// @acknowledge the event
$this->websocket->send(json_encode(['envelope_id'=>$payload['envelope_id']]));
}
Log::debug(sprintf('%s:- Responded to event [%s] for (%s)',self::LOGKEY,$payload['envelope_id'],$payload['type']),['m'=>__METHOD__]);
}
// If we havent already handled the event, we'll emit it to be handled upstream
if (isset($payload['type']) && ! $emitted) {
// emit an event with the attached json
$this->emit($payload['type'],[$payload]);
}
if (! isset($payload['type']) || $payload['type'] == 'pong') {
@ -269,6 +317,6 @@ class SocketMode extends API
}
}
Log::debug('= End',['m'=>__METHOD__]);
Log::debug(sprintf('%s:= End',self::LOGKEY),['m'=>__METHOD__]);
}
}

View File

@ -2,16 +2,17 @@
namespace Slack\Command;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Slack\Base as SlackBase;
use Slack\Blockkit\Blocks;
use Slack\Message;
abstract class Base extends SlackBase
{
public function __construct(Request $request)
public function __construct(array $request)
{
Log::info(sprintf('SCb:Slack SLASHCOMMAND Initialised [%s]',get_class($this)),['m'=>__METHOD__]);
parent::_construct($request);
parent::__construct($request);
}
/**
@ -51,4 +52,20 @@ abstract class Base extends SlackBase
return object_get($this->_data,'trigger_id');
}
}
protected function bot_in_channel(): ?Message
{
$o = new Message;
if (! $this->channel() || ! $this->channel()->active) {
$blocks = new Blocks;
$blocks->addHeader(':robot_face: Bot not in this channel');
$blocks->addSectionText(Blocks\Text::item(sprintf('Please add %s to this channel and try this again.',$this->team()->bot->name ?: 'the BOT')));
$o->setAttachments((new Message\Attachments())->setBlocks($blocks)->setColor('#ff0000'));
}
return $o->isEmpty() ? NULL : $o;
}
}

View File

@ -2,10 +2,10 @@
namespace Slack\Command;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Slack\Client\Payload;
class Factory {
private const LOGKEY = 'SCf';
@ -14,39 +14,32 @@ class Factory {
* @var array event type to event class mapping
*/
public const map = [
'ask'=>Watson::class,
'ate'=>Ask::class,
'help'=>Help::class,
'goto'=>Link::class,
'leaders'=>Leaders::class,
'products'=>Products::class,
'review'=>Review::class,
'wc'=>WatsonCollection::class,
];
/**
* Returns new event instance
*
* @param string $type
* @param Request $request
* @param string $type
* @param array $request
* @return Base
*/
public static function create(string $type,Request $request)
public static function create(string $type,array $request): Base
{
$class = Arr::get(self::map,$type,Unknown::class);
$class = Arr::get(config('slack.commands',self::map),$type,Unknown::class);
Log::debug(sprintf('%s:Working out Slash Command Class for [%s] as [%s]',static::LOGKEY,$type,$class),['m'=>__METHOD__]);
if (App::environment() == 'dev')
file_put_contents('/tmp/command.'.$type,print_r(json_decode(json_encode($request->all())),TRUE));
if (App::environment() == 'local')
file_put_contents('/tmp/command.'.$type,print_r(json_decode(json_encode($request)),TRUE));
return new $class($request);
}
public static function make(Request $request): Base
public static function make(Payload $request): Base
{
$data = json_decode(json_encode($request->all()));
$command = preg_replace('/^([a-z]+)(\s?.*)/','$1',$data->text);
$data = json_decode(json_encode($request->getData()));
$command = preg_replace('/^([a-z]+)(\s?.*)/','$1',$data->payload->text);
return self::create($command ?: 'help',$request);
return self::create($command ?: 'help',Arr::get($request->getData(),'payload'));
}
}

View File

@ -3,7 +3,7 @@
namespace Slack\Command;
use Slack\Message;
use Slack\Message\Attachment;
use Slack\Message\Attachments;
class Help extends Base
{
@ -13,12 +13,12 @@ class Help extends Base
{
$o = new Message;
$o->setText('Hi, I am the a *NEW* Bot');
$o->setText('Hi, I am a *NEW* Bot');
// Version
$a = new Attachment;
$a->addField('Version',config('app.version'),TRUE);
$o->addAttachment($a);
$a = new Attachments;
$a->addField('Version',config('app.version','unknown'),TRUE);
$o->setAttachments($a);
return $o;
}

View File

@ -13,7 +13,7 @@ use Slack\Message;
*/
final class Unknown extends Base
{
public function __construct(Request $request)
public function __construct(array $request)
{
Log::notice(sprintf('SCU:UNKNOWN Slack Interaction Option received [%s]',get_class($this)),['m'=>__METHOD__]);

View File

@ -6,6 +6,9 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use React\EventLoop\Loop;
use Slack\Client\SocketMode;
use Slack\Command\Factory as SlackCommandFactory;
use Slack\Event\Factory as SlackEventFactory;
use Slack\Interactive\Factory as SlackInteractiveFactory;
class SlackSocketClient extends Command
{
@ -47,12 +50,20 @@ class SlackSocketClient extends Command
$client = new SocketMode($loop);
$client->setToken(config('slack.socket_token'));
$client->on('events_api', function ($data) use ($client) {
dump(['data'=>$data]);
$client->on('events_api',function ($data) {
event(SlackEventFactory::make($data));
});
$client->on('interactive',function ($data) {
event(SlackInteractiveFactory::make($data));
});
$client->on('slash_command',function ($data) {
event(SlackCommandFactory::make($data));
});
$client->connect()->then(function () {
Log::debug(sprintf('%s: Connected to slack.',self::LOGKEY));
Log::debug(sprintf('%s:- Connected to slack.',self::LOGKEY));
});
$loop->run();

View File

@ -8,7 +8,7 @@ use Slack\Base as SlackBase;
abstract class Base extends SlackBase
{
public function __construct(Request $request)
public function __construct(array $request)
{
Log::info(sprintf('SEb:Slack Event Initialised [%s]',get_class($this)),['m'=>__METHOD__]);

View File

@ -6,6 +6,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Slack\Client\Payload;
class Factory {
private const LOGKEY = 'SEf';
@ -27,22 +28,22 @@ class Factory {
/**
* Returns new event instance
*
* @param string $type
* @param Request $request
* @param string $type
* @param array $request
* @return Base
*/
public static function create(string $type,Request $request)
public static function create(string $type,array $request): Base
{
$class = Arr::get(self::map,$type,Unknown::class);
Log::debug(sprintf('%s:Working out Event Class for [%s] as [%s]',static::LOGKEY,$type,$class),['m'=>__METHOD__]);
if (App::environment() == 'dev')
file_put_contents('/tmp/event.'.$type,print_r($request->all(),TRUE));
if (App::environment() == 'local')
file_put_contents('/tmp/event.'.$type,print_r($request,TRUE));
return new $class($request);
}
public static function make(Request $request): Base
public static function make(Payload $request): Base
{
// During the life of the event, this method is called twice - once during Middleware processing, and finally by the Controller.
static $o = NULL;
@ -50,7 +51,7 @@ class Factory {
if (! $o OR ($or != $request)) {
$or = $request;
$o = self::create($request->input('event.type'),$request);
$o = self::create(Arr::get($request->getData(),'payload.event.type'),Arr::get($request->getData(),'payload'));
}
return $o;

View File

@ -12,10 +12,10 @@ use Illuminate\Support\Facades\Log;
*/
class Unknown extends Base
{
public function __construct(Request $request)
public function __construct(array $request)
{
Log::notice(sprintf('SEU:UNKNOWN Slack Event received [%s]',get_class($this)),['m'=>__METHOD__]);
parent::__contruct($request);
parent::__construct($request);
}
}

View File

@ -16,25 +16,6 @@ class SlackAppController extends Controller
{
private const LOGKEY = 'CSA';
protected static $scopes = [
/*
'channels:history',
'channels:read',
'chat:write',
'chat:write.customize',
'groups:history',
'groups:read',
'im:history',
'im:read',
'im:write',
'team:read',
*/
];
protected static $user_scopes = [
//'pins.write',
];
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_button = 'https://platform.slack-edge.com/img/add_to_slack.png';
@ -173,9 +154,15 @@ class SlackAppController extends Controller
$so->admin_id = $uo->id;
$so->save();
return sprintf('All set up! Head back to your slack instance <strong>%s</strong>."',$so->description);
return sprintf('All set up! Head back to your slack instance <strong>%s</strong>.',$so->description);
}
/**
* Define our parameters to install this Slack Integration
*
* @note The configuration file should include a list of scopes that this application needs
* @return array
*/
private function parameters(): array
{
return [

View File

@ -2,7 +2,6 @@
namespace Slack\Interactive;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Slack\Base as SlackBase;
@ -14,12 +13,12 @@ abstract class Base extends SlackBase
// When retrieving multiple action values, this is the index we are retrieving.
protected $index = 0;
public function __construct(Request $request)
public function __construct(array $request)
{
Log::info(sprintf('SIb:Slack INTERACTIVE MESSAGE Initialised [%s]',get_class($this)),['m'=>__METHOD__]);
// Our data is in a payload value
$this->_data = json_decode($request->input('payload'));
parent::__construct($request);
}
/**

View File

@ -60,25 +60,24 @@ class BlockActions extends Base
public function __get($key)
{
switch ($key) {
case 'callback_id':
return object_get($this->_data,'view.callback_id');
case 'actions':
return object_get($this->_data,$key);
case 'action_id':
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),$key);
// An event can have more than 1 action, each action can have 1 value.
case 'action_id':
case 'action_key':
return $this->action('action');
case 'action_value':
return $this->action('value');
case 'value':
switch (Arr::get(object_get($this->_data,'actions'),$this->index)->type) {
case 'external_select':
case 'overflow':
case 'static_select':
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'selected_option.value');
default:
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'value');
}
case 'callback_id':
return object_get($this->_data,'view.callback_id');
case 'keys':
return collect(object_get($this->_data,'view.blocks'))->pluck('accessory.action_id');
// For Block Actions that are messages
case 'message_ts':
@ -87,16 +86,41 @@ class BlockActions extends Base
case 'channel_id':
return object_get($this->_data,'channel.id') ?: Channel::findOrFail($this->action('value'))->channel_id;
case 'team_id': // view.team_id represent workspace publishing view
return object_get($this->_data,'user.team_id');
case 'view_id':
return object_get($this->_data,'view.id');
case 'actions':
return object_get($this->_data,$key);
case 'value':
switch (Arr::get(object_get($this->_data,'actions'),$this->index)->type) {
case 'external_select':
case 'overflow':
case 'static_select':
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'selected_option.value');
case 'multi_static_select':
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'selected_options.value');
default:
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),$key);
}
// For some reason this object is not making sense, and while we should be getting team.id or even view.team_id, the actual team appears to be in user.team_id
// @todo Currently working with Slack to understand this behaviour
case 'team_id': // view.team_id represent workspace publishing view
return object_get($this->_data,'user.team_id');
case 'values':
switch (Arr::get(object_get($this->_data,'actions'),$this->index)->type) {
// @todo To Check
case 'external_select':
// @todo To Check
case 'overflow':
// @todo To Check
case 'static_select':
return count(object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'selected_option'));
case 'multi_static_select':
return collect(object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'selected_options'))->pluck('value');
default:
return count(object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'value'));
}
case 'value_count':
return count($this->values);
default:
return parent::__get($key);
@ -116,16 +140,16 @@ class BlockActions extends Base
$value = NULL;
// We only take the action up to the pipe symbol
$action_id = object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'action_id');
$action_key = object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'action_id');
if (preg_match($regex,$action_id)) {
$action = preg_replace($regex,'$1',$action_id);
$value = preg_replace($regex,'$2',$action_id);
if (preg_match($regex,$action_key)) {
$action = preg_replace($regex,'$1',$action_key);
$value = preg_replace($regex,'$2',$action_key);
}
switch ($key) {
case 'action':
return $action ?: $action_id;
return $action ?: $action_key;
case 'value':
return $value;
}
@ -142,20 +166,4 @@ class BlockActions extends Base
{
return object_get($this->_data,'message') ? TRUE : FALSE;
}
/**
* Get the selected options from a block action actions array
*
* @return Collection
*/
public function selected_options(): Collection
{
$result = collect();
foreach (Arr::get(object_get($this->_data,'actions'),'0')->selected_options as $option) {
$result->push($option->value);
}
return $result;
}
}

View File

@ -6,6 +6,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Slack\Client\Payload;
class Factory {
private const LOGKEY = 'SIF';
@ -24,31 +25,30 @@ class Factory {
/**
* Returns new event instance
*
* @param string $type
* @param Request $request
* @param string $type
* @param array $request
* @return Base
*/
public static function create(string $type,Request $request)
public static function create(string $type,array $request): Base
{
$class = Arr::get(self::map,$type,Unknown::class);
Log::debug(sprintf('%s:Working out Interactive Message Event Class for [%s] as [%s]',static::LOGKEY,$type,$class),['m'=>__METHOD__]);
if (App::environment() == 'dev')
file_put_contents('/tmp/interactive.'.$type,print_r(json_decode($request->input('payload')),TRUE));
if (App::environment() == 'local')
file_put_contents('/tmp/interactive.'.$type,print_r($request,TRUE));
return new $class($request);
}
public static function make(Request $request): Base
public static function make(Payload $request): Base
{
// During the life of the event, this method is called twice - once during Middleware processing, and finally by the Controller.
static $o = NULL;
static $or = NULL;
if (! $o OR ($or != $request)) {
$data = json_decode($request->input('payload'));
$or = $request;
$o = self::create($data->type,$request);
$o = self::create(Arr::get($request->getData(),'payload.type'),Arr::get($request->getData(),'payload'));
}
return $o;

View File

@ -12,7 +12,7 @@ use Illuminate\Support\Facades\Log;
*/
class Unknown extends Base
{
public function __construct(Request $request)
public function __construct(array $request)
{
Log::notice(sprintf('SIU:UNKNOWN Slack Interaction Option received [%s]',get_class($this)),['m'=>__METHOD__]);

View File

@ -0,0 +1,30 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Jobs\SlackHomeTabUpdate;
use Slack\Event\AppHomeOpened;
class AppHomeOpenedListener implements ShouldQueue
{
private const LOGKEY = 'LAH';
public $queue = 'high';
/**
* Handle the event.
*
* @param AppHomeOpened $event
* @return void
*/
public function handle(AppHomeOpened $event)
{
// Do some magic with event data
Log::info(sprintf('%s:App Home Page open for [%s] in team [%s]',self::LOGKEY,$event->user_id,$event->team_id),['m'=>__METHOD__]);
dispatch((new SlackHomeTabUpdate($event))->onQueue('high'));
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Slack\Jobs\DeleteChat;
use Slack\Interactive\BlockActions;
class BlockActionListener
{
protected const LOGKEY = 'LBA';
// Block actions arent queued, since slack expects a response to the request
//public $queue = 'high';
/**
* Handle the event.
*
* @param BlockActions $event
* @return void
* @throws \Exception
*/
public function handle(BlockActions $event): void
{
// Do some magic with event data
Log::info(sprintf('%s:Block Action for Callback [%s] User [%s] in [%s]',self::LOGKEY,$event->callback_id,$event->user_id,$event->team_id),['m'=>__METHOD__]);
switch ($event->callback_id) {
// Messages that generate a block action dont have a callback ID
case NULL:
Log::debug(sprintf('%s:Callback NULL [%s] (%s)',self::LOGKEY,$event->isMessage(),$event->action_id),['m'=>__METHOD__]);
$this->messageEvent($event);
break;
default:
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',self::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
}
/**
* Events from messages
*
* @param BlockActions $event
* @throws \Exception
*/
protected function messageEvent(BlockActions $event): void
{
Log::info(sprintf('%s:[%s] has [%d] actions',self::LOGKEY,$event->callback_id,count($event->actions)),['m'=>__METHOD__]);
foreach ($event->actions as $id => $action) {
$event->index = $id;
Log::debug(sprintf('%s:Action [%s]',self::LOGKEY,$event->action_id),['m'=>__METHOD__]);
switch ($event->action_id) {
case 'self_destruct':
// Queue the delete of the message
dispatch((new DeleteChat($event->user(),$event->message_ts))->onQueue('low'));
// @todo If this message is on integrations messages channel, which is not the user_id() - need to use the user's integration direct channel ID
break;
default:
Log::notice(sprintf('%s:Unhandled ACTION [%s]',self::LOGKEY,$event->action_id),['m'=>__METHOD__]);
}
}
}
/**
* Store data coming in from a block action dialog
*
* @param BlockActions $event
*/
protected function store(BlockActions $event): void
{
foreach ($event->actions as $id => $action) {
$event->index = $id;
switch ($event->action_id) {
default:
Log::notice(sprintf('%s:Unhandled ACTION [%s]',static::LOGKEY,$event->action_id),['m'=>__METHOD__]);
}
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Event\MemberJoinedChannel;
class ChannelJoinListener implements ShouldQueue
{
private const LOGKEY = 'LCJ';
public $queue = 'high';
/**
* Handle the event.
*
* @param MemberJoinedChannel $event
* @return void
*/
public function handle(MemberJoinedChannel $event)
{
// Do some magic with event data
Log::info(sprintf('%s:User [%s] joined Channel [%s]',self::LOGKEY,$event->invited,$event->channel_id),['m'=>__METHOD__]);
if ($event->team()->bot->user_id == $event->invited) {
$o = $event->channel(TRUE);
$o->active = TRUE;
$o->save();
Log::debug(sprintf('%s:BOT [%s] Joined Channel [%s]',self::LOGKEY,$event->invited,$event->channel_id),['m'=>__METHOD__]);
} else {
Log::debug(sprintf('%s:Wasnt the BOT who joined Channel [%s]',self::LOGKEY,$event->channel_id),['m'=>__METHOD__]);
}
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Event\{Base,ChannelLeft,GroupLeft};
class ChannelLeftListener implements ShouldQueue
{
private const LOGKEY = 'LCL';
public $queue = 'high';
/**
* Handle the event.
*
* @param Base $event
* @return void
*/
public function handle(Base $event)
{
if (! $event instanceof ChannelLeft AND ! $event instanceof GroupLeft)
abort(500,'Wrong class calling this listener? '.get_class($event));
// Do some magic with event data
Log::info(sprintf('%s:Bot Left Channel [%s]',self::LOGKEY,$event->channel_id),['m'=>__METHOD__,'c'=>$event->channel_id]);
$o = $event->channel(TRUE);
$o->active = FALSE;
$o->save();
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Interactive\InteractiveMessage;
class InteractiveMessageListener implements ShouldQueue
{
private const LOGKEY = 'LIM';
public $queue = 'high';
/**
* Handle the event.
*
* @param InteractiveMessage $event
* @return void
*/
public function handle(InteractiveMessage $event)
{
// Do some magic with event data
Log::info(sprintf('%s:Interactive Message for Callback [%s] User [%s] in [%s]',self::LOGKEY,$event->callback_id,$event->user_id,$event->team_id),['m'=>__METHOD__]);
switch ($event->callback_id) {
default:
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',self::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Event\Message;
class MessageListener implements ShouldQueue
{
private const LOGKEY = 'LM-';
public $queue = 'high';
/**
* Handle the event.
*
* @param Message $event
* @return void
*/
public function handle(Message $event)
{
// Do some magic with event data
Log::info(sprintf('%s:Message event [%s] - subtype [%s]',self::LOGKEY,$event->ts,$event->type),['m'=>__METHOD__]);
switch ($event->type) {
case 'channel_join': // Handled by another event (member_joined_channel - needs to be defined in Event Subscriptions)
case 'group_join': // Handled by another event (member_joined_channel - needs to be defined in Event Subscriptions)
case 'message_changed':
Log::debug(sprintf('%s:Ignoring message subtype [%s]',self::LOGKEY,$event->type),['m'=>__METHOD__]);
break;
default:
Log::notice(sprintf('%s:Unhandled TYPE [%s]',self::LOGKEY,$event->type),['m'=>__METHOD__]);
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Event\PinAdded;
class PinAddedListener implements ShouldQueue
{
private const LOGKEY = 'LPA';
public $queue = 'high';
/**
* Handle the event.
*
* @param PinAdded $event
* @return void
*/
public function handle(PinAdded $event)
{
// Do some magic with event data
Log::info(sprintf('%s:Pin Added to message [%s] in [%s]',self::LOGKEY,$event->ts,$event->channel_id),['m'=>__METHOD__]);
Log::notice(sprintf('%s:Ignoring Pin Add on [%s]',static::LOGKEY,$event->ts),['m'=>__METHOD__]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Event\PinRemoved;
class PinRemovedListener implements ShouldQueue
{
private const LOGKEY = 'LPR';
public $queue = 'high';
/**
* Handle the event.
*
* @param PinRemoved $event
* @return void
*/
public function handle(PinRemoved $event)
{
// Do some magic with event data
Log::info(sprintf('%s:Pin Removed from message [%s] in [%s]',self::LOGKEY,$event->ts,$event->channel_id),['m'=>__METHOD__]);
Log::debug(sprintf('%s:Ignoring Pin Remove on [%s]',static::LOGKEY,$event->ts),['m'=>__METHOD__]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Event\ReactionAdded;
class ReactionAddedListener implements ShouldQueue
{
private const LOGKEY = 'LRA';
public $queue = 'high';
/**
* Handle the event.
*
* @param ReactionAdded $event
* @return void
*/
public function handle(ReactionAdded $event)
{
// Do some magic with event data
Log::info(sprintf('%s:Reaction [%s] added to message in [%s]',self::LOGKEY,$event->reaction,$event->team_id),['m'=>__METHOD__]);
Log::debug(sprintf('%s:Ignoring Reaction Add [%s] on [%s]',static::LOGKEY,$event->reaction,$event->ts),['m'=>__METHOD__]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Listeners;
use Illuminate\Support\Facades\Log;
use Slack\Blockkit\Blocks;
use Slack\Blockkit\Modal;
use Slack\Interactive\Shortcut;
use Slack\Message;
class ShortcutListener //implements ShouldQueue
{
private const LOGKEY = 'LSC';
// Block actions arent queued, since slack expects a response to the request
//public $queue = 'high';
/**
* Handle the event.
*
* @param Shortcut $event
* @return void
* @todo To Test
*/
public function handle(Shortcut $event): Message
{
if (! $event->channel() || ! $event->channel()->active) {
$modal = new Modal(Blocks\TextEmoji::item(config('app.name')));
$blocks = new Blocks;
$blocks->addHeader(':robot_face: Bot not in this channel');
$blocks->addSectionText(Blocks\Text::item('Please add the BOT to this channel and try this again.'));
$modal->setBlocks($blocks);
try {
$event->team()->slackAPI()->viewOpen($event->trigger_id,json_encode($modal));
} catch (\Exception $e) {
Log::error(sprintf('%s:Got an error posting view to slack: %s',static::LOGKEY,$e->getMessage()),['m'=>__METHOD__]);
}
return (new Message)->blank();
}
// Do some magic with event data
Log::info(sprintf('%s:Shortcut [%s] triggered for: [%s]',self::LOGKEY,$event->callback_id,$event->team_id),['m'=>__METHOD__]);
switch ($event->callback_id) {
default:
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',self::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Interactive\ViewClosed;
class ViewClosedListener implements ShouldQueue
{
private const LOGKEY = 'LVC';
public $queue = 'high';
/**
* Handle the event.
*
* @param ViewClosed $event
* @return void
*/
public function handle(ViewClosed $event)
{
// Do some magic with event data
Log::info(sprintf('%s:Block Action for Callback [%s] User [%s] in [%s]',self::LOGKEY,$event->callback_id,$event->user_id,$event->team_id),['m'=>__METHOD__]);
switch ($event->callback_id) {
default:
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',self::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Slack\Listeners;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Slack\Interactive\ViewSubmission;
class ViewSubmissionListener implements ShouldQueue
{
private const LOGKEY = 'LVC';
public $queue = 'high';
/**
* Handle the event.
*
* @param ViewSubmission $event
* @return void
*/
public function handle(ViewSubmission $event)
{
// Do some magic with event data
Log::info(sprintf('%s:View Submission for Callback [%s] User [%s] in [%s]',self::LOGKEY,$event->callback_id,$event->user_id,$event->team_id),['m'=>__METHOD__]);
switch ($event->callback_id) {
default:
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',self::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
}
}

View File

@ -6,11 +6,11 @@ use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Slack\Jobs\DeleteChat;
use Slack\Models\{Channel,User};
use Slack\Blockkit\Block;
use Slack\Blockkit\Blocks;
use Slack\Exceptions\SlackException;
use Slack\Message\Attachment;
use Slack\Jobs\DeleteChat;
use Slack\Message\Attachments;
use Slack\Models\{Channel,User};
use Slack\Response\Generic;
/**
@ -20,55 +20,48 @@ class Message implements \JsonSerializable
{
protected const LOGKEY = 'SM-';
private $o;
private $attachments;
private $blocks;
private Model $o;
private Blocks $blocks;
/**
* Message constructor.
*
* @param Model|null $o Who the message will be to - Channel or User
* @throws SlackException
*/
public function __construct(Model $o=NULL)
{
$this->_data = collect();
// Message is to a channel
if ($o instanceof Channel) {
$this->setChannel($o);
if ($o) {
// Message is to a channel
if ($o instanceof Channel) {
$this->setChannel($o);
// Message is to a user
} elseif ($o instanceof User) {
$this->setUser($o);
// Message is to a user
} elseif ($o instanceof User) {
$this->setUser($o);
} else {
throw new SlackException('Model not handled: '.get_class($o));
}
$this->o = $o;
}
$this->o = $o;
$this->attachments = collect();
$this->blocks = collect();
}
/**
* Add an attachment to a message
*
* @param Attachment $attachment
* @return Message
*/
public function addAttachment(Attachment $attachment): self
{
$this->attachments->push($attachment);
return $this;
$this->blocks = new Blocks;
}
/**
* Add a block to the message
*
* @param BlockKit $block
* @return $this
* @param Blocks $blocks
* @return Message
* @todo to test
*/
public function addBlock(BlockKit $block): self
public function addBlock(Blocks $blocks): self
{
$this->blocks->push($block);
$this->blocks = $blocks;
return $this;
}
@ -76,11 +69,12 @@ class Message implements \JsonSerializable
/**
* Empty the message
*
* @return $this
* @return Message
*/
public function blank(): self
{
$this->_data = collect();
$this->blocks = new Blocks;
return $this;
}
@ -117,15 +111,8 @@ class Message implements \JsonSerializable
*/
public function jsonSerialize()
{
if ($this->blocks->count()) {
if ($this->_data->has('text'))
throw new \Exception('Messages cannot have text and blocks!');
if ($this->blocks->count())
$this->_data->put('blocks',$this->blocks);
}
if ($this->attachments->count())
$this->_data->put('attachments',$this->attachments);
// For interactive messages that generate a dialog, we need to return NULL
return $this->_data->count() ? $this->_data : NULL;
@ -143,6 +130,9 @@ class Message implements \JsonSerializable
if ($this->_data->has('ephemeral'))
abort('500','Cannot post ephemeral messages.');
if ($this->blocks->count() && $this->_data->get('attachments'))
throw new SlackException('Message cannot have blocks and attachments.');
$api = $this->o->team->slackAPI();
$response = $this->_data->has('ts') ? $api->updateMessage($this) : $api->postMessage($this);
@ -156,18 +146,33 @@ class Message implements \JsonSerializable
return $response;
}
public function replace(bool $replace=TRUE): self
public function setReplace(bool $replace=TRUE): self
{
$this->_data['replace_original'] = $replace ? 'true' : 'false';
$this->_data->put('replace_original',$replace ? 'true' : 'false');
return $this;
}
/**
* To slack from rendering URLs in the message
*
* @param bool $unfurl
* @return $this
*/
public function setUnfurlLinks(bool $unfurl): self
{
$this->_data->put('unfurl_links',$unfurl ? 'true' : 'false');
return $this;
}
/**
* Post a message to slack using the respond_url
* @note This URL can only be used 5 times in 30 minutes
*
* @note This URL can only be used 5 times in 30 minutes
* @param string $url
* @return string
* @throws SlackException
*/
public function respond(string $url)
{
@ -212,23 +217,53 @@ class Message implements \JsonSerializable
*/
public function selfdestruct(Carbon $time): Generic
{
$this->addBlock(
(new Block)->addContext(
$this->blocks->addContextElements(
collect()
->push((new BlockKit)->text(sprintf('This message will self destruct in %s...',$time->diffForHumans(Carbon::now(),['syntax' => CarbonInterface::DIFF_RELATIVE_TO_NOW]))))));
->push(Blocks\Text::item(sprintf('This message will self destruct in %s...',$time->diffForHumans(Carbon::now(),['syntax' => CarbonInterface::DIFF_RELATIVE_TO_NOW])))));
return $this->post($time);
}
/**
* Add an attachment to a message
*
* @param Attachment $attachments
* @return Message
*/
public function setAttachments(Attachments $attachments): self
{
$this->_data->put('attachments',[$attachments]);
return $this;
}
/**
* Add blocks to the message
*
* @param Blocks $blocks
* @return Message
* @throws \Exception
*/
public function setBlocks(Blocks $blocks): self
{
if ($this->blocks->count())
throw new \Exception('Blocks already defined');
$this->blocks = $blocks;
return $this;
}
/**
* Set our channel
*
* @param Channel $o
* @return Message
*/
public function setChannel(Channel $o)
public function setChannel(Channel $o): self
{
$this->_data['channel'] = $o->channel_id;
$this->_data->put('channel',$o->channel_id);
$this->o = $o;
return $this;
}
@ -237,7 +272,7 @@ class Message implements \JsonSerializable
* Set the icon next to the message
*
* @param string $icon
* @return $this
* @return Message
* @deprecated
*/
public function setIcon(string $icon): self
@ -255,7 +290,7 @@ class Message implements \JsonSerializable
*/
public function setOptionGroup(array $array): void
{
$this->_data = collect();
$this->_data = collect(); // @todo Why are clearing our data?
$this->_data->put('option_groups',$array);
}
@ -263,7 +298,7 @@ class Message implements \JsonSerializable
* Message text
*
* @param string $string
* @return $this
* @return Message
*/
public function setText(string $string): self
{
@ -272,6 +307,12 @@ class Message implements \JsonSerializable
return $this;
}
/**
* Set the timestamp, used when replacing messages
*
* @param string $string
* @return Message
*/
public function setTS(string $string): self
{
$this->_data->put('ts',$string);
@ -279,6 +320,12 @@ class Message implements \JsonSerializable
return $this;
}
/**
* Set the thread timestamp, used when adding a threaded response
*
* @param string $string
* @return Message
*/
public function setThreadTS(string $string): self
{
$this->_data->put('thread_ts',$string);
@ -292,16 +339,17 @@ class Message implements \JsonSerializable
* @param User $o
* @return Message
*/
public function setUser(User $o)
public function setUser(User $o): self
{
$this->_data['channel'] = $o->user_id;
$this->_data->put('channel',$o->user_id);
$this->o = $o;
return $this;
}
public function setUserName(string $user)
public function setUserName(string $user): self
{
$this->_data['username'] = $user;
$this->_data->put('username',$user);
return $this;
}

View File

@ -4,6 +4,7 @@ namespace Slack\Message;
use Slack\BlockKit;
use Slack\Blockkit\BlockAction;
use Slack\Blockkit\Blocks;
/**
* Class MessageAttachment - Slack Message Attachments
@ -11,23 +12,27 @@ use Slack\Blockkit\BlockAction;
*
* @package Slack\Message
*/
class Attachment implements \JsonSerializable
class Attachments implements \JsonSerializable
{
private $_data;
private $actions;
private $blocks;
//private $blocks;
private $blockactions;
// @todo To rework
public function __construct()
{
$this->actions = collect();
$this->blocks = collect();
//$this->blocks = collect();
$this->blockactions = collect();
$this->_data = collect();
}
// @todo To rework
public function jsonSerialize()
{
/*
if ($this->actions->count() AND ! $this->_data->has('callback_id'))
abort(500,'Actions without a callback ID');
@ -47,15 +52,16 @@ class Attachment implements \JsonSerializable
if ($this->blocks->count())
$this->_data->put('blocks',$this->blocks);
*/
return $this->_data;
}
/**
* Add an attachment to a message
*
* @param AttachmentAction $action
* @return Attachment
* @todo To rework
*/
public function addAction(AttachmentAction $action): self
{
@ -69,6 +75,7 @@ class Attachment implements \JsonSerializable
*
* @param BlockKit $block
* @return Attachment
* @deprecated
*/
public function addBlock(BlockKit $block): self
{
@ -82,6 +89,7 @@ class Attachment implements \JsonSerializable
*
* @param BlockAction $action
* @return $this
* @todo To rework
*/
public function addBlockAction(BlockAction $action): self
{
@ -90,6 +98,8 @@ class Attachment implements \JsonSerializable
return $this;
}
//* @todo To rework
public function addField(string $title,string $value,bool $short): self
{
if (! $this->_data->has('fields'))
@ -103,12 +113,12 @@ class Attachment implements \JsonSerializable
return $this;
}
/**
* Set where markdown should be parsed by slack
*
* @param array $array
* @return $this
* @todo To rework
*/
public function markdownIn(array $array): self
{
@ -123,6 +133,7 @@ class Attachment implements \JsonSerializable
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setCallbackID(string $string): self
{
@ -131,11 +142,25 @@ class Attachment implements \JsonSerializable
return $this;
}
/**
* Add a blocks to the message attachment
*
* @param Blocks $blocks
* @return self
*/
public function setBlocks(Blocks $blocks): self
{
$this->_data->put('blocks',$blocks);
return $this;
}
/**
* Configure the attachment color (on the left of the attachment)
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setColor(string $string): self
{
@ -149,6 +174,7 @@ class Attachment implements \JsonSerializable
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setFooter(string $string): self
{
@ -162,6 +188,7 @@ class Attachment implements \JsonSerializable
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setPretext(string $string): self
{
@ -175,6 +202,7 @@ class Attachment implements \JsonSerializable
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setText(string $string): self
{
@ -188,6 +216,7 @@ class Attachment implements \JsonSerializable
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setTitle(string $string): self
{

View File

@ -53,4 +53,18 @@ class Channel extends Model
{
return preg_match('/^D/',$this->channel_id) OR $this->name == 'directmessage';
}
/**
* Return a slack URL to the timestamp
*
* @param string $ts
* @return string
*/
public function url(string $ts): string
{
if (! $this->team->url)
return '';
return sprintf('https://%s/archives/%s/p%s',$this->team->url,$this->channel_id,str_replace('.','',$ts));
}
}

View File

@ -64,6 +64,15 @@ class Team extends Model
return implode('-',$attrs);
}
/**
* Return the team's slack URL
* @return string
*/
public function getUrlAttribute(): string
{
return $this->team_name ? sprintf('%s.slack.com',$this->team_name) : '';
}
/* METHODS */
/**

View File

@ -2,7 +2,11 @@
namespace Slack\Providers;
use Illuminate\Notifications\ChannelManager;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ServiceProvider;
use Slack\API;
use Slack\Channels\SlackBotChannel;
use Slack\Console\Commands\SlackSocketClient;
class SlackServiceProvider extends ServiceProvider
@ -35,5 +39,11 @@ class SlackServiceProvider extends ServiceProvider
$this->mergeConfigFrom(__DIR__.'/../config/slack.php','slack');
$this->loadRoutesFrom(realpath(__DIR__ .'/../routes.php'));
Notification::resolved(function (ChannelManager $service) {
$service->extend('slackapp', function ($app) {
return new SlackBotChannel($app->make(API::class));
});
});
}
}

View File

@ -40,7 +40,7 @@ class SlackIntegration extends Migration
$table->timestamps();
$table->string('team_id', 45)->unique();
$table->string('name')->nullable();
$table->string('team_name')->nullable();
$table->string('description')->nullable();
$table->boolean('active');
@ -62,8 +62,8 @@ class SlackIntegration extends Migration
$table->string('name')->nullable();
$table->boolean('active');
$table->integer('enterprise_id')->nullable()->unsigned();
$table->foreign('enterprise_id')->references('id')->on('slack_enterprises');
$table->integer('team_id')->nullable()->unsigned();
$table->foreign('team_id')->references('id')->on('slack_teams');
});
Schema::table('slack_users', function (Blueprint $table) {