microsoft-teams
microsoft-teams copied to clipboard
MS Teams Adaptive Card Format Proposal
As part of the upcoming forced transition from traditional webhooks to workflow trigger, we will also all need to transition from traditional message cards to "Adaptive Cards". Here's a proposed starting point for an adaptive card for this laravel notification channel that is optimised for consistency with the existing message format. Shared as inspiration and a prompt for discussion on a community consensus for the format going forwards...
<?php
namespace App\Services;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
class MicrosoftTeamsAdaptiveCard extends MicrosoftTeamsMessage
{
protected $payload = [];
protected $type = 'Default';
protected $webhookUrl = null;
public function __construct()
{
parent::__construct();
$this->payload = [
'$schema' => 'https://adaptivecards.io/schemas/adaptive-card.json',
'type' => 'AdaptiveCard',
'version' => '1.2',
'msteams' => [
'width' => 'Full',
],
];
}
public static function create(string $content = ''): self
{
return new self();
}
public function title(string $title, array $params = []): self
{
$this->payload['body'][] = [
'type' => 'TextBlock',
'wrap' => true,
'style' => 'heading',
'text' => $title,
'weight' => 'bolder',
'size' => 'large',
];
return $this;
}
public function type(string $type): self
{
$types = [
'info' => 'Default', // Black
'error' => 'Attention', // Red
'accent' => 'Accent', // Blue
'dark' => 'Dark', // Dark
'light' => 'Light', // Light
'alert' => 'Attention', // Red
'warning' => 'Warning', // Yellow
'success' => 'Good' // Green
];
$this->type = $types[$type];
return $this;
}
public function to(?string $webhookUrl): self
{
if (!$webhookUrl) {
throw new \Exception('Webhook url is required. Tried to send a teams notification without a webhook url.');
}
$this->webhookUrl = $webhookUrl;
return $this;
}
public function content(string $content, array $params = []): self
{
$this->payload['body'][] = [
'type' => 'TextBlock',
'text' => $this->convertHtmlToMarkdown($content),
'wrap' => true,
'size' => 'medium',
'separator' => true,
];
return $this;
}
public function button(string $text, string $url = '', array $params = []): self
{
$this->payload['actions'][] = [
'type' => 'Action.OpenUrl',
'title' => $text,
'url' => $url,
'style' => 'positive',
];
return $this;
}
public function addStartGroupToSection($sectionId = 'standard_section'): self
{
return $this;
}
public function fact(string $name, string $value, $sectionId = 'standard_section'): self
{
$factset = collect($this->payload['body'])->filter(function ($item) {
return $item['type'] === 'FactSet';
});
if($factset->isEmpty()) {
$factset = [
'type' => 'FactSet',
'facts' => [],
];
}
$factset['facts'][] = [
'title' => $name,
'value' => $value,
];
$this->payload['body'][] = $factset;
return $this;
}
public function getWebhookUrl(): string
{
info("Sending to teams: $this->webhookUrl");
return $this->webhookUrl;
}
public function toArray(): array
{
if($this->payload['body'][0] && $this->payload['body'][0]['type'] === 'TextBlock') {
$this->payload['body'][0]['color'] = $this->type;
}
return $this->payload;
}
private function convertHtmlToMarkdown(string $html): string
{
// Convert <b> or <strong> to **
$markdown = preg_replace('/<(b|strong)>(.*?)<\/\1>/i', '**$2**', $html);
// Convert <i> or <em> to *
$markdown = preg_replace('/<(i|em)>(.*?)<\/\1>/i', '*$2*', $markdown);
// Convert <a href="...">...</a> to [...](...)
$markdown = preg_replace('/<a href="(.*?)">(.*?)<\/a>/i', '[$2]($1)', $markdown);
// Convert <br> or <br/> to newline
$markdown = preg_replace('/<br\s*\/?>/i', "\n", $markdown);
// Convert paragraph tags
$markdown = preg_replace('/<p>(.*?)<\/p>/i', "$1\n", $markdown);
// Convert bullet points and ordered lists
$markdown = preg_replace('/<ul>(.*?)<\/ul>/is', '$1', $markdown);
$markdown = preg_replace('/<ol>(.*?)<\/ol>/is', '$1', $markdown);
$markdown = preg_replace('/<li>(.*?)<\/li>/i', "- $1\n", $markdown);
return $markdown;
}
}