slack/src/API.php

466 lines
14 KiB
PHP

<?php
namespace Slack;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Slack\Blockkit\Modal;
use Slack\Response\Chat;
use Slack\Exceptions\{SlackAlreadyPinnedException,
SlackChannelNotFoundException,
SlackException,
SlackHashConflictException,
SlackMessageNotFoundException,
SlackNoAuthException,
SlackNoPinException,
SlackNotFoundException,
SlackNotInChannelException,
SlackThreadNotFoundException,
SlackTokenScopeException};
use Slack\Models\{Team,Token,User};
use Slack\Response\ChannelList;
use Slack\Response\Generic;
use Slack\Response\User as ResponseUser;
use Slack\Response\Team as ResponseTeam;
use Slack\Response\Test;
final class API
{
private const LOGKEY = 'API';
private const scopes = [
'auth.test'=>'', // No scope required
'chat.delete'=>'chat:write',
'chat.postEphemeral'=>'chat:write',
'chat.postMessage'=>'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.info'=>'channels:history', // (channels:read) Also need groups:read for Private Channels
'conversations.list'=>'channels:read',
'conversations.replies'=>'channels:history', // Also need groups:history for Private Channels
'dialog.open'=>'', // No scope required
'pins.add'=>'pins:write',
'pins.remove'=>'pins:write',
'chat.scheduleMessage'=>'chat:write',
'chat.scheduledMessages.list'=>'',
'team.info'=>'team:read',
'views.open'=>'', // No scope required
'views.publish'=>'', // No scope required
'views.push'=>'', // No scope required
'views.update'=>'', // No scope required
'users.conversations'=>'channels:read',
'users.info'=>'users:read',
];
// Our slack token to use
private Token $_token;
public function __construct(Team $o)
{
$this->_token = $o->token;
Log::debug(sprintf('%s:Slack API with token [%s]',static::LOGKEY,$this->_token ? $this->_token->token_hidden : NULL),['m'=>__METHOD__]);
}
public function authTest(): Test
{
Log::debug(sprintf('%s:Auth Test',static::LOGKEY),['m'=>__METHOD__]);
return new Test($this->execute('auth.test',[]));
}
/**
* Delete a message in a channel
*
* @param string $channel
* @param string $timestamp
* @return Generic
* @throws SlackException
*/
public function deleteChat(string $channel,string $timestamp): Generic
{
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]));
}
/**
* Open a dialogue with the user
*
* @param string $trigger
* @param string $dialog
* @return Generic
* @throws SlackException
*/
public function dialogOpen(string $trigger,string $dialog): Generic
{
Log::debug(sprintf('%s:Open a Dialog',static::LOGKEY),['m'=>__METHOD__,'d'=>$dialog,'t'=>$trigger]);
return new Generic($this->execute('dialog.open',['dialog'=>$dialog,'trigger_id'=>$trigger]));
}
/**
* Get Messages on a channel from a specific timestamp
*
* @param string $channel
* @param string $timestamp
* @param int $limit
* @return Generic
* @throws SlackException
*/
public function getChannelHistory(string $channel,string $timestamp,int $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],TRUE));
}
/**
* Get information on a channel.
*
* @param string $channel
* @return Generic
* @throws SlackException
*/
public function getChannelInfo(string $channel): Generic
{
Log::debug(sprintf('%s:Channel Information [%s]',static::LOGKEY,$channel),['m'=>__METHOD__]);
return new Generic($this->execute('conversations.info',['channel'=>$channel],TRUE));
}
/**
* Get a list of channels.
*
* @param int $limit
* @return Generic
* @throws SlackException
*/
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],TRUE));
}
/**
* Get all messages from a thread
*
* @param string $channel
* @param string $thread_ts
* @return Chat
* @throws SlackException
*/
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],TRUE));
}
/**
* Get information on a user
*
* @param string $team_id
* @return ResponseTeam
* @throws SlackException
*/
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],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));
}
/**
* Get the list of channels for a user (the bot normally)
*
* @param User $uo
* @param int $limit
* @param string|null $cursor
* @return ChannelList
* @throws SlackException
*/
public function getUserChannels(User $uo,int $limit=100,string $cursor=NULL): ChannelList
{
Log::debug(sprintf('%s:Channel List for [%s] (%s:%s)',static::LOGKEY,$uo->user_id,$limit,$cursor),['m'=>__METHOD__]);
$args = collect([
'limit'=>$limit,
'exclude_archived'=>false,
'types'=>'public_channel,private_channel',
'user'=>$uo->user_id,
]);
if ($cursor)
$args->put('cursor',$cursor);
return new ChannelList($this->execute('users.conversations',$args->toArray(),TRUE));
}
/**
* 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]);
return new Generic($this->execute('views.open',['trigger_id'=>$trigger,'view'=>$view]));
}
/**
* Publish a view
*
* @param string $user
* @param Modal $view
* @param string $hash
* @return Generic
* @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.
*/
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]);
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,Modal $view): Generic
{
Log::debug(sprintf('%s:Push a view',static::LOGKEY),['m'=>__METHOD__,'t'=>$trigger]);
return new Generic($this->execute('views.push',['trigger_id'=>$trigger,'view'=>$view]));
}
public function viewUpdate(string $view_id,Modal $view): Generic
{
Log::debug(sprintf('%s:Update a view',static::LOGKEY),['m'=>__METHOD__,'id'=>$view_id]);
return new Generic($this->execute('views.update',['view_id'=>$view_id,'view'=>$view]));
}
/**
* Call the Slack API
*
* @param string $method
* @param mixed $parameters
* @param bool $asForm
* @return object
* @throws \Exception
* @throws SlackException
*/
private function execute(string $method,mixed $parameters,bool $asForm=FALSE): object
{
switch (config('app.env')) {
case 'steno': $url = 'http://steno:3000';
break;
case 'replay': $url = 'http://steno_replay:3000';
break;
default:
$url = 'https://slack.com';
}
// 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))) {
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));
}
$http = Http::baseUrl($url);
$http
->withToken($this->_token->token)
->acceptJson();
if ($asForm) {
if (! is_array($parameters))
throw new SlackException('Parameters are not an array for a form submission');
$http->asForm();
} elseif ($parameters) {
$http->withBody((is_array($parameters) || ($parameters instanceof BlockKit)) ? json_encode($parameters) : $parameters,'application/json');
}
try {
$request = $http->post(sprintf('/api/%s',$method),$asForm ? $parameters : NULL)->throw();
$response = $request->object();
} catch (\Exception $e) {
Log::error(sprintf('%s:Got an error while posting to [%s] (%s)',static::LOGKEY,$url,$e->getMessage()),['m'=>__METHOD__]);
throw new \Exception($e->getMessage());
}
if ($response->ok)
return $response;
else
switch ($response->error) {
case 'already_pinned':
throw new SlackAlreadyPinnedException('Already Pinned',$request->status());
case 'channel_not_found':
throw new SlackChannelNotFoundException('Channel Not Found',$request->status());
case 'hash_conflict':
if (App::environment() == 'local')
file_put_contents('/tmp/hash_conflict.'.$method,print_r(json_decode(json_decode($parameters)->view),TRUE));
throw new SlackHashConflictException('Hash Conflict',$request->status());
case 'invalid_auth':
throw new SlackNoAuthException('Invalid Auth Token',$request->status());
case 'message_not_found':
throw new SlackMessageNotFoundException('Message Not Found',$request->status());
case 'no_pin':
throw new SlackNoPinException('No Pin',$request->status());
case 'not_authed':
throw new SlackNoAuthException('No Auth Token',$request->status());
case 'not_found':
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',$request->status());
case 'not_in_channel':
throw new SlackNotInChannelException('Not In Channel',$request->status());
case 'thread_not_found':
throw new SlackThreadNotFoundException('Thread Not Found',$request->status());
case 'account_inactive':
// @todo Mark the token/team as inactive
default:
Log::error(sprintf('%s:Generic Error',static::LOGKEY),['m'=>__METHOD__,'t'=>$this->_token->team_id,'r'=>$response]);
throw new SlackException($response->error,$request->status());
}
}
}