1 <?php
2 namespace Slack;
3
4 use GuzzleHttp;
5 use Psr\Http\Message\ResponseInterface;
6 use React\EventLoop\LoopInterface;
7 use React\Promise\Deferred;
8 use Slack\Message\Message;
9 use Slack\Message\MessageBuilder;
10
11 /**
12 * A client for connecting to the Slack Web API and calling remote API methods.
13 */
14 class ApiClient
15 {
16 /**
17 * The base URL for API requests.
18 */
19 const BASE_URL = 'https://slack.com/api/';
20
21 /**
22 * @var string The Slack API token string.
23 */
24 protected $token;
25
26 /**
27 * @var GuzzleHttp\ClientInterface A Guzzle HTTP client.
28 */
29 protected $httpClient;
30
31 /**
32 * @var LoopInterface An event loop instance.
33 */
34 protected $loop;
35
36 /**
37 * Creates a new API client instance.
38 *
39 * @param GuzzleHttp\ClientInterface $httpClient A Guzzle client instance to
40 * send requests with.
41 */
42 public function __construct(LoopInterface $loop, GuzzleHttp\ClientInterface $httpClient = null)
43 {
44 $this->loop = $loop;
45 $this->httpClient = $httpClient ?: new GuzzleHttp\Client();
46 }
47
48 /**
49 * Sets the Slack API token to be used during method calls.
50 *
51 * @param string $token The API token string.
52 */
53 public function setToken($token)
54 {
55 $this->token = $token;
56 }
57
58 /**
59 * Gets a message builder for creating a new message object.
60 *
61 * @return \Slack\Message\MessageBuilder
62 */
63 public function getMessageBuilder()
64 {
65 return new MessageBuilder($this);
66 }
67
68 /**
69 * Gets the currently authenticated user.
70 *
71 * @return \React\Promise\PromiseInterface A promise for the currently authenticated user.
72 */
73 public function getAuthedUser()
74 {
75 return $this->apiCall('auth.test')->then(function (Payload $response) {
76 return $this->getUserById($response['user_id']);
77 });
78 }
79
80 /**
81 * Gets information about the current Slack team logged in to.
82 *
83 * @return \React\Promise\PromiseInterface A promise for the current Slack team.
84 */
85 public function getTeam()
86 {
87 return $this->apiCall('team.info')->then(function (Payload $response) {
88 return new Team($this, $response['team']);
89 });
90 }
91
92 /**
93 * Gets a channel, group, or DM channel by ID.
94 *
95 * @param string $id The channel ID.
96 *
97 * @return \React\Promise\PromiseInterface A promise for a channel interface.
98 */
99 public function getChannelGroupOrDMByID($id)
100 {
101 if ($id[0] === 'D') {
102 return $this->getDMById($id);
103 }
104
105 if ($id[0] === 'G') {
106 return $this->getGroupById($id);
107 }
108
109 return $this->getChannelById($id);
110 }
111
112 /**
113 * Gets all channels in the team.
114 *
115 * @return \React\Promise\PromiseInterface
116 */
117 public function getChannels()
118 {
119 return $this->apiCall('channels.list')->then(function ($response) {
120 $channels = [];
121 foreach ($response['channels'] as $channel) {
122 $channels[] = new Channel($this, $channel);
123 }
124 return $channels;
125 });
126 }
127
128 /**
129 * Gets a channel by its ID.
130 *
131 * @param string $id A channel ID.
132 *
133 * @return \React\Promise\PromiseInterface A promise for a channel object.
134 */
135 public function getChannelById($id)
136 {
137 return $this->apiCall('channels.info', [
138 'channel' => $id,
139 ])->then(function (Payload $response) {
140 return new Channel($this, $response['channel']);
141 });
142 }
143
144 /**
145 * Gets a channel by its name.
146 *
147 * @param string $name The name of the channel.
148 *
149 * @return \React\Promise\PromiseInterface
150 */
151 public function getChannelByName($name)
152 {
153 return $this->getChannels()->then(function (array $channels) use ($name) {
154 foreach ($channels as $channel) {
155 if ($channel->getName() === $name) {
156 return $channel;
157 }
158 }
159
160 throw new ApiException('Channel ' . $name . ' not found.');
161 });
162 }
163
164 /**
165 * Gets all groups the authenticated user is a member of.
166 *
167 * @return \React\Promise\PromiseInterface
168 */
169 public function getGroups()
170 {
171 return $this->apiCall('groups.list')->then(function ($response) {
172 $groups = [];
173 foreach ($response['groups'] as $group) {
174 $groups[] = new Group($this, $group);
175 }
176 return $groups;
177 });
178 }
179
180 /**
181 * Gets a group by its ID.
182 *
183 * @param string $id A group ID.
184 *
185 * @return \React\Promise\PromiseInterface A promise for a group object.
186 */
187 public function getGroupById($id)
188 {
189 return $this->apiCall('groups.info', [
190 'channel' => $id,
191 ])->then(function (Payload $response) {
192 return new Group($this, $response['group']);
193 });
194 }
195
196 /**
197 * Gets a group by its name.
198 *
199 * @param string $name The name of the group.
200 *
201 * @return \React\Promise\PromiseInterface
202 */
203 public function getGroupByName($name)
204 {
205 return $this->getGroups()->then(function (array $groups) use ($name) {
206 foreach ($groups as $group) {
207 if ($group->getName() === $name) {
208 return $group;
209 }
210 }
211
212 throw new ApiException('Group ' . $name . ' not found.');
213 });
214 }
215
216 /**
217 * Gets all DMs the authenticated user has.
218 *
219 * @return \React\Promise\PromiseInterface
220 */
221 public function getDMs()
222 {
223 return $this->apiCall('im.list')->then(function ($response) {
224 $dms = [];
225 foreach ($response['ims'] as $dm) {
226 $dms[] = new DirectMessageChannel($this, $dm);
227 }
228 return $dms;
229 });
230 }
231
232 /**
233 * Gets a direct message channel by its ID.
234 *
235 * @param string $id A DM channel ID.
236 *
237 * @return \React\Promise\PromiseInterface A promise for a DM object.
238 */
239 public function getDMById($id)
240 {
241 return $this->getDMs()->then(function (array $dms) use ($id) {
242 foreach ($dms as $dm) {
243 if ($dm->getId() === $id) {
244 return $dm;
245 }
246 }
247
248 throw new ApiException('DM ' . $id . ' not found.');
249 });
250 }
251
252 /**
253 * Gets a direct message channel for a given user.
254 *
255 * @param User $user The user to get a DM for.
256 *
257 * @return \React\Promise\PromiseInterface A promise for a DM object.
258 */
259 public function getDMByUser(User $user)
260 {
261 return $this->getDMByUserId($user->getId());
262 }
263
264 /**
265 * Gets a direct message channel by user's ID.
266 *
267 * @param string $id A user ID.
268 *
269 * @return \React\Promise\PromiseInterface A promise for a DM object.
270 */
271 public function getDMByUserId($id)
272 {
273 return $this->apiCall('im.open', [
274 'user' => $id,
275 ])->then(function (Payload $response) {
276 return $this->getDMById($response['channel']['id']);
277 });
278 }
279
280 /**
281 * Gets all users in the Slack team.
282 *
283 * @return \React\Promise\PromiseInterface A promise for an array of users.
284 */
285 public function getUsers()
286 {
287 // get the user list
288 return $this->apiCall('users.list')->then(function (Payload $response) {
289 $users = [];
290 foreach ($response['members'] as $member) {
291 $users[] = new User($this, $member);
292 }
293 return $users;
294 });
295 }
296
297 /**
298 * Gets a user by its ID.
299 *
300 * @param string $id A user ID.
301 *
302 * @return \React\Promise\PromiseInterface A promise for a user object.
303 */
304 public function getUserById($id)
305 {
306 return $this->apiCall('users.info', [
307 'user' => $id,
308 ])->then(function (Payload $response) {
309 return new User($this, $response['user']);
310 });
311 }
312
313 /**
314 * Gets a user by username.
315 *
316 * If the user could not be found, the returned promise is rejected with a
317 * `UserNotFoundException` exception.
318 *
319 * @return \React\Promise\PromiseInterface A promise for a user object.
320 */
321 public function getUserByName($username)
322 {
323 return $this->getUsers()->then(function (array $users) use ($username) {
324 foreach ($users as $user) {
325 if ($user->getUsername() === $username) {
326 return $user;
327 }
328 }
329
330 throw new UserNotFoundException("The user \"$username\" does not exist.");
331 });
332 }
333
334 /**
335 * Sends a regular text message to a given channel.
336 *
337 * @param string $text The message text.
338 * @param ChannelInterface $channel The channel to send the message to.
339 * @return \React\Promise\PromiseInterface
340 */
341 public function send($text, ChannelInterface $channel)
342 {
343 $message = $this->getMessageBuilder()
344 ->setText($text)
345 ->setChannel($channel)
346 ->create();
347
348 return $this->postMessage($message);
349 }
350
351 /**
352 * Posts a message.
353 *
354 * @param \Slack\Message\Message $message The message to post.
355 *
356 * @return \React\Promise\PromiseInterface
357 */
358 public function postMessage(Message $message)
359 {
360 $options = [
361 'text' => $message->getText(),
362 'channel' => $message->data['channel'],
363 'as_user' => true,
364 ];
365
366 if ($message->hasAttachments()) {
367 $options['attachments'] = json_encode($message->getAttachments());
368 }
369
370 return $this->apiCall('chat.postMessage', $options);
371 }
372
373 /**
374 * Sends an API request.
375 *
376 * @param string $method The API method to call.
377 * @param array $args An associative array of arguments to pass to the
378 * method call.
379 *
380 * @return \React\Promise\PromiseInterface A promise for an API response.
381 */
382 public function apiCall($method, array $args = [])
383 {
384 // create the request url
385 $requestUrl = self::BASE_URL . $method;
386
387 // set the api token
388 $args['token'] = $this->token;
389
390 // send a post request with all arguments
391 $promise = $this->httpClient->postAsync($requestUrl, [
392 'form_params' => $args,
393 ]);
394
395 // Add requests to the event loop to be handled at a later date.
396 $this->loop->futureTick(function () use ($promise) {
397 $promise->wait();
398 });
399
400 // When the response has arrived, parse it and resolve. Note that our
401 // promises aren't pretty; Guzzle promises are not compatible with React
402 // promises, so the only Guzzle promises ever used die in here and it is
403 // React from here on out.
404 $deferred = new Deferred();
405 $promise->then(function (ResponseInterface $response) use ($deferred) {
406 // get the response as a json object
407 $payload = Payload::fromJson((string) $response->getBody());
408
409 // check if there was an error
410 if (isset($payload['ok']) && $payload['ok'] === true) {
411 $deferred->resolve($payload);
412 } else {
413 // make a nice-looking error message and throw an exception
414 $niceMessage = ucfirst(str_replace('_', ' ', $payload['error']));
415 $deferred->reject(new ApiException($niceMessage));
416 }
417 });
418
419 return $deferred->promise();
420 }
421 }
422