1<?php declare(strict_types=1); 2 3/* 4 * This file is part of the Monolog package. 5 * 6 * (c) Jordi Boggiano <j.boggiano@seld.be> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Monolog\Handler; 13 14use Monolog\Formatter\FormatterInterface; 15use Monolog\Logger; 16use Monolog\Utils; 17use Monolog\Handler\Slack\SlackRecord; 18 19/** 20 * Sends notifications through Slack API 21 * 22 * @author Greg Kedzierski <greg@gregkedzierski.com> 23 * @see https://api.slack.com/ 24 * 25 * @phpstan-import-type FormattedRecord from AbstractProcessingHandler 26 */ 27class SlackHandler extends SocketHandler 28{ 29 /** 30 * Slack API token 31 * @var string 32 */ 33 private $token; 34 35 /** 36 * Instance of the SlackRecord util class preparing data for Slack API. 37 * @var SlackRecord 38 */ 39 private $slackRecord; 40 41 /** 42 * @param string $token Slack API token 43 * @param string $channel Slack channel (encoded ID or name) 44 * @param string|null $username Name of a bot 45 * @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) 46 * @param string|null $iconEmoji The emoji name to use (or null) 47 * @param bool $useShortAttachment Whether the context/extra messages added to Slack as attachments are in a short style 48 * @param bool $includeContextAndExtra Whether the attachment should include context and extra data 49 * @param string[] $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] 50 * @throws MissingExtensionException If no OpenSSL PHP extension configured 51 */ 52 public function __construct( 53 string $token, 54 string $channel, 55 ?string $username = null, 56 bool $useAttachment = true, 57 ?string $iconEmoji = null, 58 $level = Logger::CRITICAL, 59 bool $bubble = true, 60 bool $useShortAttachment = false, 61 bool $includeContextAndExtra = false, 62 array $excludeFields = array(), 63 bool $persistent = false, 64 float $timeout = 0.0, 65 float $writingTimeout = 10.0, 66 ?float $connectionTimeout = null, 67 ?int $chunkSize = null 68 ) { 69 if (!extension_loaded('openssl')) { 70 throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); 71 } 72 73 parent::__construct( 74 'ssl://slack.com:443', 75 $level, 76 $bubble, 77 $persistent, 78 $timeout, 79 $writingTimeout, 80 $connectionTimeout, 81 $chunkSize 82 ); 83 84 $this->slackRecord = new SlackRecord( 85 $channel, 86 $username, 87 $useAttachment, 88 $iconEmoji, 89 $useShortAttachment, 90 $includeContextAndExtra, 91 $excludeFields 92 ); 93 94 $this->token = $token; 95 } 96 97 public function getSlackRecord(): SlackRecord 98 { 99 return $this->slackRecord; 100 } 101 102 public function getToken(): string 103 { 104 return $this->token; 105 } 106 107 /** 108 * {@inheritDoc} 109 */ 110 protected function generateDataStream(array $record): string 111 { 112 $content = $this->buildContent($record); 113 114 return $this->buildHeader($content) . $content; 115 } 116 117 /** 118 * Builds the body of API call 119 * 120 * @phpstan-param FormattedRecord $record 121 */ 122 private function buildContent(array $record): string 123 { 124 $dataArray = $this->prepareContentData($record); 125 126 return http_build_query($dataArray); 127 } 128 129 /** 130 * @phpstan-param FormattedRecord $record 131 * @return string[] 132 */ 133 protected function prepareContentData(array $record): array 134 { 135 $dataArray = $this->slackRecord->getSlackData($record); 136 $dataArray['token'] = $this->token; 137 138 if (!empty($dataArray['attachments'])) { 139 $dataArray['attachments'] = Utils::jsonEncode($dataArray['attachments']); 140 } 141 142 return $dataArray; 143 } 144 145 /** 146 * Builds the header of the API Call 147 */ 148 private function buildHeader(string $content): string 149 { 150 $header = "POST /api/chat.postMessage HTTP/1.1\r\n"; 151 $header .= "Host: slack.com\r\n"; 152 $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; 153 $header .= "Content-Length: " . strlen($content) . "\r\n"; 154 $header .= "\r\n"; 155 156 return $header; 157 } 158 159 /** 160 * {@inheritDoc} 161 */ 162 protected function write(array $record): void 163 { 164 parent::write($record); 165 $this->finalizeWrite(); 166 } 167 168 /** 169 * Finalizes the request by reading some bytes and then closing the socket 170 * 171 * If we do not read some but close the socket too early, slack sometimes 172 * drops the request entirely. 173 */ 174 protected function finalizeWrite(): void 175 { 176 $res = $this->getResource(); 177 if (is_resource($res)) { 178 @fread($res, 2048); 179 } 180 $this->closeSocket(); 181 } 182 183 public function setFormatter(FormatterInterface $formatter): HandlerInterface 184 { 185 parent::setFormatter($formatter); 186 $this->slackRecord->setFormatter($formatter); 187 188 return $this; 189 } 190 191 public function getFormatter(): FormatterInterface 192 { 193 $formatter = parent::getFormatter(); 194 $this->slackRecord->setFormatter($formatter); 195 196 return $formatter; 197 } 198 199 /** 200 * Channel used by the bot when posting 201 */ 202 public function setChannel(string $channel): self 203 { 204 $this->slackRecord->setChannel($channel); 205 206 return $this; 207 } 208 209 /** 210 * Username used by the bot when posting 211 */ 212 public function setUsername(string $username): self 213 { 214 $this->slackRecord->setUsername($username); 215 216 return $this; 217 } 218 219 public function useAttachment(bool $useAttachment): self 220 { 221 $this->slackRecord->useAttachment($useAttachment); 222 223 return $this; 224 } 225 226 public function setIconEmoji(string $iconEmoji): self 227 { 228 $this->slackRecord->setUserIcon($iconEmoji); 229 230 return $this; 231 } 232 233 public function useShortAttachment(bool $useShortAttachment): self 234 { 235 $this->slackRecord->useShortAttachment($useShortAttachment); 236 237 return $this; 238 } 239 240 public function includeContextAndExtra(bool $includeContextAndExtra): self 241 { 242 $this->slackRecord->includeContextAndExtra($includeContextAndExtra); 243 244 return $this; 245 } 246 247 /** 248 * @param string[] $excludeFields 249 */ 250 public function excludeFields(array $excludeFields): self 251 { 252 $this->slackRecord->excludeFields($excludeFields); 253 254 return $this; 255 } 256} 257