Compare commits

...

62 Commits

Author SHA1 Message Date
Deon George
1f4c5c9774 Users created shouldnt be enabled by default, a user update job should enable users after checking guest status 2022-09-09 15:58:28 +10:00
Deon George
67f78b2f5d Enhancements to Message::class making it more similar to Modal::class in terms of methods 2022-09-09 14:55:19 +10:00
Deon George
6c6d32b4eb Fix hash conflict dump error 2022-09-08 17:43:17 +10:00
Deon George
1923f4486b Modal clear_on_close/notify_on_close defaults to TRUE, change MultiExternalSelect initial_options to be a collection 2022-09-08 13:50:48 +10:00
Deon George
b289f29948 Change elements that require plain_text to only accept string , make public the consts that describe element limits 2022-09-07 19:27:30 +10:00
Deon George
e9980ff9fd Get value() by block_id when it isnt a form submission 2022-09-06 20:08:04 +10:00
Deon George
145e322317 Fixes for interactive messages, we use block_id to determine what needs to be done 2022-09-06 17:23:54 +10:00
Deon George
1b55d7ab52 Standardisation of callback_id/key/value and action_id/key/value, Message can only have 50 blocks 2022-09-06 11:31:30 +10:00
Deon George
be26c3c0a3 Consistent handling of Listeners methods (the should be overwritten in the application) 2022-09-06 10:19:56 +10:00
Deon George
521de13d92 BlockKit classes is now countable 2022-09-05 23:13:44 +10:00
Deon George
a1be3ccd09 Switch Message::respond() to use Http::class instead of curl directly 2022-09-05 22:26:08 +10:00
Deon George
2c791ccead Minor fixes 2022-09-05 17:17:43 +10:00
Deon George
1529f470fa Add __set method to Job and auto-initialize _data, update Message to check for a Team if the message is being used in a slack API 2022-09-05 17:17:43 +10:00
Deon George
905c207956 Switch API to use Http::class instead of curl directly 2022-09-05 17:17:43 +10:00
Deon George
15a6933026 Enable __set() in Jobs, so all data is stored in _data 2022-09-05 11:43:38 +10:00
Deon George
4ff944cb3a Implemented SlackSyntaxException and removed some deprecated functions 2022-09-05 11:43:38 +10:00
Deon George
4c7d18c6b0 App::environment is now 'local' not 'dev', consistent use of slack queue 'slack' 2022-09-05 11:43:38 +10:00
Deon George
ff00e88417 Options code tidyup 2022-09-05 11:43:38 +10:00
Deon George
b6dc14971f Listener code tidyup - consistent use of slack queue "slack" 2022-09-05 11:43:38 +10:00
Deon George
89c13bcb73 API code tidyup 2022-09-04 10:21:01 +10:00
Deon George
a68c7936a6 Add active to Team fillable 2022-09-02 23:49:44 +10:00
Deon George
3bac7dcf6b More functionality with emphemeral messages 2022-09-02 23:08:55 +10:00
Deon George
e9a4eae7d1 Enable sending ephemeral messages to Slack 2022-09-02 17:39:49 +10:00
Deon George
80c1768534 Fixes to getting values from buttons 2022-08-30 12:01:13 +10:00
Deon George
9611939c57 Fixes for Button elements 2022-08-25 13:44:03 +10:00
Deon George
64228796ae Add ExternalSelect, fix spelling for PlainTextInput, other minor syntax errors 2022-08-24 23:44:36 +10:00
Deon George
32a5d7f05a Added Blocks/Elements/MultiExternalSelect, ViewSubmission enhancements and Options/Base updates 2022-08-24 17:32:05 +10:00
Deon George
8c4f3413f2 Consistent use of LOGKEY and make classes final where there downstream app shouldnt override them 2022-08-24 14:21:18 +10:00
Deon George
5f14be0cae Add our table names to our models, since they are not determined automatically 2022-08-24 12:08:49 +10:00
Deon George
6b27945142 Rename our migration so that it is always first 2022-08-24 12:08:49 +10:00
Deon George
612798e67f BlockAction optimisation 2022-08-23 21:35:11 +10:00
Deon George
4691a3f775 Code styling update, no functional changes 2022-08-23 17:48:09 +10:00
Deon George
e5f803d122 Add SlashCommand controller, and change routes to slack/* and api/slack/* 2022-08-22 21:59:02 +10:00
Deon George
bb5539f158 Add callback_id to BlockActions, fix block::sections can also only have an accessory 2022-06-27 22:25:57 +10:00
Deon George
1e5281de29 More debugging trying to catch websocket connect errors 2022-06-22 10:56:45 +10:00
Deon George
b69220b1ce Record in the logs the contents of any Invalid JSON messages 2022-06-10 09:34:09 +10:00
Deon George
1cbab6df2a Update phpws due to vulenerabilities in earlier versions of laminas\ 2022-06-01 10:48:13 +10:00
Deon George
ec8173a339 We now use \Laminas as \Zend is abandoned 2022-06-01 10:20:38 +10:00
Deon George
a7f043b23e Enable overriding database Models by the calling application - those models must be in the Slack/ subdir 2022-05-19 17:51:26 +10:00
Deon George
eb73a67fa8 Add slack to composer.json keywords 2022-05-19 14:16:10 +10:00
Deon George
1c3966598d Change to how we resolve a user's team between Slack/ and App/* models 2022-02-24 13:56:47 +11:00
Deon George
c5a13046ed Added in HTTP interactive options messages and Controller 2022-02-24 12:28:05 +11:00
Deon George
29d3591125 Added in HTTP interactive messages and Controller 2022-02-24 12:28:05 +11:00
Deon George
dbd355555b added Message/AttachmentAction 2022-02-24 09:00:45 +11:00
Deon George
3cadf2b808 Changed calling open home screen job 2022-02-23 14:30:47 +11:00
Deon George
a98debe69a add Elements/StaticSelect, viewOpen now only takes a modal 2022-02-23 14:30:47 +11:00
Deon George
4ea6152194 Add Accessories/Overflow 2022-02-23 14:30:47 +11:00
Deon George
181365f456 Added Input/PlainTextInput, some minor fixes 2022-02-23 11:32:32 +11:00
Deon George
6b16d07d80 Incorporate HTTP endpoint logic so we can now do websockets or HTTP endpoints 2022-02-23 11:32:32 +11:00
Deon George
b0c3897e45 Remove calls to new Block that were missed from #b2cd5c7 2022-02-01 08:57:35 +11:00
Deon George
3a7f411020 Show size of chars in debug 2022-01-19 16:43:29 +11:00
Deon George
b2cd5c7d46 Rework BlockKit to include validation on size and counts 2022-01-19 14:06:04 +11:00
Deon George
b20d7e2988 Fix when user is not defined and a test is made to $user->is_admin 2021-12-22 10:12:45 +11:00
Deon George
0edfa512c7 Fix for creating users - active wasnt included. Also make admin default to FALSE 2021-12-18 10:30:06 +11:00
Deon George
0414760cf0 Add some helper functions 2021-12-15 11:27:36 +11:00
Deon George
44fd2207fe Change add to slack image to inline svg 2021-12-15 09:48:34 +11:00
Deon George
104bc217fc Change to use leenooks/phpws, for DNS_SERVER fixes 2021-12-09 15:53:48 +11:00
Deon George
2514bafd97 Added more slack debugging when using socket mode 2021-12-09 13:35:23 +11:00
Deon George
66031eff47 Add scheduled messages 2021-12-08 14:21:57 +11:00
Deon George
1a5371a7b9 Seems commands __construct() is called during composer dump autoload 2021-12-07 14:57:46 +11:00
Deon George
7ee2df607c
Add ability to overrload SlackBotChannel::class with a local implementation. 2021-12-01 14:33:42 +11:00
Deon George
e864d46430
Added some logging to SlackBotChannel::class 2021-12-01 13:33:10 +11:00
98 changed files with 3151 additions and 1706 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "leenooks/slack", "name": "leenooks/slack",
"description": "Leenooks Slack Interaction.", "description": "Leenooks Slack Interaction.",
"keywords": ["laravel", "leenooks"], "keywords": ["laravel","leenooks","slack"],
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
@ -10,7 +10,7 @@
} }
], ],
"require": { "require": {
"mpociot/phpws": "^2.1" "leenooks/phpws": "^2.1.3"
}, },
"require-dev": { "require-dev": {
}, },

View File

@ -4,7 +4,10 @@ namespace Slack;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Blockkit\Modal;
use Slack\Response\Chat; use Slack\Response\Chat;
use Slack\Exceptions\{SlackAlreadyPinnedException, use Slack\Exceptions\{SlackAlreadyPinnedException,
SlackChannelNotFoundException, SlackChannelNotFoundException,
@ -17,20 +20,21 @@ use Slack\Exceptions\{SlackAlreadyPinnedException,
SlackNotInChannelException, SlackNotInChannelException,
SlackThreadNotFoundException, SlackThreadNotFoundException,
SlackTokenScopeException}; SlackTokenScopeException};
use Slack\Models\{Team,User}; use Slack\Models\{Team,Token,User};
use Slack\Response\ChannelList; use Slack\Response\ChannelList;
use Slack\Response\Generic; use Slack\Response\Generic;
use Slack\Response\User as ResponseUser; use Slack\Response\User as ResponseUser;
use Slack\Response\Team as ResponseTeam; use Slack\Response\Team as ResponseTeam;
use Slack\Response\Test; use Slack\Response\Test;
class API final class API
{ {
private const LOGKEY = 'API'; private const LOGKEY = 'API';
private const scopes = [ private const scopes = [
'auth.test'=>'', // No scope required 'auth.test'=>'', // No scope required
'chat.delete'=>'chat:write', 'chat.delete'=>'chat:write',
'chat.postEphemeral'=>'chat:write',
'chat.postMessage'=>'chat:write', 'chat.postMessage'=>'chat:write',
'chat.update'=>'chat:write', 'chat.update'=>'chat:write',
'conversations.history'=>'channels:history', // Also need groups:history for Private Channels and im:history for messages to the bot. 'conversations.history'=>'channels:history', // Also need groups:history for Private Channels and im:history for messages to the bot.
@ -40,6 +44,8 @@ class API
'dialog.open'=>'', // No scope required 'dialog.open'=>'', // No scope required
'pins.add'=>'pins:write', 'pins.add'=>'pins:write',
'pins.remove'=>'pins:write', 'pins.remove'=>'pins:write',
'chat.scheduleMessage'=>'chat:write',
'chat.scheduledMessages.list'=>'',
'team.info'=>'team:read', 'team.info'=>'team:read',
'views.open'=>'', // No scope required 'views.open'=>'', // No scope required
'views.publish'=>'', // No scope required 'views.publish'=>'', // No scope required
@ -50,7 +56,7 @@ class API
]; ];
// Our slack token to use // Our slack token to use
private $_token; private Token $_token;
public function __construct(Team $o) public function __construct(Team $o)
{ {
@ -69,190 +75,118 @@ class API
/** /**
* Delete a message in a channel * Delete a message in a channel
* *
* @param $channel * @param string $channel
* @param $timestamp * @param string $timestamp
* @return Generic * @return Generic
* @throws \Exception * @throws SlackException
*/ */
public function deleteChat($channel,$timestamp): Generic public function deleteChat(string $channel,string $timestamp): Generic
{ {
Log::debug(sprintf('%s:Delete Message [%s] in [%s]',static::LOGKEY,$timestamp,$channel),['m'=>__METHOD__]); Log::debug(sprintf('%s:Delete Message [%s] in [%s]',static::LOGKEY,$timestamp,$channel),['m'=>__METHOD__]);
return new Generic($this->execute('chat.delete',['channel'=>$channel,'ts'=>$timestamp])); return new Generic($this->execute('chat.delete',['channel'=>$channel,'ts'=>$timestamp]));
} }
/**
* Get Messages on a channel from a specific timestamp
*
* @param $channel
* @param $timestamp
* @param int $limit
* @return Generic
* @throws \Exception
*/
public function getChannelHistory($channel,$timestamp,$limit=20): Generic
{
Log::debug(sprintf('%s:Message History for Channel [%s] from Timestamp [%s]',static::LOGKEY,$channel,$timestamp),['m'=>__METHOD__]);
return new Generic($this->execute('conversations.history',['channel'=>$channel,'oldest'=>$timestamp,'limit'=>$limit]));
}
/**
* Get information on a channel.
*
* @param $channel
* @return Generic
* @throws \Exception
*/
public function getChannelInfo($channel): Generic
{
Log::debug(sprintf('%s:Channel Information [%s]',static::LOGKEY,$channel),['m'=>__METHOD__]);
return new Generic($this->execute('conversations.info',['channel'=>$channel]));
}
/**
* Get a list of channels.
*
* @param int $limit
* @return Generic
* @throws \Exception
*/
public function getChannelList(int $limit=100): Generic
{
Log::debug(sprintf('%s:Channel List',static::LOGKEY),['m'=>__METHOD__]);
return new Generic($this->execute('conversations.list',['limit'=>$limit]));
}
/**
* Get all messages from a thread
*
* @param string $channel
* @param string $thread_ts
* @return Chat
* @throws \Exception
*/
public function getMessageHistory(string $channel,string $thread_ts): Chat
{
Log::debug(sprintf('%s:Get Message Threads for Message [%s] on Channel [%s]',static::LOGKEY,$thread_ts,$channel),['m'=>__METHOD__]);
return new Chat($this->execute('conversations.replies',['channel'=>$channel,'ts'=>$thread_ts]));
}
/**
* Get information on a user
*
* @param string $team_id
* @return ResponseTeam
* @throws \Exception
*/
public function getTeam(string $team_id): ResponseTeam
{
Log::debug(sprintf('%s:Team Info [%s]',static::LOGKEY,$team_id),['m'=>__METHOD__]);
return new ResponseTeam($this->execute('team.info',['team'=>$team_id]));
}
/**
* Get information on a user
*
* @param $user_id
* @return ResponseUser
* @throws \Exception
*/
public function getUser($user_id): ResponseUser
{
Log::debug(sprintf('%s:User Info [%s]',static::LOGKEY,$user_id),['m'=>__METHOD__]);
return new ResponseUser($this->execute('users.info',['user'=>$user_id]));
}
/** /**
* Open a dialogue with the user * Open a dialogue with the user
* *
* @param string $trigger * @param string $trigger
* @param string $dialog * @param string $dialog
* @return Generic * @return Generic
* @throws \Exception * @throws SlackException
*/ */
public function dialogOpen(string $trigger,string $dialog): Generic public function dialogOpen(string $trigger,string $dialog): Generic
{ {
Log::debug(sprintf('%s:Open a Dialog',static::LOGKEY),['m'=>__METHOD__,'d'=>$dialog,'t'=>$trigger]); Log::debug(sprintf('%s:Open a Dialog',static::LOGKEY),['m'=>__METHOD__,'d'=>$dialog,'t'=>$trigger]);
return new Generic($this->execute('dialog.open',json_encode(['dialog'=>$dialog,'trigger_id'=>$trigger]))); return new Generic($this->execute('dialog.open',['dialog'=>$dialog,'trigger_id'=>$trigger]));
} }
/** /**
* Migrate users to Enterprise IDs * Get Messages on a channel from a specific timestamp
* *
* @param array $users * @param string $channel
* @param string $timestamp
* @param int $limit
* @return Generic * @return Generic
* @throws \Exception * @throws SlackException
*/ */
public function migrationExchange(array $users): Generic public function getChannelHistory(string $channel,string $timestamp,int $limit=20): Generic
{ {
Log::debug(sprintf('%s:Migrate Exchange [%s] users',static::LOGKEY,count($users)),['m'=>__METHOD__]); Log::debug(sprintf('%s:Message History for Channel [%s] from Timestamp [%s]',static::LOGKEY,$channel,$timestamp),['m'=>__METHOD__]);
return new Generic($this->execute('migration.exchange',['users'=>join(',',$users)])); return new Generic($this->execute('conversations.history',['channel'=>$channel,'oldest'=>$timestamp,'limit'=>$limit],TRUE));
} }
/** /**
* Pin a message in a channel * Get information on a channel.
* *
* @param $channel * @param string $channel
* @param $timestamp
* @return Generic * @return Generic
* @throws \Exception * @throws SlackException
*/ */
public function pinMessage(string $channel,string $timestamp): Generic public function getChannelInfo(string $channel): Generic
{ {
Log::debug(sprintf('%s:Pin Message [%s|%s]',static::LOGKEY,$channel,$timestamp),['m'=>__METHOD__]); Log::debug(sprintf('%s:Channel Information [%s]',static::LOGKEY,$channel),['m'=>__METHOD__]);
return new Generic($this->execute('pins.add',json_encode(['channel'=>$channel,'timestamp'=>$timestamp]))); return new Generic($this->execute('conversations.info',['channel'=>$channel],TRUE));
} }
/** /**
* Post a Slack Message * Get a list of channels.
* *
* @param Message $request * @param int $limit
* @return Generic * @return Generic
* @throws \Exception * @throws SlackException
*/ */
public function postMessage(Message $request): Generic public function getChannelList(int $limit=100): Generic
{ {
Log::debug(sprintf('%s:Post a Slack Message',static::LOGKEY),['m'=>__METHOD__,'r'=>$request]); Log::debug(sprintf('%s:Channel List',static::LOGKEY),['m'=>__METHOD__]);
return new Generic($this->execute('chat.postMessage',json_encode($request))); return new Generic($this->execute('conversations.list',['limit'=>$limit],TRUE));
} }
/** /**
* Remove a Pin from a message * Get all messages from a thread
* *
* @param $channel * @param string $channel
* @param $timestamp * @param string $thread_ts
* @return Generic * @return Chat
* @throws \Exception * @throws SlackException
*/ */
public function unpinMessage($channel,$timestamp): Generic public function getMessageHistory(string $channel,string $thread_ts): Chat
{ {
Log::debug(sprintf('%s:Remove Pin from Message [%s|%s]',static::LOGKEY,$channel,$timestamp),['m'=>__METHOD__]); Log::debug(sprintf('%s:Get Message Threads for Message [%s] on Channel [%s]',static::LOGKEY,$thread_ts,$channel),['m'=>__METHOD__]);
return new Generic($this->execute('pins.remove',json_encode(['channel'=>$channel,'timestamp'=>$timestamp]))); return new Chat($this->execute('conversations.replies',['channel'=>$channel,'ts'=>$thread_ts],TRUE));
} }
/** /**
* Update a Slack Message * Get information on a user
* *
* @param Message $request * @param string $team_id
* @return Generic * @return ResponseTeam
* @throws \Exception * @throws SlackException
*/ */
public function updateMessage(Message $request): Generic public function getTeam(string $team_id): ResponseTeam
{ {
Log::debug(sprintf('%s:Update a Slack Message',static::LOGKEY),['m'=>__METHOD__,'r'=>$request]); Log::debug(sprintf('%s:Team Info [%s]',static::LOGKEY,$team_id),['m'=>__METHOD__]);
return new Generic($this->execute('chat.update',json_encode($request))); return new ResponseTeam($this->execute('team.info',['team'=>$team_id],TRUE));
}
/**
* Get information on a user
*
* @param string $user_id
* @return ResponseUser
* @throws SlackException
*/
public function getUser(string $user_id): ResponseUser
{
Log::debug(sprintf('%s:User Info [%s]',static::LOGKEY,$user_id),['m'=>__METHOD__]);
return new ResponseUser($this->execute('users.info',['user'=>$user_id],TRUE));
} }
/** /**
@ -262,7 +196,7 @@ class API
* @param int $limit * @param int $limit
* @param string|null $cursor * @param string|null $cursor
* @return ChannelList * @return ChannelList
* @throws \Exception * @throws SlackException
*/ */
public function getUserChannels(User $uo,int $limit=100,string $cursor=NULL): ChannelList public function getUserChannels(User $uo,int $limit=100,string $cursor=NULL): ChannelList
{ {
@ -278,105 +212,205 @@ class API
if ($cursor) if ($cursor)
$args->put('cursor',$cursor); $args->put('cursor',$cursor);
return new ChannelList($this->execute('users.conversations',$args->toArray())); return new ChannelList($this->execute('users.conversations',$args->toArray(),TRUE));
} }
public function viewOpen(string $trigger,string $view): Generic /**
* Migrate users to Enterprise IDs
*
* @param array $users
* @return Generic
* @throws SlackException
*/
public function migrationExchange(array $users): Generic
{
Log::debug(sprintf('%s:Migrate Exchange [%s] users',static::LOGKEY,count($users)),['m'=>__METHOD__]);
return new Generic($this->execute('migration.exchange',['users'=>join(',',$users)],TRUE));
}
/**
* Pin a message in a channel
*
* @param string $channel
* @param string $timestamp
* @return Generic
* @throws SlackException
*/
public function pinMessage(string $channel,string $timestamp): Generic
{
Log::debug(sprintf('%s:Pin Message [%s|%s]',static::LOGKEY,$channel,$timestamp),['m'=>__METHOD__]);
return new Generic($this->execute('pins.add',['channel'=>$channel,'timestamp'=>$timestamp]));
}
/**
* Post a Slack Message to a user as an ephemeral message
*
* @param Message $request
* @return Generic
* @throws SlackException
*/
public function postEphemeral(Message $request): Generic
{
Log::debug(sprintf('%s:Post a Slack Ephemeral Message',static::LOGKEY),['m'=>__METHOD__,'r'=>$request]);
return new Generic($this->execute('chat.postEphemeral',$request));
}
/**
* Post a Slack Message
*
* @param Message $request
* @return Generic
* @throws SlackException
*/
public function postMessage(Message $request): Generic
{
Log::debug(sprintf('%s:Post a Slack Message',static::LOGKEY),['m'=>__METHOD__,'r'=>$request]);
return new Generic($this->execute('chat.postMessage',$request));
}
/**
* Schedule a slack message
*
* @param Message $request
* @return Generic
* @throws SlackException
*/
public function scheduleMessage(Message $request): Generic
{
Log::debug(sprintf('%s:Scheduling a Slack Message',static::LOGKEY),['m'=>__METHOD__,'r'=>$request]);
return new Generic($this->execute('chat.scheduleMessage',$request));
}
/**
* Get the scheduled messages
*
* @param string|null $request
* @return Generic
* @throws SlackException
*/
public function scheduleMessagesList(string $request=NULL): Generic
{
Log::debug(sprintf('%s:Get the Scheduled Messages in Slack',static::LOGKEY),['m'=>__METHOD__,'r'=>$request]);
return new Generic($this->execute('chat.scheduledMessages.list',$request ? ['channel'=>$request] : []));
}
/**
* Remove a Pin from a message
*
* @param string $channel
* @param string $timestamp
* @return Generic
* @throws SlackException
*/
public function unpinMessage(string $channel,string $timestamp): Generic
{
Log::debug(sprintf('%s:Remove Pin from Message [%s|%s]',static::LOGKEY,$channel,$timestamp),['m'=>__METHOD__]);
return new Generic($this->execute('pins.remove',['channel'=>$channel,'timestamp'=>$timestamp]));
}
/**
* Update a Slack Message
*
* @param Message $request
* @return Generic
* @throws SlackException
*/
public function updateMessage(Message $request): Generic
{
Log::debug(sprintf('%s:Update a Slack Message',static::LOGKEY),['m'=>__METHOD__,'r'=>$request]);
return new Generic($this->execute('chat.update',$request));
}
public function viewOpen(string $trigger,Modal $view): Generic
{ {
Log::debug(sprintf('%s:Open a view',static::LOGKEY),['m'=>__METHOD__,'t'=>$trigger]); Log::debug(sprintf('%s:Open a view',static::LOGKEY),['m'=>__METHOD__,'t'=>$trigger]);
return new Generic($this->execute('views.open',json_encode(['trigger_id'=>$trigger,'view'=>$view]))); return new Generic($this->execute('views.open',['trigger_id'=>$trigger,'view'=>$view]));
} }
/** /**
* Publish a view * Publish a view
* *
* @param string $user * @param string $user
* @param string $view * @param Modal $view
* @param string $hash * @param string $hash
* @return Generic * @return Generic
* @throws \Exception * @throws SlackException
* @todo Add some smarts to detect if the new view is the same as the current view, and thus no need to post. * @todo Add some smarts to detect if the new view is the same as the current view, and thus no need to post.
*/ */
public function viewPublish(string $user,string $view,string $hash=''): Generic public function viewPublish(string $user,Modal $view,string $hash=''): Generic
{ {
Log::debug(sprintf('%s:Publish a view',static::LOGKEY),['m'=>__METHOD__,'u'=>$user,'h'=>$hash]); Log::debug(sprintf('%s:Publish a view',static::LOGKEY),['m'=>__METHOD__,'u'=>$user,'h'=>$hash]);
return new Generic($this->execute('views.publish',json_encode($hash ? ['user_id'=>$user,'view'=>$view,'hash'=>$hash] : ['user_id'=>$user,'view'=>$view]))); return new Generic($this->execute('views.publish',$hash ? ['user_id'=>$user,'view'=>$view,'hash'=>$hash] : ['user_id'=>$user,'view'=>$view]));
} }
public function viewPush(string $trigger,string $view): Generic public function viewPush(string $trigger,Modal $view): Generic
{ {
Log::debug(sprintf('%s:Push a view',static::LOGKEY),['m'=>__METHOD__,'t'=>$trigger]); Log::debug(sprintf('%s:Push a view',static::LOGKEY),['m'=>__METHOD__,'t'=>$trigger]);
return new Generic($this->execute('views.push',json_encode(['trigger_id'=>$trigger,'view'=>$view]))); return new Generic($this->execute('views.push',['trigger_id'=>$trigger,'view'=>$view]));
} }
public function viewUpdate(string $view_id,string $view): Generic public function viewUpdate(string $view_id,Modal $view): Generic
{ {
Log::debug(sprintf('%s:Update a view',static::LOGKEY),['m'=>__METHOD__,'id'=>$view_id]); Log::debug(sprintf('%s:Update a view',static::LOGKEY),['m'=>__METHOD__,'id'=>$view_id]);
return new Generic($this->execute('views.update',json_encode(['view_id'=>$view_id,'view'=>$view]))); return new Generic($this->execute('views.update',['view_id'=>$view_id,'view'=>$view]));
} }
/** /**
* Call the Slack API * Call the Slack API
* *
* @param string $method * @param string $method
* @param null $parameters * @param mixed $parameters
* @param bool $asForm
* @return object * @return object
* @throws \Exception * @throws \Exception
* @throws SlackException
*/ */
private function execute(string $method,$parameters = NULL): object private function execute(string $method,mixed $parameters,bool $asForm=FALSE): object
{ {
switch (config('app.env')) { switch (config('app.env')) {
case 'dev': $url = 'http://steno:3000'; case 'steno': $url = 'http://steno:3000';
break; break;
case 'testing': $url = 'http://localhost:3000'; case 'replay': $url = 'http://steno_replay:3000';
break;
case 'testing-l': $url = 'http://steno_replay:3000';
break; break;
default: default:
$url = 'https://slack.com'; $url = 'https://slack.com';
} }
$url .= '/api/'.$method;
// If we dont have a scope definition, or if the scope definition is not in the token // If we dont have a scope definition, or if the scope definition is not in the token
if (is_null($x=Arr::get(self::scopes,$method)) OR (($x !== '') AND ! $this->_token->hasScope($x))) { if (is_null($x=Arr::get(self::scopes,$method)) OR (($x !== '') AND ! $this->_token->hasScope($x))) {
throw new SlackTokenScopeException(sprintf('Token [%d:%s] doesnt have the required scope: [%s] for [%s]',$this->_token->id,$this->_token->token_hidden,serialize($x),$method)); throw new SlackTokenScopeException(sprintf('Token [%d:%s] doesnt have the required scope: [%s] for [%s]',$this->_token->id,$this->_token->token_hidden,serialize($x),$method));
} }
// If we are passed an array, we'll do a normal post. $http = Http::baseUrl($url);
if (is_array($parameters)) { $http
$parameters['token'] = $this->_token->token; ->withToken($this->_token->token)
$request = $this->prepareRequest( ->acceptJson();
$url,
$parameters
);
// If we are json, then we'll do an application/json post if ($asForm) {
} elseif (is_json($parameters)) { if (! is_array($parameters))
$request = $this->prepareRequest( throw new SlackException('Parameters are not an array for a form submission');
$url,
$parameters,
[
'Content-Type: application/json; charset=utf-8',
'Content-Length: '.strlen($parameters),
'Authorization: Bearer '.$this->_token->token,
]
);
} else { $http->asForm();
throw new \Exception('Parameters unknown');
} elseif ($parameters) {
$http->withBody((is_array($parameters) || ($parameters instanceof BlockKit)) ? json_encode($parameters) : $parameters,'application/json');
} }
try { try {
$response = curl_exec($request); $request = $http->post(sprintf('/api/%s',$method),$asForm ? $parameters : NULL)->throw();
if (! $response) $response = $request->object();
throw new \Exception('CURL exec returned an empty response: '.serialize(curl_getinfo($request)));
$result = json_decode($response);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error(sprintf('%s:Got an error while posting to [%s] (%s)',static::LOGKEY,$url,$e->getMessage()),['m'=>__METHOD__]); Log::error(sprintf('%s:Got an error while posting to [%s] (%s)',static::LOGKEY,$url,$e->getMessage()),['m'=>__METHOD__]);
@ -384,73 +418,49 @@ class API
throw new \Exception($e->getMessage()); throw new \Exception($e->getMessage());
} }
if (! $result) { if ($response->ok)
Log::error(sprintf('%s:Our result shouldnt be empty',static::LOGKEY),['m'=>__METHOD__,'r'=>$request,'R'=>$response]); return $response;
throw new SlackException('Slack Result is Empty'); else
} switch ($response->error) {
if (! $result->ok) {
switch ($result->error) {
case 'already_pinned': case 'already_pinned':
throw new SlackAlreadyPinnedException('Already Pinned',curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackAlreadyPinnedException('Already Pinned',$request->status());
case 'not_authed':
throw new SlackNoAuthException('No Auth Token',curl_getinfo($request,CURLINFO_HTTP_CODE));
case 'channel_not_found': case 'channel_not_found':
throw new SlackChannelNotFoundException('Channel Not Found',curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackChannelNotFoundException('Channel Not Found',$request->status());
case 'hash_conflict': case 'hash_conflict':
if (App::environment() == 'dev') if (App::environment() == 'local')
file_put_contents('/tmp/hash_conflict.'.$method,print_r(json_decode(json_decode($parameters)->view),TRUE)); file_put_contents('/tmp/hash_conflict.'.$method,print_r($response,TRUE));
throw new SlackHashConflictException('Hash Conflict',curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackHashConflictException('Hash Conflict',$request->status());
case 'invalid_auth':
throw new SlackNoAuthException('Invalid Auth Token',$request->status());
case 'message_not_found': case 'message_not_found':
throw new SlackMessageNotFoundException('Message Not Found',curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackMessageNotFoundException('Message Not Found',$request->status());
case 'no_pin': case 'no_pin':
throw new SlackNoPinException('No Pin',curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackNoPinException('No Pin',$request->status());
case 'not_in_channel': case 'not_authed':
throw new SlackNotInChannelException('Not In Channel',curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackNoAuthException('No Auth Token',$request->status());
case 'not_found': case 'not_found':
file_put_contents('/tmp/method.'.$method,print_r(['request'=>is_json($parameters) ? json_decode($parameters,TRUE) : $parameters,'response'=>$result],TRUE)); file_put_contents('/tmp/method.'.$method,print_r(['request'=>is_json($parameters) ? json_decode($parameters,TRUE) : $parameters,'response'=>$response],TRUE));
throw new SlackNotFoundException('Not Found',curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackNotFoundException('Not Found',$request->status());
case 'not_in_channel':
throw new SlackNotInChannelException('Not In Channel',$request->status());
case 'thread_not_found': case 'thread_not_found':
throw new SlackThreadNotFoundException('Thread Not Found',curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackThreadNotFoundException('Thread Not Found',$request->status());
case 'account_inactive':
// @todo Mark the token/team as inactive
default: default:
Log::error(sprintf('%s:Generic Error',static::LOGKEY),['m'=>__METHOD__,'t'=>$this->_token->team_id,'r'=>$result]); Log::error(sprintf('%s:Generic Error',static::LOGKEY),['m'=>__METHOD__,'t'=>$this->_token->team_id,'r'=>$response]);
throw new SlackException($result->error,curl_getinfo($request,CURLINFO_HTTP_CODE)); throw new SlackException($response->error,$request->status());
} }
} }
curl_close($request);
return $result;
}
/**
* Setup the API call
*
* @param $url
* @param string $parameters
* @param array $headers
* @return resource
*/
private function prepareRequest($url,$parameters='',$headers = [])
{
$request = curl_init();
curl_setopt($request,CURLOPT_URL,$url);
curl_setopt($request,CURLOPT_RETURNTRANSFER,TRUE);
curl_setopt($request,CURLOPT_HTTPHEADER,$headers);
curl_setopt($request,CURLINFO_HEADER_OUT,TRUE);
curl_setopt($request,CURLOPT_SSL_VERIFYPEER,FALSE);
curl_setopt($request,CURLOPT_POSTFIELDS,$parameters);
return $request;
}
} }

View File

@ -2,8 +2,12 @@
namespace Slack; namespace Slack;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Models\Slack\Channel as AppChannel;
use App\Models\Slack\Enterprise as AppEnterprise;
use App\Models\Slack\Team as AppTeam;
use App\Models\Slack\User as AppUser;
use Slack\Models\{Channel,Enterprise,Team,User}; use Slack\Models\{Channel,Enterprise,Team,User};
/** /**
@ -40,10 +44,13 @@ abstract class Base
* *
* @param bool $create * @param bool $create
* @return Channel|null * @return Channel|null
* @todo Enable simulating an existing channel, using FALSE (dont create), 0 (create dont save), 1 (create and save)
*/ */
final public function channel(bool $create=FALSE): ?Channel final public function channel(bool $create=FALSE): ?Channel
{ {
$o = Channel::firstOrNew( $class = class_exists(AppChannel::class) ? AppChannel::class : Channel::class;
$o = $class::firstOrNew(
[ [
'channel_id'=>$this->channel_id, 'channel_id'=>$this->channel_id,
]); ]);
@ -57,9 +64,49 @@ abstract class Base
return $o->exists ? $o : NULL; return $o->exists ? $o : NULL;
} }
/**
* Separate out a callback command to the id that the command relates to
*
* @param string $key
* @param string|null $item
* @return string|null
* @throws \Exception
*/
final protected function keyitem(string $key,string $item=NULL): ?string
{
if (! $item)
return $item;
$regex = '/^([a-z_]+)\|([0-9]+)$/';
$id = NULL;
$value = NULL;
if (preg_match($regex,$item)) {
$id = preg_replace($regex,'$1',$item);
$value = preg_replace($regex,'$2',$item);
}
switch ($key) {
case 'id':
return $id ?: $item;
case 'value':
return $value;
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* Return the Eneterprise object that a Response is related to
*
* @return Enterprise
*/
final public function enterprise(): Enterprise final public function enterprise(): Enterprise
{ {
return Enterprise::firstOrNew( $class = class_exists(AppEnterprise::class) ? AppEnterprise::class : Enterprise::class;
return $class::firstOrNew(
[ [
'enterprise_id'=>$this->enterprise_id 'enterprise_id'=>$this->enterprise_id
]); ]);
@ -73,7 +120,9 @@ abstract class Base
*/ */
final public function team(bool $any=FALSE): ?Team final public function team(bool $any=FALSE): ?Team
{ {
$o = Team::firstOrNew( $class = class_exists(AppTeam::class) ? AppTeam::class : Team::class;
$o = $class::firstOrNew(
[ [
'team_id'=>$this->team_id 'team_id'=>$this->team_id
]); ]);
@ -83,7 +132,6 @@ abstract class Base
} }
return $o->exists ? $o : NULL; return $o->exists ? $o : NULL;
} }
/** /**
@ -94,7 +142,9 @@ abstract class Base
*/ */
final public function user(): User final public function user(): User
{ {
$o = User::firstOrNew( $class = class_exists(AppUser::class) ? AppUser::class : User::class;
$o = $class::firstOrNew(
[ [
'user_id'=>$this->user_id, 'user_id'=>$this->user_id,
]); ]);
@ -102,6 +152,7 @@ abstract class Base
if (! $o->exists) { if (! $o->exists) {
$o->team_id = $this->enterprise_id ? NULL : $this->team()->id; $o->team_id = $this->enterprise_id ? NULL : $this->team()->id;
$o->enterprise_id = ($x=$this->enterprise())->exists ? $x->id : NULL; $o->enterprise_id = ($x=$this->enterprise())->exists ? $x->id : NULL;
$o->active = FALSE;
$o->save(); $o->save();
Log::debug(sprintf('%s: User Created in DB [%s] (%s)',self::LOGKEY,$this->user_id,$o->id)); Log::debug(sprintf('%s: User Created in DB [%s] (%s)',self::LOGKEY,$this->user_id,$o->id));

View File

@ -2,14 +2,27 @@
namespace Slack; namespace Slack;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Slack\Exceptions\SlackSyntaxException;
/** /**
* Class BlockKit - Slack Blockit Objects * Class BlockKit - Slack Blockit Objects
* *
* @notes
* + callback_id is used to identify the source of any action (modal). (Message blocks do not have a callback_id, accept in legacy attachments).
* eg: hometab, add_product, ask_modal
* + block_id is used to identify the sub action(s) of any action (modal). (Messages with blocks can have a block_id, we need to use this to determine what to do.)
* eg: multiple blocks (list of something)
* + action_id is used to identify the action that was initiated
* eg: view, edit, review
* + value is the value of the action_id
* eg: 5 (question #), yes, no, skip, abort
*
* @package Slack * @package Slack
*/ */
class BlockKit implements \JsonSerializable abstract class BlockKit implements \JsonSerializable,\Countable
{ {
protected Collection $_data; protected Collection $_data;
@ -18,6 +31,14 @@ class BlockKit implements \JsonSerializable
$this->_data = collect(); $this->_data = collect();
} }
public function __get($key) {
return $this->_data->get($key);
}
public function __set(string $key,$value) {
return $this->_data->put($key,$value);
}
public function count() public function count()
{ {
return $this->_data->count(); return $this->_data->count();
@ -27,4 +48,12 @@ class BlockKit implements \JsonSerializable
{ {
return $this->_data; return $this->_data;
} }
protected function validate(string $key,$value)
{
if (Arr::get(static::LIMITS,$key) && (strlen($value) > static::LIMITS[$key]))
throw new SlackSyntaxException(sprintf('%s must be %d chars or less for buttons %s',$key,static::LIMITS[$key],get_class($this)));
return $value;
}
} }

View File

@ -1,40 +0,0 @@
<?php
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
{
/**
* Add a block button
*
* @param string $text
* @param string $action
* @param string $value
* @param string $style
* @return BlockAction
* @throws \Exception
* @deprecated Move to Blocks/Button?
*/
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',$text);
$this->_data->put('value',$value);
if ($style)
$this->_data->put('style',$style);
return $this;
}
}

View File

@ -2,340 +2,8 @@
namespace Slack\Blockkit; namespace Slack\Blockkit;
use Illuminate\Support\Collection;
use Slack\BlockKit; use Slack\BlockKit;
use Slack\Exceptions\SlackException;
use Slack\Blockkit\Blocks\{Button,Text,TextEmoji};
/** abstract class Blocks extends BlockKit
* 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,60 @@
<?php
namespace Slack\Blockkit\Blocks\Accessories;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Slack\Blockkit\Blocks\Elements\{Confirm,Text};
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
final class Overflow extends Element
{
public const LIMITS = [
'action_id' => 255,
];
public const MIN_OPTIONS = 1;
public const MAX_OPTIONS = 5;
/**
* @param string $action_id
* @param Collection $options
* @throws SlackSyntaxException
* @todo We dont handle option_groups yet.
*/
public function __construct(string $action_id,Collection $options)
{
parent::__construct();
// Defaults
$this->type = 'overflow';
$this->action_id = $this->validate('action_id',$action_id);
if (count($options) < self::MIN_OPTIONS)
throw new SlackSyntaxException(sprintf('Must have atleast %d options',self::MIN_OPTIONS));
if (count($options) > self::MAX_OPTIONS)
throw new SlackSyntaxException(sprintf('Can only have maximum %d options',self::MAX_OPTIONS));
$this->options = $options->transform(function($item) {
return ['text'=>Text::item(Arr::get($item,'name'),'plain_text'),'value'=>(string)Arr::get($item,'id')];
});
}
public static function item(string $action_id,Collection $options): self
{
return new self($action_id,$options);
}
/* OPTIONAL ITEMS */
public function confirm(Confirm $confirm): self
{
$this->confirm = $confirm;
return $this;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Slack\Blockkit\Blocks;
use Illuminate\Support\Collection;
use Slack\Blockkit\Blocks;
use Slack\Blockkit\Blocks\Elements\{Button,MultiStaticSelect};
use Slack\Exceptions\SlackSyntaxException;
final class Actions extends Blocks
{
public const LIMITS = [
'block_id' => 255, // @todo Should be unique for each message
];
public const MAX_ELEMENTS = 5;
public const VALID_ELEMENTS = [
Button::class,
MultiStaticSelect::class,
Blocks\Accessories\Overflow::class,
];
public function __construct()
{
parent::__construct();
// Defaults
$this->type = 'actions';
}
public static function item(): self
{
return new self();
}
public function jsonSerialize()
{
if (! $this->elements)
throw new SlackSyntaxException('Must define at least 1 element');
return parent::jsonSerialize();
}
/* OPTIONAL ITEMS */
public function block_id(string $string): self
{
$this->block_id = $this->validate('block_id',$string);
return $this;
}
public function elements(Collection $collection): self
{
if (count($collection) > self::MAX_ELEMENTS)
throw new SlackSyntaxException(sprintf('Can only have maximum %d elements',self::MAX_ELEMENTS));
// @todo Check that a valid element is added. https://api.slack.com/reference/block-kit/blocks#actions
$this->elements = $collection;
return $this;
}
}

View File

@ -1,30 +0,0 @@
<?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,49 @@
<?php
namespace Slack\Blockkit\Blocks;
use Illuminate\Support\Collection;
use Slack\Blockkit\Blocks;
use Slack\Exceptions\SlackSyntaxException;
final class Context extends Blocks
{
public const LIMITS = [
'block_id' => 255, // @todo Should be unique for each message
];
public const MAX_ELEMENTS = 10;
/**
* @param Collection $collection
* @throws SlackSyntaxException
* @todo Collection can only be image or text elements
*/
public function __construct(Collection $collection)
{
parent::__construct();
// Defaults
$this->type = 'context';
if (count($collection) > self::MAX_ELEMENTS)
throw new SlackSyntaxException(sprintf('Can only have maximum %d elements',self::MAX_ELEMENTS));
$this->elements = $collection;
}
public static function item(Collection $collection): self
{
return new self($collection);
}
/* OPTIONAL ITEMS */
public function block_id(string $string): self
{
$this->block_id = $this->validate('block_id',$string);
return $this;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Slack\Blockkit\Blocks;
use Slack\Blockkit\Blocks;
final class Divider extends Blocks
{
protected const LIMITS = [
'block_id' => 255, // @todo Should be unique for each message
];
public function __construct()
{
parent::__construct();
// Defaults
$this->type = 'divider';
}
public static function item(): self
{
return new self();
}
/* OPTIONAL ITEMS */
public function block_id(string $string): self
{
$this->block_id = $this->validate('block_id',$string);
return $this;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
final class Button extends Element
{
protected const LIMITS = [
'action_id' => 255, // @todo Should be unique for each message
'callback_id' => 255,
'text' => 75,
'url' => 3000,
'value' => 2000,
];
public function __construct(string $text,string $value,string $action_id)
{
parent::__construct();
// Defaults
$this->type = 'button';
$this->text = Text::item($this->validate('text',$text),'plain_text');
$this->value = $this->validate('value',$value);
$this->action_id = $this->validate('action_id',$action_id);
}
public static function item(string $text,string $value,string $action_id): self
{
return new self($text,$value,$action_id);
}
/* OPTIONAL ITEMS */
public function confirm(Confirm $confirm): self
{
$this->confirm = $confirm;
return $this;
}
public function style(string $string): self
{
if (! in_array($string,['default','primary','danger']))
throw new SlackSyntaxException(sprintf('Unknown style %s',$string));
$this->style = $string;
return $this;
}
public function url(string $string): self
{
$this->url = $this->validate('url',$string);
return $this;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
final class Confirm extends Element
{
protected const LIMITS = [
'title' => 100,
'text' => 300,
'confirm' => 30,
'deny' => 30,
];
public function __construct(string $title,Text $text,string $confirm,string $deny)
{
parent::__construct();
$this->title = Text::item($this->validate('title',$title),'plain_text');
$this->text = $this->validate('text',$text->text) ? $text : NULL;
$this->confirm = Text::item($this->validate('confirm',$confirm),'plain_text');
$this->deny = Text::item($this->validate('deny',$deny),'plain_text');
}
public static function item(string $title,Text $text,string $confirm,string $deny): self
{
return new self($title,$text,$confirm,$deny);
}
public function style(string $string): self
{
if (! in_array($string,['default','primary','danger']))
throw new SlackSyntaxException(sprintf('Unknown style %s',$string));
$this->style = $string;
return $this;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
final class ExternalSelect extends Element
{
protected const LIMITS = [
'action_id' => 255,
'placeholder' => 150,
];
/**
* @param string $placeholder
* @param string $action_id
* @throws SlackSyntaxException
*/
public function __construct(string $placeholder,string $action_id)
{
parent::__construct();
// Defaults
$this->type = 'external_select';
$this->placeholder = Text::item($this->validate('placeholder',$placeholder),'plain_text');
$this->action_id = $this->validate('action_id',$action_id);
}
public static function item(string $placeholder,string $action_id): self
{
return new self($placeholder,$action_id);
}
/* OPTIONAL ITEMS */
public function confirm(Confirm $confirm): self
{
$this->confirm = $confirm;
return $this;
}
// @note only 1 element in a view can have this set to true
public function focus_on_load(bool $bool): self
{
$this->focus_on_load = $bool;
return $this;
}
public function initial_option(string $option): self
{
$this->initial_option = $option;
return $this;
}
public function min_query_length(int $int): self
{
if ($int < 1)
throw new SlackSyntaxException('Minimum 1 options must be configured');
$this->min_query_length = $int;
return $this;
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Illuminate\Support\Collection;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
final class MultiExternalSelect extends Element
{
public const LIMITS = [
'action_id' => 255,
'placeholder' => 150,
];
public const MAX_OPTIONS = 100;
// @todo option_group? (double check it is applicable to this item)
/**
* @param string $placeholder
* @param string $action_id
* @throws SlackSyntaxException
*/
public function __construct(string $placeholder,string $action_id)
{
parent::__construct();
// Defaults
$this->type = 'multi_external_select';
$this->placeholder = Text::item($this->validate('placeholder',$placeholder),'plain_text');
$this->action_id = $this->validate('action_id',$action_id);
}
public static function item(string $placeholder,string $action_id): self
{
return new self($placeholder,$action_id);
}
/* OPTIONAL ITEMS */
public function confirm(Confirm $confirm): self
{
$this->confirm = $confirm;
return $this;
}
// @note only 1 element in a view can have this set to true
public function focus_on_load(bool $bool): self
{
$this->focus_on_load = $bool;
return $this;
}
public function initial_options(Collection $array=NULL): self
{
$this->initial_options = $array->map(function($item) { return ['text'=>['type'=>'plain_text','text'=>$item->name],'value'=>(string)$item->value]; });
return $this;
}
public function min_query_length(int $int): self
{
if ($int < 1)
throw new SlackSyntaxException('Minimum 1 options must be configured');
$this->min_query_length = $int;
return $this;
}
public function max_selected_items(int $int): self
{
if ($int < 1)
throw new SlackSyntaxException('Minimum 1 options must be configured');
$this->max_selected_items = $int;
return $this;
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Illuminate\Support\Collection;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
final class MultiStaticSelect extends Element
{
public const LIMITS = [
'action_id' => 255,
'placeholder' => 150,
];
public const MAX_OPTIONS = 100;
// @todo option_group? (double check it is applicable to this item)
/**
* @param string $placeholder
* @param string $action_id
* @param Collection $options
* @throws SlackSyntaxException
* @todo We dont handle option_groups yet.
*/
public function __construct(string $placeholder,string $action_id,Collection $options)
{
parent::__construct();
// Defaults
$this->type = 'multi_static_select';
$this->placeholder = Text::item($this->validate('placeholder',$placeholder),'plain_text');
$this->action_id = $this->validate('action_id',$action_id);
if (! $options->count())
throw new SlackSyntaxException('There are no options?');
if ($options->count() > self::MAX_OPTIONS)
throw new SlackSyntaxException(sprintf('Can only have maximum %d options',self::MAX_OPTIONS));
$this->options = $options->transform(function($item) {
return ['text'=>Text::item($item->name,'plain_text'),'value'=>(string)$item->value];
});
}
public static function item(string $placeholder,string $action_id,Collection $options): self
{
return new self($placeholder,$action_id,$options);
}
/* OPTIONAL ITEMS */
public function confirm(Confirm $confirm): self
{
$this->confirm = $confirm;
return $this;
}
public function focus_on_load(bool $bool): self
{
$this->focus_on_load = $bool;
return $this;
}
public function initial_options(Collection $initial=NULL): self
{
// No initial options.
if (count($initial)) {
if (! $this->options)
throw new SlackSyntaxException('Cannot set an initial value without options defined first');
if (count($initial) > self::MAX_OPTIONS)
throw new SlackSyntaxException(sprintf('Can only have maximum %d options',self::MAX_OPTIONS));
$this->initial_options = $this->options->filter(function($item) use ($initial) { return $initial->contains($item['value']); });
}
return $this;
}
public function max_selected_items(int $int): self
{
if ($int < 1)
throw new SlackSyntaxException('Minimum 1 options must be configured');
$this->max_selected_items = $int;
return $this;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
/**
* @note Overflow, select, and multi-select menus can only use plain_text objects,
* while radio buttons and checkboxes can use mrkdwn text objects
*/
final class Options extends Element
{
protected const LIMITS = [
'description' => 75,
'text' => 75,
'value' => 75,
'url' => 3000,
];
public function __construct(Text $text,string $value)
{
parent::__construct();
if (strlen($text->text) > self::LIMITS['text'])
throw new SlackSyntaxException(sprintf('Text must be %d chars or less',self::LIMITS['text']));
$this->text = $text;
$this->value = $this->validate('value',$value);
}
public static function item(Text $text,string $value): self
{
return new self($text,$value);
}
/* OPTIONAL ITEMS */
public function description(string $text): self
{
$this->description = Text::item($this->validate('description',$text),'plain_text');
return $this;
}
public function url(string $string): self
{
$this->url = $this->validate('url',$string);
return $this;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
/**
* This is an element of an input dialog
*/
final class PlainTextInput extends Element
{
public const LIMITS = [
'action_id' => 255, // @todo Should be unique for each message
'placeholder' => 150,
];
public const MAX_MIN_LENGTH = 3000;
// @todo dispatch_action_config
// @todo focus_on_load
public function __construct(string $action_id)
{
parent::__construct();
$this->type = 'plain_text_input';
$this->action_id = $this->validate('action_id',$action_id);
}
public static function item(string $action_id): self
{
return new self($action_id);
}
/* OPTIONAL ITEMS */
public function initial_value(string $text): self
{
$this->initial_value = $text;
return $this;
}
public function min_length(int $int): self
{
if ($int > self::MAX_MIN_LENGTH)
throw new SlackSyntaxException(sprintf('min_length must be less than %d',self::MAX_MIN_LENGTH));
$this->min_length = $int;
return $this;
}
public function max_length(int $int): self
{
if ($this->min_length && ($int < $this->min_length))
throw new SlackSyntaxException('max_length must be greater than min_length');
$this->max_length = $int;
return $this;
}
public function multiline(bool $bool): self
{
$this->multiline = $bool;
return $this;
}
public function placeholder(Text $text): self
{
if (strlen($text->text) > self::LIMITS['placeholder'])
throw new SlackSyntaxException(sprintf('Text must be %d chars or less',self::LIMITS['placeholder']));
$this->placeholder = $text;
return $this;
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Illuminate\Support\Collection;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
final class StaticSelect extends Element
{
public const LIMITS = [
'action_id' => 255,
'placeholder' => 150,
];
public const MAX_OPTIONS = 100;
// @todo option_group
/**
* @param string $placeholder
* @param string $action_id
* @param Collection $options
* @throws SlackSyntaxException
* @todo We dont handle option_groups yet.
*/
public function __construct(string $placeholder,string $action_id,Collection $options)
{
parent::__construct();
// Defaults
$this->type = 'static_select';
$this->placeholder = Text::item($this->validate('placeholder',$placeholder),'plain_text');
$this->action_id = $this->validate('action_id',$action_id);
if (! $options->count())
throw new SlackSyntaxException('There are no options?');
if ($options->count() > self::MAX_OPTIONS)
throw new SlackSyntaxException(sprintf('Can only have maximum %d options',self::MAX_OPTIONS));
$this->options = $options->transform(function($item) {
return ['text'=>Text::item($item->name,'plain_text'),'value'=>(string)$item->value];
});
}
public static function item(string $placeholder,string $action_id,Collection $options): self
{
return new self($placeholder,$action_id,$options);
}
/* OPTIONAL ITEMS */
public function confirm(Confirm $confirm): self
{
$this->confirm = $confirm;
return $this;
}
public function focus_on_load(bool $bool): self
{
$this->focus_on_load = $bool;
return $this;
}
public function initial_option(string $string=NULL): self
{
if (! $this->options)
throw new SlackSyntaxException('Cannot set an initial value without options defined first');
if ($string)
$this->initial_option = $this->options->first(function($item) use ($string) { return $item['value'] == $string; });
return $this;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Slack\Blockkit\Blocks\Elements;
use Slack\Blockkit\Element;
use Slack\Exceptions\SlackSyntaxException;
final class Text extends Element
{
public const TYPES = ['mrkdwn','plain_text'];
public function __construct(string $text,string $type)
{
parent::__construct();
if (! in_array($type,self::TYPES))
throw new SlackSyntaxException(sprintf('Type [%s] not valid',$type));
$this->text = $text;
$this->type = $type;
}
public static function item(string $text,string $type='mrkdwn'): self
{
return new self($text,$type);
}
/* OPTIONAL ITEMS */
public function emoji(bool $bool): self
{
if ($this->type != 'plain_text')
throw new SlackSyntaxException(sprintf('Cannnot use emoji when type is [%s]',$this->type));
$this->emoji = $bool;
return $this;
}
public function verbatim(bool $bool): self
{
$this->verbatim = $bool;
return $this;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Slack\Blockkit\Blocks;
use Slack\Blockkit\Blocks;
use Slack\Blockkit\Blocks\Elements\Text;
use Slack\Exceptions\SlackSyntaxException;
final class Header extends Blocks
{
protected const LIMITS = [
'block_id' => 255, // @todo Should be unique for each message
'text' => 150,
];
/**
* @param string $text
* @throws SlackSyntaxException
*/
public function __construct(string $text)
{
parent::__construct();
// Defaults
$this->type = 'header';
$this->text = Text::item($this->validate('text',$text),'plain_text');
}
public static function item(string $text): self
{
return new self($text);
}
/* OPTIONAL ITEMS */
public function block_id(string $string): self
{
$this->block_id = $this->validate('block_id',$string);
return $this;
}
}

View File

@ -2,23 +2,84 @@
namespace Slack\Blockkit\Blocks; namespace Slack\Blockkit\Blocks;
use Slack\Blockkit\Input\Element; use Slack\Blockkit\{Blocks,Blocks\Elements\Text,Element};
use Slack\Exceptions\SlackSyntaxException;
final class Input final class Input extends Blocks
{ {
public string $type; public const LIMITS = [
public Element $element; 'block_id' => 255, // @todo Should be unique for each message
public TextEmoji $label; 'hint' => 2000,
'label' => 2000,
];
public function __construct(Element $element,TextEmoji $label) public const VALID_ELEMENTS = [
Elements\PlainTextInput::class,
Elements\MultiStaticSelect::class
];
// @todo dispatch_action
/**
* @param string|null $label
* @throws SlackSyntaxException
*/
public function __construct(string $label=NULL)
{ {
parent::__construct();
// Defaults
$this->type = 'input'; $this->type = 'input';
$this->element = $element;
$this->label = $label; $this->label = Text::item($this->validate('label',$label),'plain_text');
} }
public static function item(Element $element,TextEmoji $label): self public static function item(string $label=NULL): self
{ {
return new self($element,$label); return new self($label);
}
public function jsonSerialize()
{
if (! $this->element)
throw new SlackSyntaxException('Must define an element');
return parent::jsonSerialize();
}
/* OPTIONAL ITEMS */
public function block_id(string $string): self
{
$this->block_id = $this->validate('block_id',$string);
return $this;
}
public function element(Element $object): self
{
if (! in_array(get_class($object),self::VALID_ELEMENTS))
throw new SlackSyntaxException(sprintf('Invalid element [%s] added to input',get_class($object)));
$this->element = $object;
return $this;
}
public function hint(Elements\Text $text): self
{
if (strlen($text->text) > self::LIMITS['hint'])
throw new SlackSyntaxException(sprintf('Text must be %d chars or less',self::LIMITS['hint']));
$this->hint = $text;
return $this;
}
public function optional(bool $bool): self
{
$this->optional = $bool;
return $this;
} }
} }

View File

@ -0,0 +1,81 @@
<?php
namespace Slack\Blockkit\Blocks;
use Illuminate\Support\Collection;
use Slack\Blockkit\{Blocks,Element};
use Slack\BLockKit\Blocks\Elements\Text;
use Slack\Exceptions\SlackSyntaxException;
final class Section extends Blocks
{
public const LIMITS = [
'block_id' => 255, // @todo Should be unique for each message
'text' => 3000,
];
public const MAX_FIELDS = 10;
public const MAX_FIELDS_TEXT = 2000;
/**
* @param Text|NULL $text not required if fields is provided
* @throws SlackSyntaxException
*/
public function __construct(Text $text=NULL)
{
parent::__construct();
// Defaults
$this->type = 'section';
if ($text) {
if (strlen($text->text) > self::LIMITS['text'])
throw new SlackSyntaxException(sprintf('Text must be %d chars or less',self::LIMITS['text']));
$this->text = $text;
}
}
public static function item(Text $text=NULL): self
{
return new self($text);
}
public function jsonSerialize()
{
if (! $this->text && ! $this->fields && ! $this->accessory)
throw new SlackSyntaxException('Must define text, accessory or fields');
return parent::jsonSerialize();
}
/* OPTIONAL ITEMS */
public function accessory(Element $object): self
{
$this->accessory = $object;
return $this;
}
public function block_id(string $string): self
{
$this->block_id = $this->validate('block_id',$string);
return $this;
}
public function fields(Collection $collection): self
{
if (count($collection) > self::MAX_FIELDS)
throw new SlackSyntaxException(sprintf('Can only have maximum %d fields',self::MAX_FIELDS));
if (($x=$collection->map(function($item) { return strlen($item->text); })->max()) > self::MAX_FIELDS_TEXT)
throw new SlackSyntaxException(sprintf('The maximum size of the text in a field is %d (%d)',self::MAX_FIELDS_TEXT,$x));
$this->fields = $collection;
return $this;
}
}

View File

@ -1,20 +0,0 @@
<?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

@ -1,22 +0,0 @@
<?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);
}
}

9
src/Blockkit/Element.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace Slack\Blockkit;
use Slack\BlockKit;
abstract class Element extends BlockKit
{
}

View File

@ -1,22 +0,0 @@
<?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

@ -3,29 +3,62 @@
namespace Slack\Blockkit; namespace Slack\Blockkit;
use Slack\BlockKit; use Slack\BlockKit;
use Slack\Blockkit\Blocks\TextEmoji; use Slack\Blockkit\Blocks\Divider;
use Slack\Blockkit\Blocks\Elements\Text;
use Slack\Blockkit\Blocks\Section;
use Slack\Exceptions\SlackSyntaxException;
/** /**
* This class creates a slack Modal Response * This class creates a slack Modal Response
*/ */
class Modal extends BlockKit final class Modal extends BlockKit
{ {
public const LIMITS = [
'callback_id' => 255,
'close' => 24,
'private_metadata' => 3000,
'submit' => 24,
'title' => 24,
];
public const MAX_BLOCKS = 100;
private $action = NULL; private $action = NULL;
public function __construct(TextEmoji $title) public function __construct(string $title=NULL,string $type='modal')
{ {
parent::__construct(); parent::__construct();
$this->_data->put('type','modal'); if (! in_array($type,['modal','home']))
$this->_data->put('title',$title); throw new SlackSyntaxException(sprintf('Unknown type %s',$type));
if ($title) {
if ($type != 'modal')
throw new SlackSyntaxException(sprintf('Titles are not required for %s',$type));
$this->title = Text::item($this->validate('title',$title),'plain_text');
} }
/* $this->type = $type;
public function action(string $action) $this->blocks = collect();
{
$this->action = $action;
} }
/**
* Add a block to this modal
*
* @param Blocks $block
* @return $this
* @throws SlackSyntaxException
*/ */
public function addBlock(Blocks $block): self
{
$this->blocks->push($block);
if ($this->blocks->count() > self::MAX_BLOCKS)
throw new SlackSyntaxException(sprintf('Modal can only have %d blocks',self::MAX_BLOCKS));
return $this;
}
/** /**
* The data that will be returned when converted to JSON. * The data that will be returned when converted to JSON.
@ -37,77 +70,114 @@ class Modal extends BlockKit
return ['response_action'=>'clear']; return ['response_action'=>'clear'];
case 'update': case 'update':
return ['response_action'=>'update','view'=>$this->_data]; return ['response_action'=>'update','view'=>parent::jsonSerialize()];
default: default:
return $this->_data; return parent::jsonSerialize();
} }
} }
/* OPTIONAL ITEMS */
public function action(string $string)
{
if (! in_array($string,['clear','update']))
throw new SlackSyntaxException(sprintf('Unknown action %s',$string));
$this->action = $string;
return $this;
}
public function clear_on_close(bool $bool=TRUE): self
{
if ($this->type != 'modal')
throw new SlackSyntaxException(sprintf('clear_on_close is not required for %s',$this->type));
$this->clear_on_close = $bool;
return $this;
}
public function callback_id(string $string): self
{
$this->callback_id = $this->validate('callback_id',$string);
return $this;
}
public function close(string $text): self
{
if ($this->type != 'modal')
throw new SlackSyntaxException(sprintf('Close is not required for %s',$this->type));
$this->close = Text::item($this->validate('close',$text),'plain_text');
return $this;
}
// This is a helper method
public function divider(): self
{
$this->blocks->push(Divider::item());
return $this;
}
/** /**
* Add a block to the modal * @param string $string
*
* @param Blocks $blocks
* @return $this * @return $this
* @note A custom identifier that must be unique for all views on a per-team basis.
*/ */
public function setBlocks(Blocks $blocks): self public function external_id(string $string): self
{ {
$this->_data->put('blocks',$blocks); $this->external_id = $string;
return $this; return $this;
} }
public function setCallback(string $id): self public function notify_on_close(bool $bool=TRUE): self
{ {
$this->_data->put('callback_id',$id); if ($this->type != 'modal')
throw new SlackSyntaxException(sprintf('notify_on_close is not required for %s',$this->type));
$this->notify_on_close = $bool;
return $this; return $this;
} }
/* public function private_metadata(string $string): self
public function close(string $text='Cancel'): self
{ {
$this->_data->put('close', $this->private_metadata = $this->validate('private_metadata',$string);
[
'type'=>'plain_text',
'text'=>$text,
'emoji'=>true,
]);
return $this; return $this;
} }
public function meta(string $id): self // This is a helper method
public function spacer(): self
{ {
$this->_data->put('private_metadata',$id); $this->blocks->push(Section::item(Text::item(' ')));
return $this; return $this;
} }
public function notifyClose(): self public function submit(string $text): self
{ {
$this->_data->put('notify_on_close',TRUE); if ($this->type != 'modal')
throw new SlackSyntaxException(sprintf('Submit is not required for %s',$this->type));
$this->submit = Text::item($this->validate('submit',$text),'plain_text');
return $this; return $this;
} }
public function private(array $data): self public function submit_disabled(bool $bool): self
{ {
$this->_data->put('private_metadata',json_encode($data)); if ($this->type != 'modal')
throw new SlackSyntaxException(sprintf('submit_disabled is not required for %s',$this->type));
$this->submit_disabled = $bool;
return $this; return $this;
} }
public function submit(string $text='Submit'): self
{
$this->_data->put('submit',
[
'type'=>'plain_text',
'text'=>$text,
'emoji'=>true,
]);
return $this;
}
*/
} }

View File

@ -2,11 +2,14 @@
namespace Slack\Channels; namespace Slack\Channels;
use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log;
class SlackBotChannel class SlackBotChannel
{ {
public function send($notifiable,Notification $notification) private const LOGKEY = 'SBC';
public function send($notifiable,Notification $notification): void
{ {
if (! $co = $notifiable->routeNotificationFor('slackapp',$notification)) { if (! $co = $notifiable->routeNotificationFor('slackapp',$notification)) {
return; return;
@ -15,6 +18,14 @@ class SlackBotChannel
$o = $notification->toSlack($notifiable); $o = $notification->toSlack($notifiable);
$o->setChannel($co); $o->setChannel($co);
return $o->post(); Log::debug(sprintf('%s:Sending Event to Channel [%s]',self::LOGKEY,$co->channel_id));
try {
$result = $o->post();
} catch (\Exception $e) {
Log::error(sprintf('Error posting to slack [%s]',$e->getMessage()));
return;
}
} }
} }

View File

@ -17,9 +17,9 @@ class Payload implements \ArrayAccess, \JsonSerializable
* *
* @param array $data The payload data. * @param array $data The payload data.
*/ */
public function __construct(array $data) public function __construct(array $data,bool $key=FALSE)
{ {
$this->data = $data; $this->data = $key ? ['payload'=>$data ] : $data;
} }
/** /**
@ -33,7 +33,7 @@ class Payload implements \ArrayAccess, \JsonSerializable
$data = json_decode((string)$json,true); $data = json_decode((string)$json,true);
if (json_last_error() !== JSON_ERROR_NONE || (! is_array($data))) { if (json_last_error() !== JSON_ERROR_NONE || (! is_array($data))) {
throw new \UnexpectedValueException('Invalid JSON message.'); throw new \UnexpectedValueException('Invalid JSON message:'.serialize($data));
} }
return new static($data); return new static($data);

View File

@ -9,6 +9,8 @@ use GuzzleHttp\ClientInterface;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Laminas\Log\Logger;
use Laminas\Log\Writer\Stream;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use React\Promise\Deferred; use React\Promise\Deferred;
use React\Promise; use React\Promise;
@ -25,7 +27,7 @@ class SocketMode extends API
private WebSocket $websocket; private WebSocket $websocket;
/** /**
* @var \Zend\Log\Logger Logger for this client * @var Laminas\Log\Logger Logger for this client
*/ */
protected $logger = null; protected $logger = null;
private array $pendingMessages; private array $pendingMessages;
@ -33,8 +35,8 @@ class SocketMode extends API
public function __construct(LoopInterface $loop, ClientInterface $httpClient = null) { public function __construct(LoopInterface $loop, ClientInterface $httpClient = null) {
parent::__construct($loop, $httpClient); parent::__construct($loop, $httpClient);
$this->logger = new \Zend\Log\Logger(); $this->logger = new Logger;
$this->logger->addWriter(new \Zend\Log\Writer\Stream('php://stderr')); $this->logger->addWriter(new Stream('php://stderr'));
} }
/** /**
@ -87,14 +89,15 @@ class SocketMode extends API
// initiate the websocket connection // initiate the websocket connection
// write PHPWS things to the existing logger // write PHPWS things to the existing logger
$this->websocket = new WebSocket($response['url'].($this->debug_reconnect ? '&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) { $this->websocket->on('message', function ($message) use ($response) {
Log::debug(sprintf('%s:- Calling onMessage ...',self::LOGKEY),['m'=>__METHOD__]); Log::debug(sprintf('%s:- Calling onMessage ...',self::LOGKEY),['m'=>__METHOD__,'payload'=>serialize($response),'message'=>serialize($message)]);
$this->onMessage($message); $this->onMessage($message);
}); });
return $this->websocket->open(); return $this->websocket->open();
}, function($exception) use ($deferred) { }, function($exception) use ($deferred) {
Log::error(sprintf('%s:! Could not connect to Slack API [%s]...',self::LOGKEY,$exception->getMessage()),['m'=>__METHOD__]);
// if connection was not successful // if connection was not successful
$deferred->reject(new ConnectionException( $deferred->reject(new ConnectionException(
'Could not connect to Slack API: '.$exception->getMessage(), 'Could not connect to Slack API: '.$exception->getMessage(),
@ -118,6 +121,14 @@ class SocketMode extends API
'Could not connect to WebSocket: '.$data['error']['msg'], 'Could not connect to WebSocket: '.$data['error']['msg'],
$data['error']['code'])); $data['error']['code']));
}); });
}, function($exception) use ($deferred) {
Log::error(sprintf('%s:! Could not connect to Slack API [%s]...',self::LOGKEY,$exception->getMessage()),['m'=>__METHOD__]);
// if connection was not successful
$deferred->reject(new ConnectionException(
'Could not connect to Slack API: '.$exception->getMessage(),
$exception->getCode()
));
}); });
return $deferred->promise(); return $deferred->promise();
@ -158,7 +169,11 @@ class SocketMode extends API
Log::debug(sprintf('%s:+ Start',self::LOGKEY),['m'=>__METHOD__]); Log::debug(sprintf('%s:+ Start',self::LOGKEY),['m'=>__METHOD__]);
// parse the message and get the event name // parse the message and get the event name
try {
$payload = Payload::fromJson($message->getData()); $payload = Payload::fromJson($message->getData());
} catch (\UnexpectedValueException $e) {
Log::error(sprintf('%s:! ERROR in json payload in onMessage',self::LOGKEY),['message'=>serialize($message)]);
}
$emitted = FALSE; $emitted = FALSE;
if (isset($payload['type'])) { if (isset($payload['type'])) {

View File

@ -3,8 +3,8 @@
namespace Slack\Command; namespace Slack\Command;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Base as SlackBase; use Slack\Base as SlackBase;
use Slack\Blockkit\Blocks;
use Slack\Message; use Slack\Message;
abstract class Base extends SlackBase abstract class Base extends SlackBase
@ -58,12 +58,12 @@ abstract class Base extends SlackBase
$o = new Message; $o = new Message;
if (! $this->channel() || ! $this->channel()->active) { if (! $this->channel() || ! $this->channel()->active) {
$blocks = new Blocks; $o->addAttachment(
Message\Attachment::item()
$blocks->addHeader(':robot_face: Bot not in this channel'); ->title(':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'))); ->text(sprintf('Please add %s to this channel and try this again.',$this->team()->bot->name ?: 'the BOT'))
->color('#ff0000')
$o->setAttachments((new Message\Attachments())->setBlocks($blocks)->setColor('#ff0000')); );
} }
return $o->isEmpty() ? NULL : $o; return $o->isEmpty() ? NULL : $o;

View File

@ -5,6 +5,7 @@ namespace Slack\Command;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Client\Payload; use Slack\Client\Payload;
class Factory { class Factory {

View File

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

View File

@ -2,8 +2,8 @@
namespace Slack\Command; namespace Slack\Command;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Message; use Slack\Message;
/** /**
@ -24,7 +24,7 @@ final class Unknown extends Base
{ {
$o = new Message; $o = new Message;
$o->setText(sprintf('I didnt understand your command "%s". You might like to try `%s help` instead.',$this->command,$this->slashcommand)); $o->text(sprintf('I didnt understand your command "%s". You might like to try `%s help` instead.',$this->command,$this->slashcommand));
return $o; return $o;
} }

View File

@ -5,9 +5,11 @@ namespace Slack\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use React\EventLoop\Loop; use React\EventLoop\Loop;
use Slack\Client\SocketMode; use Slack\Client\SocketMode;
use Slack\Command\Factory as SlackCommandFactory; use Slack\Command\Factory as SlackCommandFactory;
use Slack\Event\Factory as SlackEventFactory; use Slack\Event\Factory as SlackEventFactory;
use Slack\Exceptions\SlackException;
use Slack\Interactive\Factory as SlackInteractiveFactory; use Slack\Interactive\Factory as SlackInteractiveFactory;
class SlackSocketClient extends Command class SlackSocketClient extends Command
@ -28,23 +30,18 @@ class SlackSocketClient extends Command
*/ */
protected $description = 'Start SocketMode Client'; protected $description = 'Start SocketMode Client';
public function __construct()
{
// Make sure our socket_token is defined
if (! config('slack.socket_token'))
throw new \Exception('SocketMode Client Token not defined.');
parent::__construct();
}
/** /**
* Execute the console command. * Execute the console command.
* *
* @return void * @return void
* @throws \Exception * @throws SlackException
*/ */
public function handle() public function handle()
{ {
// Make sure our socket_token is defined
if (! config('slack.socket_token'))
throw new SlackException('SocketMode Client Token not defined.');
$loop = Loop::get(); $loop = Loop::get();
$client = new SocketMode($loop); $client = new SocketMode($loop);

View File

@ -2,8 +2,8 @@
namespace Slack\Event; namespace Slack\Event;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Base as SlackBase; use Slack\Base as SlackBase;
abstract class Base extends SlackBase abstract class Base extends SlackBase

View File

@ -2,10 +2,10 @@
namespace Slack\Event; namespace Slack\Event;
use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Client\Payload; use Slack\Client\Payload;
class Factory { class Factory {

View File

@ -2,7 +2,6 @@
namespace Slack\Event; namespace Slack\Event;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
/** /**

View File

@ -0,0 +1,7 @@
<?php
namespace Slack\Exceptions;
class SlackSyntaxException extends SlackException
{
}

View File

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

View File

@ -0,0 +1,45 @@
<?php
namespace Slack\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use Slack\Client\Payload;
use Slack\Message;
use Slack\Interactive\Factory as InteractiveMessageFactory;
final class InteractiveMessageController extends Controller
{
private const LOGKEY = 'CIM';
/**
* Fire slack event
*
* @param Request $request
* @return \Illuminate\Http\Response|\Laravel\Lumen\Http\ResponseFactory
*/
public function fire(Request $request)
{
$event = InteractiveMessageFactory::make(new Payload(json_decode($request->payload,TRUE),TRUE));
Log::debug(sprintf('%s:Firing Event [%s] and responding [%s]',static::LOGKEY,get_class($event),$event->respondNow));
if ($event->respondNow) {
if (! method_exists($event,'respond')) {
Log::alert(sprintf('%s:Cant respond to Event [%s], no respond method',static::LOGKEY,get_class($event)));
} else {
$result = $event->respond();
return (($result instanceof Message) AND $result->isEmpty()) ? NULL : $result;
}
} else {
event($event);
Log::info(sprintf('%s:Dispatched Event [%s]',static::LOGKEY,get_class($event)));
return response('IM Event Processed',200);
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Slack\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use Slack\Client\Payload;
use Slack\Options\Factory as SlackOptionsFactory;
final class InteractiveOptionsController extends Controller
{
private const LOGKEY = 'CIO';
/**
* Fire slack event
*
* @param Request $request
* @return \Illuminate\Http\Response|\Laravel\Lumen\Http\ResponseFactory
*/
public function fire(Request $request)
{
$event = SlackOptionsFactory::make(new Payload(json_decode($request->payload,TRUE),TRUE));
Log::debug(sprintf('%s:Firing Event [%s] and responding [%s]',static::LOGKEY,get_class($event),$event->respondNow));
if ($event->respondNow) {
if (! method_exists($event,'respond')) {
Log::alert(sprintf('%s:Cant respond to Event [%s], no respond method',static::LOGKEY,get_class($event)),['m'=>__METHOD__]);
} else {
return ($x=$event->respond())->isEmpty() ? NULL : $x;
}
} else {
event($event);
Log::info(sprintf('%s:Dispatched Event [%s]',static::LOGKEY,get_class($event)),['m'=>__METHOD__]);
return response('IO Event Processed',200);
}
}
}

View File

@ -2,23 +2,25 @@
namespace Slack\Http\Controllers; namespace Slack\Http\Controllers;
use App\Http\Controllers\Controller;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\Redirect;
use Slack\Jobs\TeamUpdate; use Slack\Jobs\TeamUpdate;
use Slack\Models\{Enterprise,Team,Token,User}; use Slack\Models\{Enterprise,Team,Token,User};
use App\Http\Controllers\Controller;
use App\Models\Slack\Enterprise as AppEnterprise;
use App\Models\Slack\Team as AppTeam;
use App\Models\Slack\User as AppUser;
class SlackAppController extends Controller class SlackAppController extends Controller
{ {
private const LOGKEY = 'CSA'; protected const LOGKEY = 'CSA';
private const slack_authorise_url = 'https://slack.com/oauth/v2/authorize'; private const slack_authorise_url = 'https://slack.com/oauth/v2/authorize';
private const slack_oauth_url = 'https://slack.com/api/oauth.v2.access'; private const slack_oauth_url = 'https://slack.com/api/oauth.v2.access';
private const slack_button = 'https://platform.slack-edge.com/img/add_to_slack.png';
/** /**
* Install this app - Slack Button * Install this app - Slack Button
@ -26,16 +28,21 @@ class SlackAppController extends Controller
public function button() public function button()
{ {
return sprintf( return sprintf(
'<a href="%s?%s"><img alt="Add to Slack" height="40" width="139" src="%s" srcSet="%s 1x, %s@2x.png 2x" /></a>', '<a href="%s?%s" style="align-items:center;color:#000;background-color:#fff;border:1px solid #ddd;border-radius:4px;display:inline-flex;font-family:Lato, sans-serif;font-size:18px;font-weight:600;height:40px;justify-content:center;text-decoration:none;width:180px">'
.'<svg xmlns="http://www.w3.org/2000/svg" style="height:24px;width:24px;margin-right:12px" viewBox="0 0 122.8 122.8">'
.'<path d="M25.8 77.6c0 7.1-5.8 12.9-12.9 12.9S0 84.7 0 77.6s5.8-12.9 12.9-12.9h12.9v12.9zm6.5 0c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v32.3c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V77.6z" fill="#e01e5a"></path>'
.'<path d="M45.2 25.8c-7.1 0-12.9-5.8-12.9-12.9S38.1 0 45.2 0s12.9 5.8 12.9 12.9v12.9H45.2zm0 6.5c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H12.9C5.8 58.1 0 52.3 0 45.2s5.8-12.9 12.9-12.9h32.3z" fill="#36c5f0"></path>'
.'<path d="M97 45.2c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9-5.8 12.9-12.9 12.9H97V45.2zm-6.5 0c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V12.9C64.7 5.8 70.5 0 77.6 0s12.9 5.8 12.9 12.9v32.3z" fill="#2eb67d"></path>'
.'<path d="M77.6 97c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9-12.9-5.8-12.9-12.9V97h12.9zm0-6.5c-7.1 0-12.9-5.8-12.9-12.9s5.8-12.9 12.9-12.9h32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H77.6z" fill="#ecb22e"></path>'
.'</svg>Add to Slack</a>',
self::slack_authorise_url, self::slack_authorise_url,
http_build_query($this->parameters()), http_build_query($this->parameters())
self::slack_button,self::slack_button,self::slack_button
); );
} }
public function home() public function home()
{ {
return sprintf('Hi, for instructions on how to install me, please reach out to <strong>@deon.</strong>'); return sprintf('Hi, for instructions on how to install me, please reach out to <strong>%s</strong>.',config('slack.app_admin','Your slack admin'));
} }
public function setup() public function setup()
@ -47,10 +54,11 @@ class SlackAppController extends Controller
* Install this Slack Application. * Install this Slack Application.
* *
* @param Request $request * @param Request $request
* @param bool $oauth
* @return string * @return string
* @throws \GuzzleHttp\Exception\GuzzleException * @throws \GuzzleHttp\Exception\GuzzleException
*/ */
public function install(Request $request) public function install(Request $request,bool $oauth=FALSE)
{ {
if (! config('slack.client_id') OR ! config('slack.client_secret')) if (! config('slack.client_id') OR ! config('slack.client_secret'))
abort(403,'Slack ClientID or Secret not set'); abort(403,'Slack ClientID or Secret not set');
@ -73,17 +81,20 @@ class SlackAppController extends Controller
if (App::environment() == 'local') if (App::environment() == 'local')
file_put_contents('/tmp/install',print_r($output,TRUE)); file_put_contents('/tmp/install',print_r($output,TRUE));
if (! $output->ok) if (! $output->ok) {
abort(403,'Something didnt work, status not OK ['.(string)$response->getBody().']'); switch ($output->error) {
case 'invalid_code': abort(403,'Did you use the install button?');
default:
abort(500,'Something didnt work, status not OK ['.(string)$response->getBody().']');
}
}
// Are we an enterprise? // Are we an enterprise?
$eo = NULL; $eo = NULL;
if ($output->enterprise) { if ($output->enterprise) {
$eo = Enterprise::firstOrNew( $eo = (class_exists(AppEnterprise::class) ? new AppEnterprise : new Enterprise)
[ ->firstOrNew(['enterprise_id'=>$output->enterprise->id]);
'enterprise_id'=>$output->enterprise->id
]);
$eo->name = $output->enterprise->name; $eo->name = $output->enterprise->name;
$eo->active = TRUE; $eo->active = TRUE;
@ -91,10 +102,8 @@ class SlackAppController extends Controller
} }
// Store our team details // Store our team details
$so = Team::firstOrNew( $so = (class_exists(AppTeam::class) ? new AppTeam : new Team)
[ ->firstOrNew(['team_id'=>$output->team->id]);
'team_id'=>$output->team->id
]);
// We just installed, so we'll make it active, even if it already exists. // We just installed, so we'll make it active, even if it already exists.
$so->description = $output->team->name; $so->description = $output->team->name;
@ -108,7 +117,7 @@ class SlackAppController extends Controller
$to = $so->token; $to = $so->token;
if (! $to) { if (! $to) {
$to = new Token; $to = class_exists(AppToken::class) ? new AppToken : new Token;
$to->description = 'App: Oauth'; $to->description = 'App: Oauth';
} }
@ -121,10 +130,9 @@ class SlackAppController extends Controller
// Create the bot user // Create the bot user
// Store the user who install, and make them admin // Store the user who install, and make them admin
$bo = User::firstOrNew( $bo = (class_exists(AppUser::class) ? new AppUser : new User)
[ ->firstOrNew(['user_id'=>$output->bot_user_id]);
'user_id'=>$output->bot_user_id,
]);
$bo->enterprise_id = $eo ? $eo->id : NULL; $bo->enterprise_id = $eo ? $eo->id : NULL;
$bo->team_id = $so->id; $bo->team_id = $so->id;
$bo->active = 0; $bo->active = 0;
@ -137,10 +145,8 @@ class SlackAppController extends Controller
Log::debug(sprintf('%s:BOT Created [%s]',self::LOGKEY,$bo->id),['m'=>__METHOD__]); Log::debug(sprintf('%s:BOT Created [%s]',self::LOGKEY,$bo->id),['m'=>__METHOD__]);
// Store the user who install, and make them admin // Store the user who install, and make them admin
$uo = User::firstOrNew( $uo = (class_exists(AppUser::class) ? new AppUser : new User)
[ ->firstOrNew(['user_id'=>$output->authed_user->id]);
'user_id'=>$output->authed_user->id,
]);
$uo->enterprise_id = $eo ? $eo->id : NULL; $uo->enterprise_id = $eo ? $eo->id : NULL;
$uo->team_id = $eo ? NULL : $so->id; $uo->team_id = $eo ? NULL : $so->id;
@ -154,7 +160,7 @@ class SlackAppController extends Controller
$so->admin_id = $uo->id; $so->admin_id = $uo->id;
$so->save(); $so->save();
return sprintf('All set up! Head back to your slack instance <strong>%s</strong>.',$so->description); return $oauth ? $output : sprintf('All set up! Head back to your slack instance <strong>%s</strong>.',$so->description);
} }
/** /**

View File

@ -0,0 +1,97 @@
<?php
namespace Slack\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use Slack\Client\Payload;
use Slack\Command\Factory;
use Slack\Message;
use Slack\Message\Attachment;
final class SlashCommandController extends Controller
{
private const LOGKEY = 'CSC';
public function fire(Request $request): ?Message
{
$command = Factory::make(new Payload($request->all(),TRUE));
$o = new Message;
// If the team is not active.
if (! $command->team()->active) {
Log::notice(sprintf('%s:IGNORING command, Team INACTIVE [%s]',static::LOGKEY,$command->team()->id));
$o->text(sprintf("Sorry, it would appear you are not allowed to use me.\nYour instance hasnt been enabled.\nAsk an admin to install me [%s].",$command->team()->team_id));
return $o;
}
if ($command->channel()) {
// If the channel is a direct message
if (str_starts_with($command->channel()->channel_id,'D')) {
Log::notice(sprintf('%s:IGNORING command, Channel is for direct messages [%s]',static::LOGKEY,$command->channel()->id));
$o->text('Sorry, I dont work with direct messages yet. Please use me in a channel.');
return $o;
}
// If the channel is not enabled
if (! $command->channel()->is_allowed) {
Log::notice(sprintf('%s:IGNORING command, Channel INACTIVE [%s]',static::LOGKEY,$request->channel_id));
$o->text(sprintf('Sorry, it would appear this channel is not allowed to use me. %s may be able to help.',$command->team()->owner->slack_user));
return $o;
}
// If the user hasnt been checked yet
if (is_null($command->user()->active)) {
Log::notice(sprintf('%s:IGNORING command, User UNKNOWN [%s]',static::LOGKEY,$command->user()->id));
$o->text('Sorry, unfortunately I cannot work with you - as I dont know who you are (yet)...');
$a = new Attachment;
if ($command->user()->created_at < Carbon::now()->subMinutes(5))
$a->color('#CC0000')
->text(sprintf('I probably should have figured out who you are by now, so something might be wrong. You might like to check with the %s.',$command->team()->owner->slack_user));
else
$a->color('#008800')
->text(sprintf('If I havent worked out who you are with the next 5 mins, you might like to check with the %s.',$command->team()->owner->slack_user));
$o->addAttachment($a);
return $o;
}
// If the user is not enabled
if (! $command->user()->isAllowed($command->channel())) {
Log::notice(sprintf('%s:IGNORING command, User INACTIVE [%s]',static::LOGKEY,$command->user()->id));
$o->text(sprintf('Sorry, it would appear that you are not allowed to use me. %s may be able to help.',$command->team()->owner->slack_user));
return $o;
}
}
// Check that we are active in the channel
if (! $command->channel() OR ! $command->channel()->active) {
$o->text('Hi, I am the *Ask the Experts* Bot');
$a = new Attachment;
$a->title('Greetings!')
->text('Im not active in this channel, so I cant do anything here unless you invite me. Please invite me with `/invite`')
->color('#FF8080');
$o->addAttachment($a);
return $o;
}
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));
}
return ($x=$command->respond())->isEmpty() ? NULL : $x;
}
}

View File

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

View File

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

View File

@ -3,11 +3,14 @@
namespace Slack\Interactive; namespace Slack\Interactive;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Base as SlackBase; use Slack\Base as SlackBase;
abstract class Base extends SlackBase abstract class Base extends SlackBase
{ {
// Does the event respond with a reply to the HTTP request, or via a post with a trigger // Does the event respond with a reply to the HTTP request, or via a post with a trigger
// Child class should have a respond() function.
// (There should be a local implementation of the child class should respondNow = TRUE)
public $respondNow = FALSE; public $respondNow = FALSE;
// When retrieving multiple action values, this is the index we are retrieving. // When retrieving multiple action values, this is the index we are retrieving.
@ -32,6 +35,7 @@ abstract class Base extends SlackBase
* + user_id * + user_id
* @param string $key * @param string $key
* @return mixed|object * @return mixed|object
* @throws \Exception
*/ */
public function __get(string $key) public function __get(string $key)
{ {
@ -43,6 +47,12 @@ abstract class Base extends SlackBase
case 'user_id': case 'user_id':
return object_get($this->_data,'user.id'); return object_get($this->_data,'user.id');
case 'callback_key':
return $this->keyitem('id',$this->callback_id);
case 'callback_value':
return $this->keyitem('value',$this->callback_id);
case 'callback_id': case 'callback_id':
case 'trigger_id': case 'trigger_id':
case 'type': case 'type':

View File

@ -4,6 +4,7 @@ namespace Slack\Interactive;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Slack\Models\Channel; use Slack\Models\Channel;
/** /**
@ -53,38 +54,71 @@ use Slack\Models\Channel;
* ) * )
* ) * )
*/ */
class BlockActions extends Base final class BlockActions extends Base
{ {
private const LOGKEY = 'IBA'; private const LOGKEY = 'IBA';
public function __get($key) public function __get($key)
{ {
switch ($key) { switch ($key) {
case 'actions': case 'action':
return object_get($this->_data,$key); return Arr::get(object_get($this->_data,'actions'),$this->index);
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. // An event can have more than 1 action, each action can have 1 value.
case 'action_key': case 'action_key':
return $this->action('action'); return $this->keyitem('id',$this->action_id);
case 'action_value': case 'action_value':
return $this->action('value'); return $this->keyitem('value',$this->action_id);
// Interactive Messages have a block_id.
case 'block_key':
return $this->keyitem('id',$this->block_id);
case 'block_value':
return $this->keyitem('value',$this->block_id);
case 'actions':
case 'response_url':
return object_get($this->_data,$key);
case 'action_id':
case 'block_id':
return object_get($this->action,$key);
case 'callback_id': case 'callback_id':
return object_get($this->_data,'view.callback_id'); switch ($this->container_type) {
case 'view':
// For app hometab, the callback is in the view->callback_id array.
return object_get($this->_data,sprintf('%s.%s',$this->container_type,$key));
case 'message':
case 'message_attachment':
return NULL;
default:
throw new \Exception('Unknown container type: '.$this->container_type);
}
case 'container_type':
return object_get($this->_data,'container.type');
case 'channel_id':
return object_get($this->_data,'channel.id') ?: Channel::findOrFail($this->keyitem('value',object_get($this->action,'action_id')))->channel_id;
case 'keys': case 'keys':
return collect(object_get($this->_data,'view.blocks'))->pluck('accessory.action_id'); return collect(object_get($this->_data,'view.blocks'))->pluck('accessory.action_id');
// For Block Actions that are messages // For Block Actions that are messages
case 'message_ts': case 'message_ts':
return object_get($this->_data,'message.ts'); switch ($this->container_type) {
// Ephemeral messages
case 'message':
return object_get($this->_data,'container.message_ts');
case 'channel_id': default:
return object_get($this->_data,'channel.id') ?: Channel::findOrFail($this->action('value'))->channel_id; return object_get($this->_data,'message.ts');
}
case 'team_id': // view.team_id represent workspace publishing view case 'team_id': // view.team_id represent workspace publishing view
return object_get($this->_data,'user.team_id'); return object_get($this->_data,'user.team_id');
@ -93,70 +127,40 @@ class BlockActions extends Base
return object_get($this->_data,'view.id'); return object_get($this->_data,'view.id');
case 'value': case 'value':
switch (Arr::get(object_get($this->_data,'actions'),$this->index)->type) { switch (object_get($this->action,'type')) {
case 'external_select': case 'external_select':
case 'overflow': case 'overflow':
case 'static_select': case 'static_select':
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'selected_option.value'); return object_get($this->action,'selected_option.value');
case 'multi_static_select': case 'multi_static_select':
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'selected_options.value'); return object_get($this->action,'selected_options.value');
default:
return object_get(Arr::get(object_get($this->_data,'actions'),$this->index),$key);
}
case 'values': case 'button':
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: default:
return count(object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'value')); return object_get($this->action,$key);
} }
case 'value_count': case 'value_count':
return count($this->values); switch ($x=object_get($this->action,'type')) {
// @todo To Check
case 'external_select':
// @todo To Check
case 'overflow':
// @todo To Check
throw new \Exception('To be implemented: ',$x);
case 'static_select':
return count(object_get($this->action,'selected_option'));
case 'multi_static_select':
return collect(object_get($this->action,'selected_options'))->pluck('value');
default:
return count(object_get($this->action,'value'));
}
default: default:
return parent::__get($key); return parent::__get($key);
} }
} }
/**
* Separate out an action command to the id that the command relates to
*
* @param string $key
* @return string|null
*/
private function action(string $key): ?string
{
$regex = '/^([a-z_]+)\|([0-9]+)$/';
$action = NULL;
$value = NULL;
// We only take the action up to the pipe symbol
$action_key = object_get(Arr::get(object_get($this->_data,'actions'),$this->index),'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_key;
case 'value':
return $value;
}
return NULL;
}
/** /**
* Some block actions are triggered by messages, and thus dont have a callback_id * Some block actions are triggered by messages, and thus dont have a callback_id
* *
@ -164,6 +168,22 @@ class BlockActions extends Base
*/ */
public function isMessage(): bool public function isMessage(): bool
{ {
return object_get($this->_data,'message') ? TRUE : FALSE; return (bool)object_get($this->_data,'message');
}
/**
* 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

@ -2,14 +2,14 @@
namespace Slack\Interactive; namespace Slack\Interactive;
use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Client\Payload; use Slack\Client\Payload;
class Factory { class Factory {
private const LOGKEY = 'SIF'; protected const LOGKEY = 'SIF';
/** /**
* @var array event type to event class mapping * @var array event type to event class mapping
@ -31,7 +31,7 @@ class Factory {
*/ */
public static function create(string $type,array $request): Base public static function create(string $type,array $request): Base
{ {
$class = Arr::get(self::map,$type,Unknown::class); $class = Arr::get(config('slack.interactive',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__]); Log::debug(sprintf('%s:Working out Interactive Message Event Class for [%s] as [%s]',static::LOGKEY,$type,$class),['m'=>__METHOD__]);
if (App::environment() == 'local') if (App::environment() == 'local')

View File

@ -4,6 +4,7 @@ namespace Slack\Interactive;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Message; use Slack\Message;
/** /**
@ -57,7 +58,7 @@ use Slack\Message;
*/ */
class InteractiveMessage extends Base class InteractiveMessage extends Base
{ {
private const LOGKEY = 'IIM'; protected const LOGKEY = 'IIM';
// Does the event respond with a reply to the HTTP request, or via a post with a trigger // Does the event respond with a reply to the HTTP request, or via a post with a trigger
public $respondNow = TRUE; public $respondNow = TRUE;
@ -90,29 +91,16 @@ class InteractiveMessage extends Base
} }
} }
/**
* This method should be overridden by a local implementation
*
* @return Message
*/
public function respond(): Message public function respond(): Message
{ {
Log::info(sprintf('%s:Interactive Message - Callback [%s] Name [%s] Type [%s]',static::LOGKEY,$this->callback_id,$this->name,$this->type),['m'=>__METHOD__]); Log::info(sprintf('%s:Interactive Message - Callback [%s] Name [%s] Type [%s]',static::LOGKEY,$this->callback_id,$this->name,$this->type),['m'=>__METHOD__]);
Log::notice(sprintf('%s:Unhandled action [%s]',static::LOGKEY,$this->callback_id),['m'=>__METHOD__]);
$action = NULL; return (new Message)->text('That didnt work, I didnt know what to do with your button - you might like to tell '.$this->team()->owner->slack_user);
$id = NULL;
if (preg_match('/^(.*)\|([0-9]+)/',$this->callback_id)) {
[$action,$id] = explode('|',$this->callback_id,2);
} elseif (preg_match('/^[a-z_]+$/',$this->callback_id)) {
$id = $this->name;
$action = $this->callback_id;
} else {
// If we get here, its an action that we dont know about.
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',static::LOGKEY,$this->callback_id),['m'=>__METHOD__]);
}
switch ($action) {
default:
Log::notice(sprintf('%s:Unhandled ACTION [%s]',static::LOGKEY,$action),['m'=>__METHOD__]);
return (new Message)->setText('That didnt work, I didnt know what to do with your button - you might like to tell '.$this->team()->owner->slack_user);
}
} }
} }

View File

@ -2,7 +2,6 @@
namespace Slack\Interactive; namespace Slack\Interactive;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
/** /**
@ -10,7 +9,7 @@ use Illuminate\Support\Facades\Log;
* *
* @package Slack\Interactive * @package Slack\Interactive
*/ */
class Unknown extends Base final class Unknown extends Base
{ {
public function __construct(array $request) public function __construct(array $request)
{ {

View File

@ -65,7 +65,7 @@ use Illuminate\Support\Arr;
*/ */
class ViewClosed extends Base class ViewClosed extends Base
{ {
private const LOGKEY = 'IVC'; protected const LOGKEY = 'IVC';
public function __get($key) public function __get($key)
{ {

View File

@ -2,10 +2,8 @@
namespace Slack\Interactive; namespace Slack\Interactive;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Models\Team;
use Slack\Blockkit\Modal; use Slack\Blockkit\Modal;
/** /**
@ -15,7 +13,7 @@ use Slack\Blockkit\Modal;
*/ */
class ViewSubmission extends Base class ViewSubmission extends Base
{ {
private const LOGKEY = 'IVS'; protected const LOGKEY = 'IVS';
// View Submissions must respond with via a trigger or inline // View Submissions must respond with via a trigger or inline
public $respondNow = TRUE; public $respondNow = TRUE;
@ -23,12 +21,24 @@ class ViewSubmission extends Base
public function __get($key) public function __get($key)
{ {
switch ($key) { switch ($key) {
case 'blocks':
return collect(object_get($this->_data,'view.'.$key));
case 'callback_id': case 'callback_id':
return object_get($this->_data,'view.'.$key); return object_get($this->_data,'view.callback_id');
case 'callback_key':
return $this->keyitem('id',$this->callback_id);
case 'callback_value':
return $this->keyitem('value',$this->callback_id);
case 'meta': case 'meta':
return object_get($this->_data,'view.private_metadata'); return object_get($this->_data,'view.private_metadata');
case 'state':
return collect(object_get($this->_data,'view.'.$key.'.values'));
case 'view_id': case 'view_id':
return object_get($this->_data,'view.id'); return object_get($this->_data,'view.id');
@ -37,57 +47,29 @@ class ViewSubmission extends Base
} }
} }
private function blocks(): Collection /**
{ * This method should be overridden by a local implementation
$result = collect(); *
* @return Modal
foreach (object_get($this->_data,'view.blocks',[]) as $id=>$block) { */
switch (object_get($block,'type')) {
case 'input':
$result->put($block->element->action_id,$block->block_id);
break;
case 'section':
$result->put($block->block_id,$id);
break;
}
}
return $result;
}
public function value(string $block_id): ?string
{
$key = Arr::get($this->blocks(),$block_id);
// Return the state value, or the original block value
return object_get($this->_data,'view.state.values.'.$key.'.'.$block_id.'.value') ?: object_get(Arr::get(object_get($this->_data,'view.blocks'),$key),'text.text','');
}
public function respond(): Modal public function respond(): Modal
{ {
// Do some magic with event data // Do some magic with event data
Log::info(sprintf('%s:View Submission for Callback [%s] User [%s] in [%s]',self::LOGKEY,$this->callback_id,$this->user_id,$this->team_id),['m'=>__METHOD__]); Log::info(sprintf('%s:View Submission for Callback [%s] User [%s] in [%s]',self::LOGKEY,$this->callback_id,$this->user_id,$this->team_id),['m'=>__METHOD__]);
Log::notice(sprintf('%s:Unhandled action [%s]',self::LOGKEY,$this->callback_key),['m'=>__METHOD__]);
$action = NULL; return new Modal;
$id = NULL; }
if (preg_match('/^(.*)\|([0-9]+)/',$this->callback_id)) { public function value(string $block_id,string $action_id=NULL): ?string
[$action,$cid] = explode('|',$this->callback_id,2); {
// If there is no state we need to search out blocks for the block_id
} elseif (preg_match('/^[a-z_]+$/',$this->callback_id)) { if (! $this->state->count()) {
$action = $this->callback_id; $block = $this->blocks->search(function($item) use ($block_id) { return $item->block_id == $block_id; });
return $block !== FALSE ? object_get($this->blocks->get($block),$action_id) : NULL;
} else { } else {
// If we get here, its an action that we dont know about. return object_get($this->state->get($block_id),$action_id.'.value');
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',static::LOGKEY,$this->callback_id),['m'=>__METHOD__]); }
}
switch ($action) {
default:
Log::notice(sprintf('%s:Unhandled ACTION [%s]',self::LOGKEY,$action),['m'=>__METHOD__]);
}
return new Modal(new Team);
} }
} }

View File

@ -4,10 +4,11 @@ namespace Slack\Jobs;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Models\{Channel,User}; use Slack\Models\{Channel,User};
use Slack\Exceptions\SlackException; use Slack\Exceptions\SlackException;
class DeleteChat extends Job final class DeleteChat extends Job
{ {
private const LOGKEY = 'JDC'; private const LOGKEY = 'JDC';
@ -21,16 +22,16 @@ class DeleteChat extends Job
public function __construct(Model $o,string $ts) public function __construct(Model $o,string $ts)
{ {
if ($o instanceof Channel) { if ($o instanceof Channel) {
$this->_data['cid'] = $o->channel_id; $this->cid = $o->channel_id;
} elseif ($o instanceof User) { } elseif ($o instanceof User) {
$this->_data['cid'] = $o->user_id; $this->cid = $o->user_id;
} else } else
throw new \Exception('Invalid Model: '.get_class($o)); throw new \Exception('Invalid Model: '.get_class($o));
$this->_data['to'] = $o->team; $this->to = $o->team;
$this->_data['ts'] = $ts; $this->ts = $ts;
} }
/** /**

View File

@ -0,0 +1,46 @@
<?php
namespace Slack\Jobs;
use Illuminate\Support\Facades\Log;
use Slack\Exceptions\SlackException;
use Slack\Message;
final class DeleteResponse extends Job
{
private const LOGKEY = 'JDC';
/**
* Create a new job instance.
*
* @param string $url
*/
public function __construct(string $url)
{
$this->url = $url;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Log::info(sprintf('%s:Start - Delete Response [%s]',static::LOGKEY,$this->url),['m'=>__METHOD__]);
$o = new Message;
$o->text('')
->delete_original();
try {
$o->respond($this->url);
Log::debug(sprintf('%s:Deleted Slack Message: %s',static::LOGKEY,$this->url),['m'=>__METHOD__]);
} catch (SlackException $e) {
Log::error(sprintf('%s:Failed to delete slack message [%s] [%s]',static::LOGKEY,$this->url,$e->getMessage()),['m'=>__METHOD__]);
}
}
}

View File

@ -7,6 +7,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
abstract class Job implements ShouldQueue abstract class Job implements ShouldQueue
{ {
@ -23,10 +24,20 @@ abstract class Job implements ShouldQueue
use InteractsWithQueue, Queueable, SerializesModels; use InteractsWithQueue, Queueable, SerializesModels;
protected $_data = []; protected Collection $_data;
public function __get($key) public function __get($key)
{ {
if (! isset($this->_data))
$this->_data = collect();
return Arr::get($this->_data,$key); return Arr::get($this->_data,$key);
} }
public function __set(string $key,$value) {
if (! isset($this->_data))
$this->_data = collect();
return $this->_data->put($key,$value);
}
} }

View File

@ -3,12 +3,13 @@
namespace Slack\Jobs; namespace Slack\Jobs;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Exceptions\SlackTokenScopeException; use Slack\Exceptions\SlackTokenScopeException;
use Slack\Models\Team; use Slack\Models\Team;
class TeamUpdate extends Job class TeamUpdate extends Job
{ {
private const LOGKEY = 'JTU'; protected const LOGKEY = 'JTU';
/** /**
* Create a new job instance. * Create a new job instance.
@ -17,7 +18,7 @@ class TeamUpdate extends Job
*/ */
public function __construct(Team $to) public function __construct(Team $to)
{ {
$this->_data['to'] = $to; $this->to = $to;
} }
public function handle() public function handle()

View File

@ -12,7 +12,7 @@ class AppHomeOpenedListener implements ShouldQueue
{ {
protected const LOGKEY = 'LAH'; protected const LOGKEY = 'LAH';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -20,11 +20,11 @@ class AppHomeOpenedListener implements ShouldQueue
* @param AppHomeOpened $event * @param AppHomeOpened $event
* @return void * @return void
*/ */
public function handle(AppHomeOpened $event) public function handle(AppHomeOpened $event): void
{ {
// Do some magic with event data // 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__]); 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')); dispatch((new SlackHomeTabUpdate($event->user(),$event->team(TRUE),$event->tab,$event->view))->onQueue('slack'));
} }
} }

View File

@ -2,16 +2,25 @@
namespace Slack\Listeners; namespace Slack\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Jobs\DeleteChat; use Slack\Jobs\DeleteChat;
use Slack\Interactive\BlockActions; use Slack\Interactive\BlockActions;
class BlockActionListener /**
* This class handles BlockActions events.
* It's expected that the local application would implement this class completely, rather than using this
* module's implementation.
*
* @note Since block actions contain a trigger_id, we shouldnt queue this as the trigger_id may have expired by the time
* the queue runs the job. (trigger_id's only last 3 seconds)
*/
class BlockActionListener //implements ShouldQueue
{ {
protected const LOGKEY = 'LBA'; protected const LOGKEY = 'LBA';
// Block actions arent queued, since slack expects a response to the request // public $queue = 'slack';
//public $queue = 'high';
/** /**
* Handle the event. * Handle the event.
@ -55,7 +64,7 @@ class BlockActionListener
switch ($event->action_id) { switch ($event->action_id) {
case 'self_destruct': case 'self_destruct':
// Queue the delete of the message // Queue the delete of the message
dispatch((new DeleteChat($event->user(),$event->message_ts))->onQueue('low')); dispatch((new DeleteChat($event->user(),$event->message_ts))->onQueue('slack'));
// @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 // @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; break;
@ -65,21 +74,4 @@ class BlockActionListener
} }
} }
} }
/**
* 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,30 @@
<?php
namespace Slack\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Slack\Options\BlockSuggestion;
class BlockSuggestionListener implements ShouldQueue
{
private const LOGKEY = 'LBS';
public $queue = 'slack';
/**
* Handle the event.
*
* @note Since BlockSuggestions are interactive, it is unlikely that this is needed.
* @param BlockSuggestion $event
* @return void
* @throws \Exception
*/
public function handle(BlockSuggestion $event): void
{
// Do some magic with event data
Log::info(sprintf('%s:Block Suggestion Callback [%s] User [%s] in [%s]',self::LOGKEY,$event->callback_id,$event->user_id,$event->team_id),['e'=>$event]);
Log::notice(sprintf('%s:Ignoring Block Suggestion [%s]',static::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
}

View File

@ -11,7 +11,7 @@ class ChannelJoinListener implements ShouldQueue
{ {
protected const LOGKEY = 'LCJ'; protected const LOGKEY = 'LCJ';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,7 +19,7 @@ class ChannelJoinListener implements ShouldQueue
* @param MemberJoinedChannel $event * @param MemberJoinedChannel $event
* @return void * @return void
*/ */
public function handle(MemberJoinedChannel $event) public function handle(MemberJoinedChannel $event): void
{ {
// Do some magic with event data // Do some magic with event data
Log::info(sprintf('%s:User [%s] joined Channel [%s]',self::LOGKEY,$event->invited,$event->channel_id),['m'=>__METHOD__]); Log::info(sprintf('%s:User [%s] joined Channel [%s]',self::LOGKEY,$event->invited,$event->channel_id),['m'=>__METHOD__]);

View File

@ -11,7 +11,7 @@ class ChannelLeftListener implements ShouldQueue
{ {
protected const LOGKEY = 'LCL'; protected const LOGKEY = 'LCL';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,7 +19,7 @@ class ChannelLeftListener implements ShouldQueue
* @param Base $event * @param Base $event
* @return void * @return void
*/ */
public function handle(Base $event) public function handle(Base $event): void
{ {
if (! $event instanceof ChannelLeft AND ! $event instanceof GroupLeft) if (! $event instanceof ChannelLeft AND ! $event instanceof GroupLeft)
abort(500,'Wrong class calling this listener? '.get_class($event)); abort(500,'Wrong class calling this listener? '.get_class($event));

View File

@ -11,7 +11,7 @@ class InteractiveMessageListener implements ShouldQueue
{ {
protected const LOGKEY = 'LIM'; protected const LOGKEY = 'LIM';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,14 +19,10 @@ class InteractiveMessageListener implements ShouldQueue
* @param InteractiveMessage $event * @param InteractiveMessage $event
* @return void * @return void
*/ */
public function handle(InteractiveMessage $event) public function handle(InteractiveMessage $event): void
{ {
// Do some magic with event data // 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__]); Log::info(sprintf('%s:Interactive Message Callback [%s] User [%s] in [%s]',self::LOGKEY,$event->callback_id,$event->user_id,$event->team_id),['m'=>__METHOD__]);
Log::notice(sprintf('%s:Ignoring Interactive Message [%s]',static::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
switch ($event->callback_id) {
default:
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',self::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
} }
} }

View File

@ -11,7 +11,7 @@ class MessageListener implements ShouldQueue
{ {
protected const LOGKEY = 'LM-'; protected const LOGKEY = 'LM-';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,7 +19,7 @@ class MessageListener implements ShouldQueue
* @param Message $event * @param Message $event
* @return void * @return void
*/ */
public function handle(Message $event) public function handle(Message $event): void
{ {
// Do some magic with event data // Do some magic with event data
Log::info(sprintf('%s:Message event [%s] - subtype [%s]',self::LOGKEY,$event->ts,$event->type),['m'=>__METHOD__]); Log::info(sprintf('%s:Message event [%s] - subtype [%s]',self::LOGKEY,$event->ts,$event->type),['m'=>__METHOD__]);

View File

@ -11,7 +11,7 @@ class PinAddedListener implements ShouldQueue
{ {
protected const LOGKEY = 'LPA'; protected const LOGKEY = 'LPA';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,7 +19,7 @@ class PinAddedListener implements ShouldQueue
* @param PinAdded $event * @param PinAdded $event
* @return void * @return void
*/ */
public function handle(PinAdded $event) public function handle(PinAdded $event): void
{ {
// Do some magic with event data // 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::info(sprintf('%s:Pin Added to message [%s] in [%s]',self::LOGKEY,$event->ts,$event->channel_id),['m'=>__METHOD__]);

View File

@ -11,7 +11,7 @@ class PinRemovedListener implements ShouldQueue
{ {
protected const LOGKEY = 'LPR'; protected const LOGKEY = 'LPR';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,7 +19,7 @@ class PinRemovedListener implements ShouldQueue
* @param PinRemoved $event * @param PinRemoved $event
* @return void * @return void
*/ */
public function handle(PinRemoved $event) public function handle(PinRemoved $event): void
{ {
// Do some magic with event data // 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::info(sprintf('%s:Pin Removed from message [%s] in [%s]',self::LOGKEY,$event->ts,$event->channel_id),['m'=>__METHOD__]);

View File

@ -11,7 +11,7 @@ class ReactionAddedListener implements ShouldQueue
{ {
protected const LOGKEY = 'LRA'; protected const LOGKEY = 'LRA';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,11 +19,10 @@ class ReactionAddedListener implements ShouldQueue
* @param ReactionAdded $event * @param ReactionAdded $event
* @return void * @return void
*/ */
public function handle(ReactionAdded $event) public function handle(ReactionAdded $event): void
{ {
// Do some magic with event data // 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::info(sprintf('%s:Reaction [%s] added to message in [%s]',self::LOGKEY,$event->reaction,$event->team_id),['m'=>__METHOD__]);
Log::notice(sprintf('%s:Ignoring Reaction Add [%s] on [%s]',static::LOGKEY,$event->reaction,$event->ts),['m'=>__METHOD__]);
Log::debug(sprintf('%s:Ignoring Reaction Add [%s] on [%s]',static::LOGKEY,$event->reaction,$event->ts),['m'=>__METHOD__]);
} }
} }

View File

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

@ -11,7 +11,7 @@ class ViewClosedListener implements ShouldQueue
{ {
protected const LOGKEY = 'LVC'; protected const LOGKEY = 'LVC';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,14 +19,10 @@ class ViewClosedListener implements ShouldQueue
* @param ViewClosed $event * @param ViewClosed $event
* @return void * @return void
*/ */
public function handle(ViewClosed $event) public function handle(ViewClosed $event): void
{ {
// Do some magic with event data // 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__]); Log::info(sprintf('%s:View Closed Callback [%s] User [%s] in [%s]',self::LOGKEY,$event->callback_id,$event->user_id,$event->team_id),['m'=>__METHOD__]);
Log::notice(sprintf('%s:Ignoring View Closed [%s]',static::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
switch ($event->callback_id) {
default:
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',self::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
} }
} }

View File

@ -11,7 +11,7 @@ class ViewSubmissionListener implements ShouldQueue
{ {
protected const LOGKEY = 'LVC'; protected const LOGKEY = 'LVC';
public $queue = 'high'; public $queue = 'slack';
/** /**
* Handle the event. * Handle the event.
@ -19,14 +19,10 @@ class ViewSubmissionListener implements ShouldQueue
* @param ViewSubmission $event * @param ViewSubmission $event
* @return void * @return void
*/ */
public function handle(ViewSubmission $event) public function handle(ViewSubmission $event): void
{ {
// Do some magic with event data // 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__]); Log::info(sprintf('%s:View Submission Callback [%s] User [%s] in [%s]',self::LOGKEY,$event->callback_id,$event->user_id,$event->team_id),['m'=>__METHOD__]);
Log::notice(sprintf('%s:Ignoring View Closed [%s]',static::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
switch ($event->callback_id) {
default:
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',self::LOGKEY,$event->callback_id),['m'=>__METHOD__]);
}
} }
} }

View File

@ -5,34 +5,42 @@ namespace Slack;
use Carbon\Carbon; use Carbon\Carbon;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Blockkit\Blocks; use Slack\Blockkit\Blocks;
use Slack\Exceptions\SlackException; use Slack\Blockkit\Blocks\{Context,Divider,Section};
use Slack\Jobs\DeleteChat; use Slack\Blockkit\Blocks\Elements\Text;
use Slack\Message\Attachments; use Slack\Exceptions\{SlackException,SlackSyntaxException};
use Slack\Jobs\{DeleteChat,DeleteResponse};
use Slack\Message\Attachment;
use Slack\Models\{Channel,User}; use Slack\Models\{Channel,User};
use Slack\Response\Generic; use Slack\Response\Generic;
/** /**
* This class is used when composing a message to send to Slack. * This class is used when composing a message to send to Slack.
*/ */
class Message implements \JsonSerializable final class Message extends BlockKit
{ {
protected const LOGKEY = 'SM-'; private const LOGKEY = 'SM-';
public const MAX_ATTACHMENTS = 20;
public const MAX_BLOCKS = 50;
private Model $o; private Model $o;
private Blocks $blocks;
private ?Carbon $selfdestruct = NULL; private ?Carbon $selfdestruct = NULL;
/** /**
* Message constructor. * Message constructor.
* *
* @param Model|null $o Who the message will be to - Channel or User * @param Model|null $o Who the message will be to - Channel or User
* @throws SlackException * @throws \Exception
*/ */
public function __construct(Model $o=NULL) public function __construct(Model $o=NULL)
{ {
$this->_data = collect(); parent::__construct();
if ($o) { if ($o) {
// Message is to a channel // Message is to a channel
@ -44,27 +52,11 @@ class Message implements \JsonSerializable
$this->setUser($o); $this->setUser($o);
} else { } else {
throw new SlackException('Model not handled: '.get_class($o)); throw new \Exception('Model not handled: '.get_class($o));
} }
$this->o = $o; $this->o = $o;
} }
$this->blocks = new Blocks;
}
/**
* Add a block to the message
*
* @param Blocks $blocks
* @return Message
* @todo to test
*/
public function addBlock(Blocks $blocks): self
{
$this->blocks = $blocks;
return $this;
} }
/** /**
@ -72,23 +64,73 @@ class Message implements \JsonSerializable
* *
* @return Message * @return Message
*/ */
public function blank(): self public static function blank(): self
{ {
$this->_data = collect(); return new self;
$this->blocks = new Blocks;
return $this;
} }
/* /* HELPER METHODS */
* @todo This doesnt appear to work
public function ephemeral(): self
{
$this->_data->put('ephemeral',TRUE);
return $this; /**
} * Add a block to the message
*
* @param Attachment $attachment
* @return Message
* @throws SlackSyntaxException
*/ */
public function addAttachment(Attachment $attachment): self
{
if (! Arr::get($this->_data,'attachments'))
$this->attachments = collect();
$this->attachments->push($attachment);
if (count($this->attachments) > self::MAX_ATTACHMENTS)
throw new SlackSyntaxException(sprintf('Messages should not have more than %d attachments',self::MAX_ATTACHMENTS));
return $this;
}
public function addBlock(Blocks $block): self
{
if (! Arr::get($this->_data,'blocks'))
$this->blocks = collect();
$this->blocks->push($block);
if (count($this->blocks) > self::MAX_BLOCKS)
throw new SlackSyntaxException(sprintf('Messages should not have more than %d blocks',self::MAX_BLOCKS));
return $this;
}
public function clearAttachments(): self
{
$this->attachments = '';
return $this;
}
/**
* For messages that we interact with via a response_url
*
* @param bool $bool
* @return Message
*/
public function delete_original(bool $bool=TRUE): self
{
$this->delete_original = $bool;
return $this;
}
// This is a helper method
public function divider(): self
{
$this->blocks->push(Divider::item());
return $this;
}
public function forgetTS(): self public function forgetTS(): self
{ {
@ -104,7 +146,7 @@ class Message implements \JsonSerializable
*/ */
public function isEmpty(): bool public function isEmpty(): bool
{ {
return $this->jsonSerialize() ? FALSE : TRUE; return ! $this->jsonSerialize();
} }
/** /**
@ -112,11 +154,8 @@ class Message implements \JsonSerializable
*/ */
public function jsonSerialize() public function jsonSerialize()
{ {
if ($this->blocks->count())
$this->_data->put('blocks',$this->blocks);
// For interactive messages that generate a dialog, we need to return NULL // For interactive messages that generate a dialog, we need to return NULL
return $this->_data->count() ? $this->_data : NULL; return count($this) ? parent::jsonSerialize() : NULL;
} }
/** /**
@ -131,68 +170,56 @@ class Message implements \JsonSerializable
if (! $delete && $this->selfdestruct) if (! $delete && $this->selfdestruct)
$delete = $this->selfdestruct; $delete = $this->selfdestruct;
if ($this->_data->has('ephemeral')) if ((! isset($this->o)) || ((! $this->o->team) && (! $this->o->user_team)))
abort('500','Cannot post ephemeral messages.'); throw new SlackSyntaxException('Message needs to have a user or a channel to work out the team.');
if ($this->blocks->count() && $this->_data->get('attachments')) $api = $this->o->team ? $this->o->team->slackAPI() : $this->o->user_team->slackAPI();
throw new SlackException('Message cannot have blocks and attachments.');
$api = $this->o->team->slackAPI(); if ($this->ephemeral) {
$response = $api->postEphemeral($this);
} else {
$response = $this->_data->has('ts') ? $api->updateMessage($this) : $api->postMessage($this); $response = $this->_data->has('ts') ? $api->updateMessage($this) : $api->postMessage($this);
}
if ($delete) { if ($delete) {
Log::debug(sprintf('%s:Scheduling Delete of [%s:%s] on [%s]',static::LOGKEY,object_get($this->o,'channel_id',$this->o->id),$response->ts,$delete->format('Y-m-d')),['m'=>__METHOD__]); Log::debug(sprintf('%s:Scheduling Delete of [%s:%s] on [%s]',static::LOGKEY,object_get($this->o,'channel_id',$this->o->id),$response->ts,$delete->format('Y-m-d H:i')),['m'=>__METHOD__,'r'=>$response,'message_ts'=>$response->ts]);
if ($this->o instanceof User) {
Log::error(sprintf('%s:Cannot schedule delete of [%s:%s] on user channels [%s]',static::LOGKEY,object_get($this->o,'channel_id',$this->o->id),$response->ts,$this->o->user_id),['m'=>__METHOD__]);
} elseif ($this->ephemeral) {
Log::error(sprintf('%s:Ignoring delete of [%s:%s] on ephemeral messages',static::LOGKEY,object_get($this->o,'channel_id',$this->o->id),$response->ts),['m'=>__METHOD__]);
} else {
// Queue the delete of the message if requested // Queue the delete of the message if requested
dispatch((new DeleteChat($this->o,$response->ts))->onQueue('low')->delay($delete)); dispatch((new DeleteChat($this->o,$response->ts))->onQueue('slack')->delay($delete));
}
} }
return $response; return $response;
} }
public function setReplace(bool $replace=TRUE): self
{
$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 * 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 * @param string $url
* @return string * @param Carbon|null $delete
* @return object
* @throws SlackException * @throws SlackException
*/ */
public function respond(string $url) public function respond(string $url,Carbon $delete=NULL): object
{ {
$request = curl_init(); if (! $delete && $this->selfdestruct)
$delete = $this->selfdestruct;
curl_setopt($request,CURLOPT_URL,$url); $http = Http::acceptJson();
curl_setopt($request,CURLOPT_RETURNTRANSFER,TRUE); $http->withBody(json_encode($this),'application/json');
curl_setopt($request,CURLINFO_HEADER_OUT,TRUE);
curl_setopt($request,CURLOPT_HTTPHEADER,['Content-Type: application/json; charset=utf-8']);
curl_setopt($request,CURLOPT_SSL_VERIFYPEER,FALSE);
curl_setopt($request,CURLOPT_POSTFIELDS,json_encode($this));
try { try {
$result = curl_exec($request); $request = $http->post($url)->throw();
if (! $result) $response = $request->object();
throw new \Exception('CURL exec returned an empty response: '.serialize(curl_getinfo($request)));
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error(sprintf('%s:Got an error while posting to [%s] (%s)',static::LOGKEY,$url,$e->getMessage()),['m'=>__METHOD__]); Log::error(sprintf('%s:Got an error while posting to [%s] (%s)',static::LOGKEY,$url,$e->getMessage()),['m'=>__METHOD__]);
@ -200,16 +227,35 @@ class Message implements \JsonSerializable
throw new \Exception($e->getMessage()); throw new \Exception($e->getMessage());
} }
if ($result !== 'ok') { if (! $response->ok) {
switch ($result) { Log::critical(sprintf('%s:Generic Error',static::LOGKEY),['m'=>__METHOD__,'r'=>$response]);
default: throw new SlackException(serialize($response),$request->status());
Log::critical(sprintf('%s:Generic Error',static::LOGKEY),['m'=>__METHOD__,'r'=>$result]);
throw new SlackException($result,curl_getinfo($request,CURLINFO_HTTP_CODE));
}
} }
curl_close($request); if ($delete) {
return $result; Log::debug(sprintf('%s:Scheduling Delete of [%s:%s] on [%s]',static::LOGKEY,object_get($this->o,'channel_id',$this->o->id),$url,$delete->format('Y-m-d')),['m'=>__METHOD__]);
// Queue the delete of the response if requested
dispatch((new DeleteResponse($url))->onQueue('slack')->delay($delete));
}
return $response;
}
/**
* Schedule a message
*
* @param Carbon $time
* @return Generic
*/
public function schedule(Carbon $time): Generic
{
$this->_data->put('post_at',$time->timestamp);
$api = $this->o->team->slackAPI();
$response = $this->_data->has('ts') ? $api->updateMessage($this) : $api->scheduleMessage($this);
return $response;
} }
/** /**
@ -217,43 +263,17 @@ class Message implements \JsonSerializable
* *
* @param Carbon $time * @param Carbon $time
* @return Message * @return Message
* @throws SlackSyntaxException
*/ */
public function selfdestruct(Carbon $time): self public function selfdestruct(Carbon $time): self
{ {
$this->blocks->addContextElements(collect([ $this->addBlock(
Blocks\Text::item(sprintf('This message will self destruct in %s...',$time->diffForHumans(Carbon::now(),['syntax' => CarbonInterface::DIFF_RELATIVE_TO_NOW]))) Context::item(collect([
])); Text::item(sprintf('This message will self destruct in %s...',$time->diffForHumans(Carbon::now(),['syntax' => CarbonInterface::DIFF_RELATIVE_TO_NOW])))
]))
);
$this->selfdestruct = $time; $this->selfdestruct = $time;
return $this;
}
/**
* Add an attachment to a message
*
* @param Attachments $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; return $this;
} }
@ -266,77 +286,12 @@ class Message implements \JsonSerializable
*/ */
public function setChannel(Channel $o): self public function setChannel(Channel $o): self
{ {
$this->_data->put('channel',$o->channel_id); $this->channel = $o->channel_id;
$this->o = $o; $this->o = $o;
return $this; return $this;
} }
/**
* Set the icon next to the message
*
* @param string $icon
* @return Message
* @deprecated
*/
public function setIcon(string $icon): self
{
$this->_data->put('icon_emoji',$icon);
return $this;
}
/**
* Option groups are used by the interactive Options controller and hold no other attributes
*
* @param array $array
* @return void
*/
public function setOptionGroup(array $array): void
{
$this->_data = collect(); // @todo Why are clearing our data?
$this->_data->put('option_groups',$array);
}
/**
* Message text
*
* @param string $string
* @return Message
*/
public function setText(string $string): self
{
$this->_data->put('text',$string);
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);
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);
return $this;
}
/** /**
* Set our channel * Set our channel
* *
@ -345,15 +300,146 @@ class Message implements \JsonSerializable
*/ */
public function setUser(User $o): self public function setUser(User $o): self
{ {
$this->_data->put('channel',$o->user_id); $this->channel = $o->user_id;
$this->o = $o; $this->o = $o;
return $this; return $this;
} }
public function setUserName(string $user): self // This is a helper method
public function spacer(): self
{ {
$this->_data->put('username',$user); $this->blocks->push(Section::item(Text::item(' ')));
return $this;
}
/* CONFIGURATION METHODS */
/**
* @return Message
*/
public function ephemeral(): self
{
$this->ephemeral = TRUE;
return $this;
}
/**
* Set the icon next to the message
*
* @param string $icon
* @return Message
* @deprecated
*/
public function icon_emoji(string $icon): self
{
$this->icon_emoji = $icon;
return $this;
}
/**
* Option groups are used by the interactive Options controller and hold no other attributes
*
* @param Collection $collection
* @return Message
*/
public function option_groups(Collection $collection): self
{
$this->option_groups = $collection;
return $this;
}
/**
* Replace the original message
*
* @param bool $bool
* @return Message
*/
public function replace_original(bool $bool=TRUE): self
{
$this->replace_original = $bool;
return $this;
}
/**
* Message text
*
* @param string $string
* @return Message
*/
public function text(string $string): self
{
$this->text = $string;
return $this;
}
/**
* Set the thread timestamp, used when adding a threaded response
*
* @param string $string
* @return Message
*/
public function thread_ts(string $string): self
{
$this->thread_ts = $string;
return $this;
}
/**
* Set the timestamp, used when replacing messages
*
* @param string $string
* @return Message
*/
public function ts(string $string): self
{
$this->ts = $string;
return $this;
}
/**
* To slack from rendering URLs in the message
*
* @param bool $bool
* @return $this
*/
public function unfurl_links(bool $bool): self
{
$this->unfurl_links = $bool;
return $this;
}
/**
* Post the message to user
*
* @param string $user
* @return $this
*/
public function user(string $user): self
{
$this->user = $user;
return $this;
}
/**
* Set bot username (used with as_user)
*
* @param string $user
* @return $this
*/
public function username(string $user): self
{
$this->username = $user;
return $this; return $this;
} }

149
src/Message/Attachment.php Normal file
View File

@ -0,0 +1,149 @@
<?php
namespace Slack\Message;
use Illuminate\Support\Arr;
use Slack\BlockKit;
use Slack\Blockkit\Blocks;
final class Attachment extends BlockKit
{
public const LIMITS = [
'footer' => 300,
];
public static function item(): self
{
return new self();
}
/* OPTIONAL ITEMS */
/**
* @note These are now legacy - use blocks instead
* @note more information on this available https://api.slack.com/legacy/interactive-message-field-guide
*/
public function addAction(AttachmentAction $action): self
{
if (! Arr::get($this->_data,'actions'))
$this->actions = collect();
$this->actions->push($action);
return $this;
}
public function addBlock(Blocks $block): self
{
if (! Arr::get($this->_data,'blocks'))
$this->blocks = collect();
$this->blocks->push($block);
return $this;
}
public function addField(string $title,string $value,bool $short): self
{
if (! Arr::get($this->_data,'fields'))
$this->fields = collect();
$this->fields->push(['title'=>$title,'value'=>$value,'short'=>$short]);
return $this;
}
/**
* @param string $string
* @return $this
* @note Can either be one of good (green), warning (yellow), danger (red), or any hex color code (eg. #439FE0)
*/
public function color(string $string): self
{
$this->color = $string;
return $this;
}
/**
* @param string $string
* @return $this
* @note Appears to be only used with actions
*/
public function callback_id(string $string): self
{
$this->callback_id = $string;
return $this;
}
public function fallback(string $string): self
{
$this->fallback = $string;
return $this;
}
public function footer(string $string): self
{
$this->footer = $this->validate('footer',$string);
return $this;
}
public function footer_icon(string $string): self
{
$this->footer_icon = $string;
return $this;
}
/**
* Set where markdown should be parsed by slack
*
* @param array $array
*/
public function mrkdwn_in(array $array): self
{
// @todo Add array check to make sure it has valid items
$this->mrkdwn_in = $array;
return $this;
}
public function pretext(string $string): self
{
$this->pretext = $string;
return $this;
}
public function text(string $string): self
{
$this->text = $string;
return $this;
}
public function title(string $string): self
{
$this->title = $string;
return $this;
}
public function title_link(string $string): self
{
$this->title_link = $string;
return $this;
}
public function ts(string $string): self
{
$this->ts = $string;
return $this;
}
}

View File

@ -2,32 +2,60 @@
namespace Slack\Message; namespace Slack\Message;
use Slack\BlockKit;
use Slack\Blockkit\Blocks\Elements\Text;
use Slack\Exceptions\SlackSyntaxException;
/** /**
* Class MessageAttachmentAction - Slack Message Attachments Actions * Class MessageAttachmentAction - Slack Message Attachments Actions
* Represents an Single Action for a Slack Message Attachment * Represents a Single Action for a Slack Message Attachment
* *
* @package Slack\Message * @package App\Slack\Message
* @note These are now legacy - use blocks instead
* @note more information on this available https://api.slack.com/legacy/interactive-message-field-guide
*/ */
class AttachmentAction implements \JsonSerializable final class AttachmentAction extends BlockKit
{ {
private $_data; public const LIMITS = [
'text' => 30,
'value' => 2000,
];
public function __construct() public const DATA_SOURCES = ['static','users','channels','conversastions','external'];
public const STYLES = ['default','danger','primary'];
public const TYPES = ['button','select'];
// @todo options
// @todo option_groups
// @todo selected_options
public function __construct(string $text,string $name,string $type)
{ {
$this->_data = collect(); parent::__construct();
if (! in_array($type,self::TYPES))
throw new SlackSyntaxException(sprintf('Type [%s] not valid',$type));
$this->type = $type;
switch ($type) {
case 'button':
$this->action_id = $name;
$this->text = Text::item($this->validate('text',$text),'plain_text');
break;
case 'select':
$this->name = $name;
$this->text = $this->validate('text',$text);
break;
}
} }
public function jsonSerialize() public static function item(string $text,string $name,string $type): self
{ {
return $this->_data; return new self($text,$name,$type);
} }
public function minSize(int $int): self /* OPTIONAL ITEMS */
{
$this->_data->put('min_query_length',$int);
return $this;
}
/** /**
* Set a confirmation diaglog when this action is selected * Set a confirmation diaglog when this action is selected
@ -38,43 +66,31 @@ class AttachmentAction implements \JsonSerializable
* @param string $dismiss_text * @param string $dismiss_text
* @return $this * @return $this
*/ */
public function setConfirm(string $title,string $text,string $ok_text,string $dismiss_text): self public function confirm(string $title,string $text,string $ok_text,string $dismiss_text): self
{ {
$this->_data->put('confirm',[ $this->confirm = [
'title' => $title, 'title' => $title,
'text' => $text, 'text' => $text,
'ok_text' => $ok_text, 'ok_text' => $ok_text,
'dismiss_text' => $dismiss_text 'dismiss_text' => $dismiss_text
]); ];
return $this; return $this;
} }
/** public function data_source(string $string): self
* Set the name of the action
*
* @param string $string
* @return $this
*/
public function setName(string $string): self
{ {
$this->_data->put('name',$string); if (! in_array($string,self::DATA_SOURCES))
throw new SlackSyntaxException(sprintf('Type [%s] not valid',$string));
$this->data_source = $string;
return $this; return $this;
} }
/** public function min_query_length(int $int): self
* Set the text displayed in the action
*
* @param string $type
* @return $this
*/
public function setStyle(string $style): self
{ {
if (! in_array($style,['danger','primary'])) $this->min_query_length = $int;
abort(500,'Style not supported: '.$style);
$this->_data->put('style',$style);
return $this; return $this;
} }
@ -84,26 +100,14 @@ class AttachmentAction implements \JsonSerializable
* *
* @param string $string * @param string $string
* @return $this * @return $this
* @throws SlackSyntaxException
*/ */
public function setText(string $string): self public function style(string $string): self
{ {
$this->_data->put('text',$string); if (! in_array($string,self::STYLES))
throw new SlackSyntaxException(sprintf('Type [%s] not valid',$string));
return $this; $this->style = $string;
}
/**
* Set the text displayed in the action
*
* @param string $type
* @return $this
*/
public function setType(string $type): self
{
if (! in_array($type,['button','select']))
abort(500,'Type not supported: '.$type);
$this->_data->put('type',$type);
return $this; return $this;
} }
@ -113,20 +117,11 @@ class AttachmentAction implements \JsonSerializable
* *
* @param string $string * @param string $string
* @return $this * @return $this
* @throws SlackSyntaxException
*/ */
public function setValue(string $string): self public function value(string $string): self
{ {
$this->_data->put('value',$string); $this->value = $this->validate('value',$string);
return $this;
}
public function source(string $string): self
{
if (! in_array($string,['external']))
abort(500,'Dont know how to handle: '.$string);
$this->_data->put('data_source',$string);
return $this; return $this;
} }

View File

@ -1,227 +0,0 @@
<?php
namespace Slack\Message;
use Slack\BlockKit;
use Slack\Blockkit\BlockAction;
use Slack\Blockkit\Blocks;
/**
* Class MessageAttachment - Slack Message Attachments
* Represents an Single Attachment that can be added to a Message
*
* @package Slack\Message
*/
class Attachments implements \JsonSerializable
{
private $_data;
private $actions;
//private $blocks;
private $blockactions;
// @todo To rework
public function __construct()
{
$this->actions = 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');
if ($this->blockactions->count()) {
$x = collect();
$x->put('type','actions');
$x->put('elements',$this->blockactions);
$this->blocks->push($x);
// Empty out our blockactions, incase we are converted to json a second time.
$this->blockactions = collect();
}
if ($this->actions->count())
$this->_data->put('actions',$this->actions);
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
{
$this->actions->push($action);
return $this;
}
/**
* Add a block to message
*
* @param BlockKit $block
* @return Attachment
* @deprecated
*/
public function addBlock(BlockKit $block): self
{
$this->blocks->push($block);
return $this;
}
/**
* Add a BlockAction to a Block
*
* @param BlockAction $action
* @return $this
* @todo To rework
*/
public function addBlockAction(BlockAction $action): self
{
$this->blockactions->push($action);
return $this;
}
//* @todo To rework
public function addField(string $title,string $value,bool $short): self
{
if (! $this->_data->has('fields'))
$this->_data->put('fields',collect());
$this->_data->get('fields')->push([
'title'=>$title,
'value'=>$value,
'short'=>$short
]);
return $this;
}
/**
* Set where markdown should be parsed by slack
*
* @param array $array
* @return $this
* @todo To rework
*/
public function markdownIn(array $array): self
{
// @todo Add array check to make sure it has valid items
$this->_data->put('mrkdown_in',$array);
return $this;
}
/**
* Configure the attachment color (on the left of the attachment)
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setCallbackID(string $string): self
{
$this->_data->put('callback_id',$string);
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
{
$this->_data->put('color',$string);
return $this;
}
/**
* Set the text used in the attachments footer
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setFooter(string $string): self
{
$this->_data->put('footer',$string);
return $this;
}
/**
* Add the pre-text, displayed after the title.
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setPretext(string $string): self
{
$this->_data->put('pretext',$string);
return $this;
}
/**
* Set the text used in the attachment
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setText(string $string): self
{
$this->_data->put('text',$string);
return $this;
}
/**
* Set the Title used in the attachment
*
* @param string $string
* @return $this
* @todo To rework
*/
public function setTitle(string $string): self
{
$this->_data->put('title',$string);
return $this;
}
}

View File

@ -4,7 +4,9 @@ namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Leenooks\Traits\ScopeActive; use Slack\Traits\ScopeActive;
use App\Models\Slack\Team as AppTeam;
class Channel extends Model class Channel extends Model
{ {
@ -17,7 +19,7 @@ class Channel extends Model
public function team() public function team()
{ {
return $this->belongsTo(Team::class); return $this->belongsTo(class_exists(AppTeam::class) ? AppTeam::class : Team::class);
} }
/* ATTRIBUTES */ /* ATTRIBUTES */
@ -29,7 +31,7 @@ class Channel extends Model
*/ */
public function getIsAllowedAttribute(): bool public function getIsAllowedAttribute(): bool
{ {
return $this->active; return $this->active && $this->team->allow_guest;
} }
/** /**
@ -42,6 +44,11 @@ class Channel extends Model
return Arr::get($this->attributes,'name') ?: $this->channel_id; return Arr::get($this->attributes,'name') ?: $this->channel_id;
} }
public function getSlackUrlAttribute(): string
{
return sprintf('<#%s>',$this->channel_id);
}
/* METHODS */ /* METHODS */
/** /**

View File

@ -3,7 +3,9 @@
namespace Slack\Models; namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Leenooks\Traits\ScopeActive;
use App\Models\Slack\Team as AppTeam;
use Slack\Traits\ScopeActive;
class Enterprise extends Model class Enterprise extends Model
{ {
@ -16,6 +18,6 @@ class Enterprise extends Model
public function teams() public function teams()
{ {
return $this->hasMany(Team::class); return $this->hasMany(class_exists(AppTeam::class) ? AppTeam::class : Team::class);
} }
} }

View File

@ -3,48 +3,52 @@
namespace Slack\Models; namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Leenooks\Traits\ScopeActive;
use App\Models\Slack\Channel as AppChannel;
use App\Models\Slack\Token as AppToken;
use App\Models\Slack\User as AppUser;
use Slack\API; use Slack\API;
use Slack\Traits\ScopeActive;
class Team extends Model class Team extends Model
{ {
use ScopeActive; use ScopeActive;
protected $fillable = ['team_id']; protected $fillable = ['team_id','active'];
protected $table = 'slack_teams'; protected $table = 'slack_teams';
/* RELATIONS */ /* RELATIONS */
public function admins() public function admins()
{ {
return $this->hasMany(User::class,'team_id','id')->where('admin','=',TRUE); return $this->hasMany(class_exists(AppUser::class) ? AppUser::class : User::class,'team_id','id')->where('admin','=',TRUE);
} }
public function bot() public function bot()
{ {
return $this->hasOne(User::class,'id','bot_id'); return $this->hasOne(class_exists(AppUser::class) ? AppUser::class : User::class,'id','bot_id');
} }
public function channels() public function channels()
{ {
return $this->hasMany(Channel::class); return $this->hasMany(class_exists(AppChannel::class) ? AppChannel::class : Channel::class);
} }
public function owner() public function owner()
{ {
return $this->belongsTo(User::class,'admin_id'); return $this->belongsTo(class_exists(AppUser::class) ? AppUser::class : User::class,'admin_id');
} }
// Tokens applicable to this team // Tokens applicable to this team
// @todo team_id can now be null, so we need to get it from the enterprise_id. // @todo team_id can now be null, so we need to get it from the enterprise_id.
public function token() public function token()
{ {
return $this->hasOne(Token::class); return $this->hasOne(class_exists(AppToken::class) ? AppToken::class : Token::class);
} }
public function users() public function users()
{ {
return $this->hasMany(User::class); return $this->hasMany(class_exists(AppUser::class) ? AppUser::class : User::class);
} }
/* ATTRIBUTES */ /* ATTRIBUTES */
@ -57,9 +61,9 @@ class Team extends Model
*/ */
public function getAppTokenObfuscateAttribute(): string public function getAppTokenObfuscateAttribute(): string
{ {
$attrs = explode('-',$this->getAppTokenAttribute()->token); $attrs = explode('-',$this->token->token);
$items = count($attrs)-1; $items = count($attrs)-1;
$attrs[$items] = '...'.substr($attrs[$items],-5); $attrs[$items] = '...'.substr($attrs[$items],-3);
return implode('-',$attrs); return implode('-',$attrs);
} }

View File

@ -4,7 +4,9 @@ namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Leenooks\Traits\ScopeActive;
use App\Models\Slack\Team as AppTeam;
use Slack\Traits\ScopeActive;
class Token extends Model class Token extends Model
{ {
@ -16,7 +18,7 @@ class Token extends Model
public function team() public function team()
{ {
return $this->belongsTo(Team::class); return $this->belongsTo(class_exists(AppTeam::class) ? AppTeam::class : Team::class);
} }
/* ATTRIBUTES */ /* ATTRIBUTES */

View File

@ -4,13 +4,17 @@ namespace Slack\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Leenooks\Traits\ScopeActive;
use App\Models\Slack\Channel;
use App\Models\Slack\Enterprise as AppEnterprise;
use App\Models\Slack\Team as AppTeam;
use Slack\Traits\ScopeActive;
class User extends Model class User extends Model
{ {
use ScopeActive; use ScopeActive;
private const LOGKEY = '-MU'; protected const LOGKEY = '-MU';
protected $fillable = ['user_id']; protected $fillable = ['user_id'];
protected $table = 'slack_users'; protected $table = 'slack_users';
@ -19,11 +23,26 @@ class User extends Model
public function enterprise() public function enterprise()
{ {
return $this->belongsTo(Enterprise::class); return $this->belongsTo(class_exists(AppEnterprise::class) ? AppEnterprise::class : Enterprise::class);
}
public function team()
{
return $this->belongsTo(class_exists(AppTeam::class) ? AppTeam::class : Team::class);
} }
/* ATTRIBUTES */ /* ATTRIBUTES */
/**
* Is this user an admin
*
* @return bool
*/
public function getIsAdminAttribute(): bool
{
return (bool)$this->admin;
}
/** /**
* Return the user in slack response format * Return the user in slack response format
*/ */
@ -34,16 +53,29 @@ class User extends Model
/** /**
* Return the team that this user is in - normally required to get the team token * Return the team that this user is in - normally required to get the team token
* For enterprise users, any team token will do. * For enterprise users, any team token will do, so we choose the first team of the enteprise
* *
* If the integration is not installed in any channels, team will be blank * If the integration is not installed in any channels, team will be blank
* *
* @return mixed * @return Team|null
*/ */
public function getTeamAttribute(): ?Team public function getUserTeamAttribute(): ?Team
{ {
Log::debug(sprintf('%s:User [%s]',self::LOGKEY,$this->id),['team'=>$this->team_id,'enterprise'=>$this->enterprise_id,'eo'=>$this->enterprise]); return $this->team_id ? $this->team : (($x=$this->enterprise->teams) ? $x->first() : NULL);
}
return $this->team_id ? Team::find($this->team_id) : (($x=$this->enterprise->teams) ? $x->first() : NULL); /* METHODS */
/**
* Return if the user is allowed to use this bot
* If the user is active status is NULL, but the channel or team allow guests, then the user is allowed.
* If the user is active status is FALSE, then the user is not allowed.
*
* @param Channel $co
* @return bool
*/
public function isAllowed(Channel $co): bool
{
return $this->active || (($this->active !== FALSE) && $co->allow_guest);
} }
} }

View File

@ -2,21 +2,27 @@
namespace Slack\Options; namespace Slack\Options;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Base as SlackBase; use Slack\Base as SlackBase;
use Slack\Exceptions\SlackException;
use Slack\Message;
abstract class Base extends SlackBase abstract class Base extends SlackBase
{ {
// Does the event respond with a reply to the HTTP request, or via a post with a trigger private const LOGKEY = 'SOb';
public $respondNow = TRUE;
public function __construct(Request $request) // Does the event respond with a reply to the HTTP request, or via a post with a trigger
// Child class should have a respond() function.
// (There should be a local implementation of the child class should respondNow = TRUE)
public $respondNow = FALSE;
public function __construct(array $request)
{ {
Log::info(sprintf('SOb:Slack INTERACTIVE MESSAGE Initialised [%s]',get_class($this)),['m'=>__METHOD__]); Log::info(sprintf('%s:Slack INTERACTIVE MESSAGE Initialised [%s]',self::LOGKEY,get_class($this)),['m'=>__METHOD__]);
// Our data is in a payload value // Our data is in a payload value
$this->_data = json_decode($request->input('payload')); parent::__construct($request);
} }
/** /**
@ -30,22 +36,42 @@ abstract class Base extends SlackBase
* + user_id * + user_id
* @param string $key * @param string $key
* @return mixed|object * @return mixed|object
* @throws \Exception
*/ */
public function __get(string $key) public function __get(string $key)
{ {
switch ($key) { switch ($key) {
case 'team_id':
return object_get($this->_data,'team.id');
case 'channel_id': case 'channel_id':
return object_get($this->_data,'channel.id'); return object_get($this->_data,'channel.id');
case 'enterprise_id':
return object_get($this->_data,'team.'.$key);
case 'team_id':
return object_get($this->_data,'team.id');
case 'user_id': case 'user_id':
return object_get($this->_data,'user.id'); return object_get($this->_data,'user.id');
case 'callback_key':
return $this->keyitem('id',$this->callback_id);
case 'callback_value':
return $this->keyitem('value',$this->callback_id);
case 'callback_id': case 'callback_id':
//case 'action_ts': //case 'action_ts':
//case 'message_ts': //case 'message_ts':
case 'type': case 'type':
return object_get($this->_data,$key); return object_get($this->_data,$key);
default:
throw new SlackException('Unknown key: '.$key);
} }
} }
/**
* Interactive messages can return their output in the incoming HTTP post.
* This function should be overwritten in the parent class, and finish by calling return Message::blank();
*
* @return Message
*/
abstract public function respond(): Message;
} }

View File

@ -0,0 +1,147 @@
<?php
namespace Slack\Options;
use Illuminate\Support\Facades\Log;
use Slack\Message;
/**
* Class BlockSuggestion
(
[type] => block_suggestion
[user] => stdClass Object
(
[id] => U01QF1B0Y6B
[username] => deon
[name] => deon
[team_id] => T01QF14NSQP
)
[container] => stdClass Object
(
[type] => view
[view_id] => V034715K8GP
)
[api_app_id] => A4TCZ007N
[token] => Oow8S2EFvrZoS9z8N4nwf9Jo
[action_id] => system_include_brand
[block_id] => YME
[value] => abcdde
[team] => stdClass Object
(
[id] => T01QF14NSQP
[domain] => els953a-1
[enterprise_id] => E01R1P48JCU
[enterprise_name] => Leenooks Enteprise A
)
[enterprise] => stdClass Object
(
[id] => E01R1P48JCU
[name] => Leenooks Enteprise A
)
[is_enterprise_install] =>
[view] => stdClass Object
(
[id] => V034715K8GP
[team_id] => T01QF14NSQP
[type] => modal
[blocks] => Array
(
[0] => stdClass Object
(
[type] => section
[block_id] => YME
[text] => stdClass Object
(
[type] => mrkdwn
[text] => Select Brands:
[verbatim] =>
)
[accessory] => stdClass Object
(
[type] => multi_external_select
[action_id] => system_include_brand
[initial_options] => Array
(
)
[placeholder] => stdClass Object
(
[type] => plain_text
[text] => select...
[emoji] => 1
)
)
)
)
[private_metadata] =>
[callback_id] =>
[state] => stdClass Object
(
[values] => stdClass Object
(
[YME] => stdClass Object
(
[system_include_brand] => stdClass Object
(
[type] => multi_external_select
[selected_options] => Array
(
)
)
)
)
)
[hash] => 1645768594.IO10vEq1
[title] => stdClass Object
(
[type] => plain_text
[text] => Configure brands
[emoji] => 1
)
[clear_on_close] =>
[notify_on_close] =>
[close] =>
[submit] =>
[previous_view_id] =>
[root_view_id] => V034715K8GP
[app_id] => A4TCZ007N
[external_id] =>
[app_installed_team_id] => T01QF14NSQP
[bot_id] => B01RKPRAT0Q
)
)
*/
abstract class BlockSuggestion extends Base
{
protected const LOGKEY = 'OIM';
public function __get($key)
{
switch ($key) {
case 'action_id':
case 'value':
return object_get($this->_data,$key);
case 'callback_id':
return object_get($this->_data,'view.'.$key);
default:
return parent::__get($key);
}
}
}

View File

@ -2,18 +2,20 @@
namespace Slack\Options; namespace Slack\Options;
use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Client\Payload;
class Factory { class Factory {
private const LOGKEY = 'SOf'; protected const LOGKEY = 'SOf';
/** /**
* @var array event type to event class mapping * @var array event type to event class mapping
*/ */
public const map = [ public const map = [
'block_suggestion' => BlockSuggestion::class,
'interactive_message' => InteractiveMessage::class, 'interactive_message' => InteractiveMessage::class,
]; ];
@ -21,30 +23,31 @@ class Factory {
* Returns new event instance * Returns new event instance
* *
* @param string $type * @param string $type
* @param Request $request * @param array $request
* @return Base * @return Base
* @note Options classes must be defined in the calling application and extend these self::map abstract classes
* otherwise errors will be thrown
*/ */
public static function create(string $type,Request $request) public static function create(string $type,array $request)
{ {
$class = Arr::get(self::map,$type,Unknown::class); $class = Arr::get(config('slack.options',self::map),$type,Unknown::class);
Log::debug(sprintf('%s:Working out Interactive Options Event Class for [%s] as [%s]',static::LOGKEY,$type,$class),['m'=>__METHOD__]); Log::debug(sprintf('%s:Working out Interactive Options Event Class for [%s] as [%s]',static::LOGKEY,$type,$class),['m'=>__METHOD__]);
if (App::environment() == 'dev') if (App::environment() == 'local')
file_put_contents('/tmp/option.'.$type,print_r(json_decode($request->input('payload')),TRUE)); file_put_contents('/tmp/option.'.$type,print_r($request,TRUE));
return new $class($request); 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. // During the life of the event, this method is called twice - once during Middleware processing, and finally by the Controller.
static $o = NULL; static $o = NULL;
static $or = NULL; static $or = NULL;
if (! $o OR ($or != $request)) { if (! $o OR ($or != $request)) {
$data = json_decode($request->input('payload'));
$or = $request; $or = $request;
$o = self::create($data->type,$request); $o = self::create(Arr::get($request->getData(),'payload.type'),Arr::get($request->getData(),'payload'));
} }
return $o; return $o;

View File

@ -3,6 +3,7 @@
namespace Slack\Options; namespace Slack\Options;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Message; use Slack\Message;
/** /**
@ -32,9 +33,9 @@ use Slack\Message;
* [attachment_id] => 3 * [attachment_id] => 3
* [token] => Oow8S2EFvrZoS9z8N4nwf9Jo * [token] => Oow8S2EFvrZoS9z8N4nwf9Jo
*/ */
class InteractiveMessage extends Base abstract class InteractiveMessage extends Base
{ {
private const LOGKEY = 'OIM'; protected const LOGKEY = 'OIM';
public function __get($key) public function __get($key)
{ {
@ -48,26 +49,4 @@ class InteractiveMessage extends Base
return parent::__get($key); return parent::__get($key);
} }
} }
/**
* Interactive messages can return their output in the incoming HTTP post
*
* @return Message
* @throws \Exception
*/
public function respond(): Message
{
Log::info(sprintf('%s:Interactive Option - Callback [%s] Name [%s] Value [%s]',static::LOGKEY,$this->callback_id,$this->name,$this->value),['m'=>__METHOD__]);
if (preg_match('/^(.*)\|([0-9]+)/',$this->callback_id)) {
[$action,$id] = explode('|',$this->callback_id,2);
} else {
// If we get here, its an action that we dont know about.
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',static::LOGKEY,$this->callback_id),['m'=>__METHOD__]);
}
return (new Message)->blank();
}
} }

View File

@ -2,20 +2,37 @@
namespace Slack\Options; namespace Slack\Options;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Message;
/** /**
* Catch all unknown slack event that we havent specifically programmed for. * Catch all unknown slack event that we havent specifically programmed for.
* *
* @package Slack\Options * @package Slack\Options
*/ */
class Unknown extends Base final class Unknown extends Base
{ {
public function __construct(Request $request) protected const LOGKEY = 'OU-';
public function __construct(array $request)
{ {
Log::notice(sprintf('SOU:UNKNOWN Slack Interaction Option received [%s]',get_class($this)),['m'=>__METHOD__]); Log::notice(sprintf('SOU:UNKNOWN Slack Interaction Option received [%s]',get_class($this)),['m'=>__METHOD__]);
parent::__construct($request); parent::__construct($request);
} }
/**
* Interactive messages can return their output in the incoming HTTP post
*
* @return Message
*/
public function respond(): Message
{
Log::info(sprintf('%s:Unknown Option - Callback [%s] Name [%s] Value [%s]',static::LOGKEY,$this->callback_id,$this->name,$this->value),['m'=>__METHOD__]);
Log::notice(sprintf('%s:Unhandled CALLBACK [%s]',static::LOGKEY,$this->callback_id),['m'=>__METHOD__]);
return Message::blank();
}
} }

View File

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

View File

@ -4,6 +4,7 @@ namespace Slack\Response;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Slack\Base as SlackBase; use Slack\Base as SlackBase;
/** /**
@ -11,9 +12,9 @@ use Slack\Base as SlackBase;
* *
* @note: This class is used for events not specifically created. * @note: This class is used for events not specifically created.
*/ */
class Base extends SlackBase implements \JsonSerializable abstract class Base extends SlackBase implements \JsonSerializable
{ {
protected const LOGKEY = 'RB-'; private const LOGKEY = 'RB-';
/** /**
* Default Constructor Setup * Default Constructor Setup
@ -28,7 +29,7 @@ class Base extends SlackBase implements \JsonSerializable
if (get_class($this) == Base::class) { if (get_class($this) == Base::class) {
Log::debug(sprintf('%s:Slack RESPONSE Initialised [%s]',static::LOGKEY,get_class($this)),['m'=>__METHOD__]); Log::debug(sprintf('%s:Slack RESPONSE Initialised [%s]',static::LOGKEY,get_class($this)),['m'=>__METHOD__]);
if (App::environment() == 'dev') if (App::environment() == 'local')
file_put_contents('/tmp/response',print_r($this,TRUE),FILE_APPEND); file_put_contents('/tmp/response',print_r($this,TRUE),FILE_APPEND);
} }
} }
@ -51,10 +52,12 @@ class Base extends SlackBase implements \JsonSerializable
return object_get($this->_data,$key) ?: object_get($this->_data,'channel'); return object_get($this->_data,$key) ?: object_get($this->_data,'channel');
case 'messages': // Used by getMessageHistory() case 'messages': // Used by getMessageHistory()
case 'scheduled_messages': // Used by scheduledMessagesList()
return collect(object_get($this->_data,$key)); return collect(object_get($this->_data,$key));
case 'team_id':
case 'ts': case 'ts':
return object_get($this->_data,$key) ?: object_get($this->_data,'message_ts');
case 'team_id':
case 'user_id': case 'user_id':
case 'type': // Needed by URL verification case 'type': // Needed by URL verification
return object_get($this->_data,$key); return object_get($this->_data,$key);
@ -66,6 +69,6 @@ class Base extends SlackBase implements \JsonSerializable
*/ */
public function jsonSerialize() public function jsonSerialize()
{ {
return $this->_data ? $this->_data : new \stdClass; return $this->_data ?: new \stdClass;
} }
} }

View File

@ -4,7 +4,7 @@ namespace Slack\Response;
final class ChannelList extends Base final class ChannelList extends Base
{ {
protected const LOGKEY = 'RCL'; private const LOGKEY = 'RCL';
/** /**
* Enable getting values for keys in the response * Enable getting values for keys in the response

View File

@ -8,9 +8,9 @@ use Illuminate\Support\Collection;
/** /**
* This is a Slack Response from a GetHistory API call which is a message Chat * This is a Slack Response from a GetHistory API call which is a message Chat
*/ */
class Chat extends Base final class Chat extends Base
{ {
protected const LOGKEY = 'RC-'; private const LOGKEY = 'RC-';
/** /**
* Enable getting values for keys in the response * Enable getting values for keys in the response

View File

@ -5,9 +5,9 @@ namespace Slack\Response;
/** /**
* This is a Generic Slack Response to API calls * This is a Generic Slack Response to API calls
*/ */
class Generic extends Base final class Generic extends Base
{ {
protected const LOGKEY = 'RGE'; private const LOGKEY = 'RGE';
/** /**
* Enable getting values for keys in the response * Enable getting values for keys in the response

View File

@ -9,7 +9,7 @@ namespace Slack\Response;
*/ */
final class Team extends Base final class Team extends Base
{ {
protected const LOGKEY = 'RT_'; private const LOGKEY = 'RT_';
/** /**
* Enable getting values for keys in the response * Enable getting values for keys in the response

View File

@ -5,9 +5,9 @@ namespace Slack\Response;
/** /**
* This is the Slack Response to an Auth Test API call * This is the Slack Response to an Auth Test API call
*/ */
class Test extends Base final class Test extends Base
{ {
protected const LOGKEY = 'RTE'; private const LOGKEY = 'RTE';
/** /**
* Enable getting values for keys in the response * Enable getting values for keys in the response

View File

@ -9,7 +9,7 @@ namespace Slack\Response;
*/ */
final class User extends Base final class User extends Base
{ {
protected const LOGKEY = 'RU_'; private const LOGKEY = 'RU_';
/** /**
* Enable getting values for keys in the response * Enable getting values for keys in the response

View File

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

View File

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

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
class SlackIntegration extends Migration return new class extends Migration
{ {
/** /**
* Run the migrations. * Run the migrations.
@ -29,7 +29,7 @@ class SlackIntegration extends Migration
$table->string('user_id', 45)->unique(); $table->string('user_id', 45)->unique();
$table->string('name')->nullable(); $table->string('name')->nullable();
$table->boolean('active'); $table->boolean('active');
$table->boolean('admin'); $table->boolean('admin')->default(FALSE);
$table->integer('enterprise_id')->nullable()->unsigned(); $table->integer('enterprise_id')->nullable()->unsigned();
$table->foreign('enterprise_id')->references('id')->on('slack_enterprises'); $table->foreign('enterprise_id')->references('id')->on('slack_enterprises');
@ -103,4 +103,4 @@ class SlackIntegration extends Migration
Schema::dropIfExists('slack_users'); Schema::dropIfExists('slack_users');
Schema::dropIfExists('slack_enterprises'); Schema::dropIfExists('slack_enterprises');
} }
} };

View File

@ -4,14 +4,43 @@ $routeConfig = [
'namespace' => 'Slack\Http\Controllers', 'namespace' => 'Slack\Http\Controllers',
]; ];
app('router')->group($routeConfig, function ($router) { app('router')
$router->get('slack-install-button', [ ->group($routeConfig, function ($router) {
$router->get('slack/install/button', [
'uses' => 'SlackAppController@button', 'uses' => 'SlackAppController@button',
'as' => 'slack-install-button', 'as' => 'slack-install-button',
]); ]);
$router->get('slack-install', [ $router->get('slack/install', [
'uses' => 'SlackAppController@install', 'uses' => 'SlackAppController@install',
'as' => 'slack-install', 'as' => 'slack-install',
]); ]);
$router->get('slack', [
'uses' => 'SlackAppController@home',
'as' => 'home',
]);
});
app('router')
->group(array_merge($routeConfig,['prefix'=>'api']), function ($router) {
$router->post('slack/event', [
'uses' => 'EventsController@fire',
'as' => 'event',
]);
$router->post('slack/imsg', [
'uses' => 'InteractiveMessageController@fire',
'as' => 'imsg',
]);
$router->post('slack/imsgopt', [
'uses' => 'InteractiveOptionsController@fire',
'as' => 'imsgopt',
]);
$router->post('slack/slashcmd', [
'uses' => 'SlashCommandController@fire',
'as' => 'slashcommand',
]);
}); });