microsoft-teams icon indicating copy to clipboard operation
microsoft-teams copied to clipboard

MS Teams Adaptive Card Format Proposal

Open peterthomson opened this issue 5 months ago • 0 comments

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;
    }
}

peterthomson avatar Sep 24 '24 06:09 peterthomson