From 4533b1d0edde0b199f4a44e032cbb47d272690ba Mon Sep 17 00:00:00 2001 From: Deon George Date: Tue, 10 Aug 2021 13:48:59 +1000 Subject: [PATCH] Enhancements --- src/API.php | 2 +- src/Base.php | 5 +- src/BlockKit.php | 84 +---- src/Blockkit/Block.php | 256 ------------- src/Blockkit/BlockAction.php | 7 +- src/Blockkit/Blocks.php | 341 ++++++++++++++++++ src/Blockkit/Blocks/Button.php | 30 ++ src/Blockkit/Blocks/Input.php | 24 ++ src/Blockkit/Blocks/Text.php | 20 + src/Blockkit/Blocks/TextEmoji.php | 22 ++ src/Blockkit/Input/Element.php | 22 ++ src/Blockkit/Modal.php | 24 +- src/Channels/SlackBotChannel.php | 20 + src/Client/API.php | 5 +- src/Client/SocketMode.php | 80 +++- src/Command/Base.php | 23 +- src/Command/Factory.php | 29 +- src/Command/Help.php | 10 +- src/Command/Unknown.php | 2 +- src/Console/Commands/SlackSocketClient.php | 17 +- src/Event/Base.php | 2 +- src/Event/Factory.php | 15 +- src/Event/Unknown.php | 4 +- src/Http/Controllers/SlackAppController.php | 27 +- src/Interactive/Base.php | 5 +- src/Interactive/BlockActions.php | 86 +++-- src/Interactive/Factory.php | 16 +- src/Interactive/Unknown.php | 2 +- src/Listeners/AppHomeOpenedListener.php | 30 ++ src/Listeners/BlockActionListener.php | 85 +++++ src/Listeners/ChannelJoinListener.php | 38 ++ src/Listeners/ChannelLeftListener.php | 34 ++ src/Listeners/InteractiveMessageListener.php | 32 ++ src/Listeners/MessageListener.php | 38 ++ src/Listeners/PinAddedListener.php | 29 ++ src/Listeners/PinRemovedListener.php | 29 ++ src/Listeners/ReactionAddedListener.php | 29 ++ src/Listeners/ShortcutListener.php | 54 +++ src/Listeners/ViewClosedListener.php | 32 ++ src/Listeners/ViewSubmissionListener.php | 32 ++ src/Message.php | 162 ++++++--- .../{Attachment.php => Attachments.php} | 39 +- src/Models/Channel.php | 14 + src/Models/Team.php | 9 + src/Providers/SlackServiceProvider.php | 10 + .../2021_08_06_002815_slack_integration.php | 6 +- 46 files changed, 1334 insertions(+), 548 deletions(-) delete mode 100644 src/Blockkit/Block.php create mode 100644 src/Blockkit/Blocks.php create mode 100644 src/Blockkit/Blocks/Button.php create mode 100644 src/Blockkit/Blocks/Input.php create mode 100644 src/Blockkit/Blocks/Text.php create mode 100644 src/Blockkit/Blocks/TextEmoji.php create mode 100644 src/Blockkit/Input/Element.php create mode 100644 src/Channels/SlackBotChannel.php create mode 100644 src/Listeners/AppHomeOpenedListener.php create mode 100644 src/Listeners/BlockActionListener.php create mode 100644 src/Listeners/ChannelJoinListener.php create mode 100644 src/Listeners/ChannelLeftListener.php create mode 100644 src/Listeners/InteractiveMessageListener.php create mode 100644 src/Listeners/MessageListener.php create mode 100644 src/Listeners/PinAddedListener.php create mode 100644 src/Listeners/PinRemovedListener.php create mode 100644 src/Listeners/ReactionAddedListener.php create mode 100644 src/Listeners/ShortcutListener.php create mode 100644 src/Listeners/ViewClosedListener.php create mode 100644 src/Listeners/ViewSubmissionListener.php rename src/Message/{Attachment.php => Attachments.php} (85%) diff --git a/src/API.php b/src/API.php index 9f20357..30cc00a 100644 --- a/src/API.php +++ b/src/API.php @@ -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 diff --git a/src/Base.php b/src/Base.php index 33a538c..16e6347 100644 --- a/src/Base.php +++ b/src/Base.php @@ -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(); } diff --git a/src/BlockKit.php b/src/BlockKit.php index c26d4e9..6344e00 100644 --- a/src/BlockKit.php +++ b/src/BlockKit.php @@ -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, - ]; - } } diff --git a/src/Blockkit/Block.php b/src/Blockkit/Block.php deleted file mode 100644 index 8035938..0000000 --- a/src/Blockkit/Block.php +++ /dev/null @@ -1,256 +0,0 @@ -_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); - } - -} diff --git a/src/Blockkit/BlockAction.php b/src/Blockkit/BlockAction.php index c6995a7..1c8dbcf 100644 --- a/src/Blockkit/BlockAction.php +++ b/src/Blockkit/BlockAction.php @@ -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) diff --git a/src/Blockkit/Blocks.php b/src/Blockkit/Blocks.php new file mode 100644 index 0000000..41fc630 --- /dev/null +++ b/src/Blockkit/Blocks.php @@ -0,0 +1,341 @@ +_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); + } +} diff --git a/src/Blockkit/Blocks/Button.php b/src/Blockkit/Blocks/Button.php new file mode 100644 index 0000000..35756cb --- /dev/null +++ b/src/Blockkit/Blocks/Button.php @@ -0,0 +1,30 @@ +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); + } +} \ No newline at end of file diff --git a/src/Blockkit/Blocks/Input.php b/src/Blockkit/Blocks/Input.php new file mode 100644 index 0000000..eb04389 --- /dev/null +++ b/src/Blockkit/Blocks/Input.php @@ -0,0 +1,24 @@ +type = 'input'; + $this->element = $element; + $this->label = $label; + } + + public static function item(Element $element,TextEmoji $label): self + { + return new self($element,$label); + } +} \ No newline at end of file diff --git a/src/Blockkit/Blocks/Text.php b/src/Blockkit/Blocks/Text.php new file mode 100644 index 0000000..7200e10 --- /dev/null +++ b/src/Blockkit/Blocks/Text.php @@ -0,0 +1,20 @@ +type = $type; + $this->text = $text; + } + + public static function item(string $text,string $type='mrkdwn'): self + { + return new self($text,$type); + } +} \ No newline at end of file diff --git a/src/Blockkit/Blocks/TextEmoji.php b/src/Blockkit/Blocks/TextEmoji.php new file mode 100644 index 0000000..830ab6f --- /dev/null +++ b/src/Blockkit/Blocks/TextEmoji.php @@ -0,0 +1,22 @@ +emoji = $emoji; + $this->text = $text; + $this->type = 'plain_text'; + } + + public static function item(string $text,bool $emoji=TRUE): self + { + return new self($text,$emoji); + } +} \ No newline at end of file diff --git a/src/Blockkit/Input/Element.php b/src/Blockkit/Input/Element.php new file mode 100644 index 0000000..f02c7a6 --- /dev/null +++ b/src/Blockkit/Input/Element.php @@ -0,0 +1,22 @@ +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); + } +} \ No newline at end of file diff --git a/src/Blockkit/Modal.php b/src/Blockkit/Modal.php index 7db40a2..48c5893 100644 --- a/src/Blockkit/Modal.php +++ b/src/Blockkit/Modal.php @@ -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; } + */ } diff --git a/src/Channels/SlackBotChannel.php b/src/Channels/SlackBotChannel.php new file mode 100644 index 0000000..0851398 --- /dev/null +++ b/src/Channels/SlackBotChannel.php @@ -0,0 +1,20 @@ +routeNotificationFor('slackapp',$notification)) { + return; + } + + $o = $notification->toSlack($notifiable); + $o->setChannel($co); + + return $o->post(); + } +} \ No newline at end of file diff --git a/src/Client/API.php b/src/Client/API.php index 84875d0..64479d4 100644 --- a/src/Client/API.php +++ b/src/Client/API.php @@ -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) { diff --git a/src/Client/SocketMode.php b/src/Client/SocketMode.php index e398967..5282b0b 100644 --- a/src/Client/SocketMode.php +++ b/src/Client/SocketMode.php @@ -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__]); } } diff --git a/src/Command/Base.php b/src/Command/Base.php index d520a83..d73b23c 100644 --- a/src/Command/Base.php +++ b/src/Command/Base.php @@ -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; + } } diff --git a/src/Command/Factory.php b/src/Command/Factory.php index 4a13d16..f5512ee 100644 --- a/src/Command/Factory.php +++ b/src/Command/Factory.php @@ -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')); } } diff --git a/src/Command/Help.php b/src/Command/Help.php index b4c2a47..1868fcb 100644 --- a/src/Command/Help.php +++ b/src/Command/Help.php @@ -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; } diff --git a/src/Command/Unknown.php b/src/Command/Unknown.php index 1b69a6e..cf03919 100644 --- a/src/Command/Unknown.php +++ b/src/Command/Unknown.php @@ -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__]); diff --git a/src/Console/Commands/SlackSocketClient.php b/src/Console/Commands/SlackSocketClient.php index 0babf79..f111f53 100644 --- a/src/Console/Commands/SlackSocketClient.php +++ b/src/Console/Commands/SlackSocketClient.php @@ -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(); diff --git a/src/Event/Base.php b/src/Event/Base.php index a8c4aad..3c7bd6a 100644 --- a/src/Event/Base.php +++ b/src/Event/Base.php @@ -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__]); diff --git a/src/Event/Factory.php b/src/Event/Factory.php index 039c1aa..a2817c0 100644 --- a/src/Event/Factory.php +++ b/src/Event/Factory.php @@ -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; diff --git a/src/Event/Unknown.php b/src/Event/Unknown.php index c0db70a..86a8486 100644 --- a/src/Event/Unknown.php +++ b/src/Event/Unknown.php @@ -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); } } diff --git a/src/Http/Controllers/SlackAppController.php b/src/Http/Controllers/SlackAppController.php index aa08d69..35c24c6 100644 --- a/src/Http/Controllers/SlackAppController.php +++ b/src/Http/Controllers/SlackAppController.php @@ -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 %s."',$so->description); + return sprintf('All set up! Head back to your slack instance %s.',$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 [ diff --git a/src/Interactive/Base.php b/src/Interactive/Base.php index 4f9ceaf..e5a31bd 100644 --- a/src/Interactive/Base.php +++ b/src/Interactive/Base.php @@ -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); } /** diff --git a/src/Interactive/BlockActions.php b/src/Interactive/BlockActions.php index 13c624b..1d513ef 100644 --- a/src/Interactive/BlockActions.php +++ b/src/Interactive/BlockActions.php @@ -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; - } } diff --git a/src/Interactive/Factory.php b/src/Interactive/Factory.php index 6c0b99b..ad7afdd 100644 --- a/src/Interactive/Factory.php +++ b/src/Interactive/Factory.php @@ -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; diff --git a/src/Interactive/Unknown.php b/src/Interactive/Unknown.php index cc043c6..6a4bf89 100644 --- a/src/Interactive/Unknown.php +++ b/src/Interactive/Unknown.php @@ -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__]); diff --git a/src/Listeners/AppHomeOpenedListener.php b/src/Listeners/AppHomeOpenedListener.php new file mode 100644 index 0000000..aca56b7 --- /dev/null +++ b/src/Listeners/AppHomeOpenedListener.php @@ -0,0 +1,30 @@ +user_id,$event->team_id),['m'=>__METHOD__]); + + dispatch((new SlackHomeTabUpdate($event))->onQueue('high')); + } +} diff --git a/src/Listeners/BlockActionListener.php b/src/Listeners/BlockActionListener.php new file mode 100644 index 0000000..1c7f9b0 --- /dev/null +++ b/src/Listeners/BlockActionListener.php @@ -0,0 +1,85 @@ +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__]); + } + } + } +} diff --git a/src/Listeners/ChannelJoinListener.php b/src/Listeners/ChannelJoinListener.php new file mode 100644 index 0000000..b35c88d --- /dev/null +++ b/src/Listeners/ChannelJoinListener.php @@ -0,0 +1,38 @@ +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__]); + } + } +} diff --git a/src/Listeners/ChannelLeftListener.php b/src/Listeners/ChannelLeftListener.php new file mode 100644 index 0000000..fc1a086 --- /dev/null +++ b/src/Listeners/ChannelLeftListener.php @@ -0,0 +1,34 @@ +channel_id),['m'=>__METHOD__,'c'=>$event->channel_id]); + + $o = $event->channel(TRUE); + $o->active = FALSE; + $o->save(); + } +} diff --git a/src/Listeners/InteractiveMessageListener.php b/src/Listeners/InteractiveMessageListener.php new file mode 100644 index 0000000..24e23b5 --- /dev/null +++ b/src/Listeners/InteractiveMessageListener.php @@ -0,0 +1,32 @@ +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__]); + } + } +} diff --git a/src/Listeners/MessageListener.php b/src/Listeners/MessageListener.php new file mode 100644 index 0000000..d9afced --- /dev/null +++ b/src/Listeners/MessageListener.php @@ -0,0 +1,38 @@ +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__]); + } + } +} diff --git a/src/Listeners/PinAddedListener.php b/src/Listeners/PinAddedListener.php new file mode 100644 index 0000000..1926b8a --- /dev/null +++ b/src/Listeners/PinAddedListener.php @@ -0,0 +1,29 @@ +ts,$event->channel_id),['m'=>__METHOD__]); + + Log::notice(sprintf('%s:Ignoring Pin Add on [%s]',static::LOGKEY,$event->ts),['m'=>__METHOD__]); + } +} diff --git a/src/Listeners/PinRemovedListener.php b/src/Listeners/PinRemovedListener.php new file mode 100644 index 0000000..c6c27f8 --- /dev/null +++ b/src/Listeners/PinRemovedListener.php @@ -0,0 +1,29 @@ +ts,$event->channel_id),['m'=>__METHOD__]); + + Log::debug(sprintf('%s:Ignoring Pin Remove on [%s]',static::LOGKEY,$event->ts),['m'=>__METHOD__]); + } +} diff --git a/src/Listeners/ReactionAddedListener.php b/src/Listeners/ReactionAddedListener.php new file mode 100644 index 0000000..48e1770 --- /dev/null +++ b/src/Listeners/ReactionAddedListener.php @@ -0,0 +1,29 @@ +reaction,$event->team_id),['m'=>__METHOD__]); + + Log::debug(sprintf('%s:Ignoring Reaction Add [%s] on [%s]',static::LOGKEY,$event->reaction,$event->ts),['m'=>__METHOD__]); + } +} diff --git a/src/Listeners/ShortcutListener.php b/src/Listeners/ShortcutListener.php new file mode 100644 index 0000000..7d4d6c8 --- /dev/null +++ b/src/Listeners/ShortcutListener.php @@ -0,0 +1,54 @@ +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__]); + } + } +} diff --git a/src/Listeners/ViewClosedListener.php b/src/Listeners/ViewClosedListener.php new file mode 100644 index 0000000..b708f5f --- /dev/null +++ b/src/Listeners/ViewClosedListener.php @@ -0,0 +1,32 @@ +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__]); + } + } +} diff --git a/src/Listeners/ViewSubmissionListener.php b/src/Listeners/ViewSubmissionListener.php new file mode 100644 index 0000000..fdb8ec0 --- /dev/null +++ b/src/Listeners/ViewSubmissionListener.php @@ -0,0 +1,32 @@ +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__]); + } + } +} diff --git a/src/Message.php b/src/Message.php index 15a472c..f11f0db 100644 --- a/src/Message.php +++ b/src/Message.php @@ -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; } diff --git a/src/Message/Attachment.php b/src/Message/Attachments.php similarity index 85% rename from src/Message/Attachment.php rename to src/Message/Attachments.php index bc6015a..5d94df3 100644 --- a/src/Message/Attachment.php +++ b/src/Message/Attachments.php @@ -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 { diff --git a/src/Models/Channel.php b/src/Models/Channel.php index 2710776..55db525 100644 --- a/src/Models/Channel.php +++ b/src/Models/Channel.php @@ -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)); + } } diff --git a/src/Models/Team.php b/src/Models/Team.php index c8e1ea2..4eb5a7f 100644 --- a/src/Models/Team.php +++ b/src/Models/Team.php @@ -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 */ /** diff --git a/src/Providers/SlackServiceProvider.php b/src/Providers/SlackServiceProvider.php index 6e3f519..6b23745 100644 --- a/src/Providers/SlackServiceProvider.php +++ b/src/Providers/SlackServiceProvider.php @@ -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)); + }); + }); } } \ No newline at end of file diff --git a/src/database/migrations/2021_08_06_002815_slack_integration.php b/src/database/migrations/2021_08_06_002815_slack_integration.php index 3b23431..ffa6110 100644 --- a/src/database/migrations/2021_08_06_002815_slack_integration.php +++ b/src/database/migrations/2021_08_06_002815_slack_integration.php @@ -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) {