Wir tauchen tiefer ein in…

MS TEAMS – Chat mit eigenen Chatvorlagen ausstatten

Die Unternehmenskommunikation via Chat wird immer populärer. Mit der höheren Anzahl an Benutzern mehren sich auch die Wünsche an die Plattform. Microsoft Teams bietet dafür Lösungsmöglichkeiten. Die folgenden Ausführungen zeigen eine nützliche Erweiterung.

Ziel ist es, Chatvorlagen für Mitarbeiter bereitzustellen, die sich im Homeoffice befinden. Das dient der Vereinfachung und Vereinheitlichung der Kommunikation. Der Mitarbeiter drückt nur noch auf den richtigen Button in den Chatvorlagen und kann die gewünschte Nachricht absenden.

Technisch basiert die Lösung auf dem Azure Bot Framework 4 und Teams Middleware.

Erstellen des Bots in Azure

Anfangs wird ein Bot über das Azure Portal erstellt.

Nach der Anlage erhält man einen Azure Bot und einen zugehörigen App Service:

Nun muss man den Quellcode herunterladen, um starten zu können:

 

Zum Zeitpunkt der Erstellung dieser Anleitung ist das Bot Framework V4 das aktuelle. Der Bot wird immer mit der letzten Release Version erstellt.

Codierung Teil I

Der Bot-Quellcode kann im Visual Studio geöffnet werden.

Die Projektstruktur im VS2019.

Die Projektstruktur im VS2019. DbContexts und Models werden erst später hinzugefügt

Die Klasse „EchoBot“ wurde von uns mit einem passendem Namen versehen, „atChatTemplateBot“. Dieser wird im Folgenden verwendet und ersetzt „EchoBot“.

 

Hinzufügen der notwendigen Teams Komponenten

Bei dem erstellten Bot handelt es sich um einen Standard Bot. Um Teams zu unterstützen, sind weitere Schritte nötig:

Hinzufügen der MS Teams Pakete via Nuget

Nuget Manager zur installation von Nuget Paketen

Registrieren des Bots in der Methode ConfigureServices in Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            // Create the Bot Framework Adapter with error handling enabled.
            services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

            // Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
            services.AddTransient<IBot, atChatTemplateBot>();


            }
Hinzufügen der TeamsMiddleware im Konstruktor zum AdapterWithErrorHandler.cs
public AdapterWithErrorHandler(IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> logger)
            : base(configuration, logger)
        {
            OnTurnError = async (turnContext, exception) =>
            {
                // Log any leaked exception from the application.
                logger.LogError($"Exception caught : {exception.Message}");

                // Send a catch-all apology to the user.
                await turnContext.SendActivityAsync("Sorry, it looks like something went wrong.");
            };

            Use(new TeamsMiddleware(new ConfigurationCredentialProvider(configuration)));

        }

Zum jetzigen Zeitpunkt ist alles so eingerichtet, dass die Requests den registrierten Bot (hier „EchoBot“) erreichen. Nun beginnt die eigentliche Arbeit am Bot.

Botlogik ausarbeiten

Hierfür wird zunächst die OnTurnAsync Methode des Bots überschrieben. Diese benutzt Bot-Framework-V4 Standards und man sollte zunächst klären, welche Arten von Aktivitäten man abfängt. In unserem Fall (Messaging Extension) erhalten wir eine Anfrage vom Typ „Invoke“ (siehe hier). Wir antworten mit einer „InvokeResponse“.

        public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
        {

            switch (turnContext.Activity.Type)
            {
                case ActivityTypes.Invoke:
                    InvokeResponse invokeResponse = this.handleActivityInvoke(turnContext, cancellationToken);
                    await turnContext.SendActivityAsync(
                                new Activity
                                {
                                    Value = invokeResponse,
                                    Type = ActivityTypesEx.InvokeResponse,
                                }).ConfigureAwait(false);
                    break;
                default:
                    break;
            }

        }

In der handleActivityInvoke Methode werden wir nun nicht länger beim reinen Bot-Framework bleiben, sondern beziehen uns auf unseren Teams Kontext. Hier haben wir nun die Möglichkeit zu erkennen, um welche Art von Teams Anfrage es sich genau handelt.

Andere mögliche Anfragen sind:

  • O365ConnectorCardActionQuery
  • SigninStateVerificationQuery
  • FileConsentResponse
  • AppBasedLinkQuery
  • MessagingExtensionFetchTask
  • MessagingExtensionSubmitAction
  • TaskModuleFetch
  • TaskModuleSubmit
public InvokeResponse handleActivityInvoke(ITurnContext turnContext, CancellationToken cancellationToken = default)
        {
            ITeamsContext teamsContext = turnContext.TurnState.Get<ITeamsContext>();
            
            if (teamsContext.IsRequestMessagingExtensionQuery())
            {
                return this.getInvokeResponse(teamsContext, cancellationToken);
            }
        }

Für unseren Zweck wird der Typ MessagingExtensionQuery benötigt. In diesem Fall müssen wir mit einer „result“ InvokeResponse antworten und da mehrere Antworten möglich sind, geben wir diese als „list“ von Attachments zurück.

Wir benutzen eine „ThumbnailCard“ für die Anzeige der zur Auswahl stehenden Chatvorlagen.

Des Weiteren benutzen wir „HeroCard“ für die Chatvorlage selbst. Eine detaillierte Beschreibung dieser Cards ist in der Dokumentation von Microsoft einzusehen:

public InvokeResponse getInvokeResponse(ITeamsContext teamsContext, CancellationToken cancellationToken = default)
        {

            var listOfResults = new List<MessagingExtensionAttachment>();

            var heroCard = new HeroCard("Homeoffice Anfang", null, "Ich bin ab sofort im dem Homeoffice erreichbar.");
            var thumbnailCard = new ThumbnailCard("Homeoffice Anfang", null, "");
            var resultListAttachment = heroCard.ToAttachment().ToMessagingExtensionAttachment(thumbnailCard.ToAttachment());
            listOfResults.Add(resultListAttachment);

            listOfResults.Add(new HeroCard("Homeoffice Anfang Pause", null, "Ich beginne nun mit meiner Pause.").ToAttachment().ToMessagingExtensionAttachment(new ThumbnailCard("Homeoffice Anfang Pause", null, "").ToAttachment()));
            listOfResults.Add(new HeroCard("Homeoffice Ende Pause", null, "Ich bin ab sofort wieder aus dem Homeoffice erreichbar.").ToAttachment().ToMessagingExtensionAttachment(new ThumbnailCard("Homeoffice Ende Pause", null, "").ToAttachment()));
            listOfResults.Add(new HeroCard("Homeoffice Ende", null, "Ich mache nun Feierabend.").ToAttachment().ToMessagingExtensionAttachment(new ThumbnailCard("Homeoffice Ende", null, "").ToAttachment()));

            return new InvokeResponse
            {
                Body = new MessagingExtensionResponse
                {
                    ComposeExtension = new MessagingExtensionResult()
                    {
                        Type = "result",
                        AttachmentLayout = "list",
                        Attachments = listOfResults,
                    },
                },
                Status = 200,
            };
        }

Im ersten Schritt werden die Chatvorlagen im Code festgelegt.

Einstellen von AppId und AppPasswort

Nun muss man noch aus dem zuvor bei Azure erstelltem Bot die nötigen Konfigurationen in die appsettings.json eintragen. Der generierte Code, den wir aus Azure heruntergeladen haben, wird diese Einträge in der appsettings.json schon haben. Dies sollte nur zur Sicherheit geprüft werden.

Damit wäre der Codeteil abgeschlossen.

Einrichten des Bots in MS Teams

Manifest Editor

Für die Einrichtung wird der Manifest Editor des MS Teams App Studios benötigt. Sollte das App Studio nicht vorliegen, kann es in Teams herunterladen werden, indem man hier auf „Weitere Apps“ klickt:

NEUES BILD 

Anschließend kann die App über die Suchmaske installiert werden:

Wenn App Studio installiert ist, kann es geöffnet werden und man kann im Manifest Editor eine neue App anlegen oder eine bestehende wählen:

In den App-Details werden grundlegende Informationen zur App hinterlegt. Hierzu gehören vor allem der Name der App, die Datenschutzerklärung und das spätere Anzeigebild. Aber auch Informationen wie Firma und Versionsnummer werden hier hinterlegt.

Im Bereich Bots muss der Bot mit der korrekten AppId hinterlegt werden (siehe Erstellung AppId und AppPasswort).
Außerdem wird hier der Messaging Endpoint angegeben. Dieser entspricht der Azure App Service Site plus der Route, die im Controler angegeben ist. Standard ist „api/message“.

Im Bereich Messaging extensions werden nun mögliche Kommandos angegeben. In unserem Fall gibt es nur ein einziges, „Nachrichten“. Sollte es mehrere geben, dienen die Angaben hier dazu im Bot in der handleActivityInvoke mit Hilfe des „ITeamsContext“ zu unterscheiden was passieren soll.

Abbildung 1: Messagin Extension Kommandos

Abbildung 1: Messagin Extension Kommandos

Über Test and Distribute kann man die App nun für ein Team installieren. Dies kann für ein Team, in dem man Mitglied ist, passieren oder für sich persönlich. Möchte man die App global oder in einem Team, in dem man Mitglied ist, installieren, so muss man das Manifest herunterladen und an denjenigen mit den passenden Rechten weiterleiten. Eine globale Installation ist über das Adminportal möglich.

Bereitstellen mit Hilfe des Veröfentlichungsprofils

Um den Bot in Azure bereitzustellen, muss aus dem App Service das Veröffentlichungsprofil heruntergeladen werden.

Dieses Profil kann in Visual Studio importiert werden, mit einem Rechtsklick auf das Projekt wird der Bot aktualisiert.

 

Debuggen des Bots

Während der Entwicklung kann man den Bot natürlich debuggen. Hierfür muss der Debugger jedoch extern erreichbar sein. Hierzu gibt es mehrere Möglichkeiten.
Beschrieben wird hier die Vorgehensweise mit „ngrok“.
Zunächst startet man den Debugger und notiert sich den Port.

Anschließend startet man ngrok mit dem Port und localhost als host-header (sonst bemängelt Teams die Weiterleitung).

Nun notiert man sich die https URL.

Anschließend ersetzt man die URL vom Messaging endpoint im Manifest Editor mit seiner eigenen (hier https://e80e0dba.ngrok.io /api/messages) und installiert die neue Variante bei sich persönlich.

Nun kann man mit dem Debuggen beginnen!

 

Codierung II:
Einstellungsmöglichkeiten für den Bot hinzufügen

Es gibt mehrere Möglichkeiten einen Bot wie diesen für den User konfigurierbar zu machen. Während der Anfrage sind über den TeamsContext Informationen über Channel, Teams und Tenant des Aufrufers ersichtlich.

        public InvokeResponse handleActivityInvoke(ITurnContext turnContext, CancellationToken cancellationToken = default)
        {
            ITeamsContext teamsContext = turnContext.TurnState.Get<ITeamsContext>();
            
            if (teamsContext.IsRequestMessagingExtensionQuery())
            {
                return this.getInvokeResponse(teamsContext, cancellationToken);
            }
        }

Im aktuellen Beispiel wollen wir eine neue Chatvorlage hinzufügen. Diese soll allerdings nur in einem Channel verfügbar sein. In einem weiteren Channel wird dann eine andere Vorlage zur Verfügung stehen. Die Vorlagen waren bisher hart verdrahtet. Die Benutzer selbst sollen diese Vorlagen anlegen können. Hierfür benötigen wir eine „Messaging Extension Action“  und eine Möglichkeit die Vorlagen zu speichern. Wir nehmen eine Cosmos DB.

Zunächst legen wir dafür im Manifest Editor ein neues Kommando an, das wir „Vorlage Hinzufügen“ nennen. Wir übergeben zwei Parameter, und zwar „Vorlage Überschrift“ und „Vorlage Text“.

Unser Bot sollte uns nun die Möglichkeit zum Anlegen von Vorlagen anzeigen.

Die vorher von uns festgelegten Parameter werden abgefragt und beim Absenden an unseren Bot gesendet.

Jetzt müssen wir noch dafür sorgen, dass der Bot das Hinzufügen der Vorlage auch im Code unterstützt. Hierfür müssen wir die Anfrage vom Typ „MessagingExtensionSubmitAction“ abfangen und bearbeiten. In den Parametern werden wir unsere CommandId mit dem von uns im Manifest hinterlegten Namen „Vorlage Hinzufügen“ finden.

public InvokeResponse handleActivityInvoke(ITurnContext turnContext, CancellationToken cancellationToken = default)
        {
            ITeamsContext teamsContext = turnContext.TurnState.Get<ITeamsContext>();
            
            if (teamsContext.IsRequestMessagingExtensionQuery())
            {
                return this.getInvokeResponse(teamsContext, cancellationToken);
            }

            if (teamsContext.IsRequestMessagingExtensionSubmitAction())
            {
                var parameter = teamsContext.GetMessagingExtensionActionData();

                switch (parameter.CommandId)
                {
                    case "Vorlage Hinzufügen":
                        var result = this.createNewVorlage(teamsContext, cancellationToken);
                        turnContext.SendActivityAsync("Die neue Vorlage wurde erfolgreich angelegt.");
                        return result;
                    default:
                        return new InvokeResponse() { Status = 400 };
                }

            }

            return new InvokeResponse();

        }

In die Methode createNewVorlage übergeben wir nun unseren teamsContext. Aus diesem können wir via GetMessagingExtensionActionData() die vom Benutzer übertragenen Parameter „VorlageUeberschrift“ und „VorlageText“ auslesen und verarbeiten.

In unserem Beispiel binden wir die Vorlage Tenant und Channel spezifisch mit der TenantId und ChannelId. Sollte geplant werden die App später zu veröffentlichen ist mindestens die Bindung an die TenantId zu empfehlen, da sich sonst alle dieselben Vorlagen teilen würden. Als InvokeResponse geben wir dem Ausführenden direkt die neu angelegte Vorlage zurück, damit er diese direkt sieht und benutzen kann.

        public InvokeResponse createNewVorlage(ITeamsContext teamsContext, CancellationToken cancellationToken = default)
        {
            var query = teamsContext.GetMessagingExtensionActionData();
            var data = JObject.FromObject(query.Data);
            var values = data.Values();
            var ueberschrift = values.FirstOrDefault(v => v.Path == "VorlageUeberschrift").Value<string>();
            var text = values.FirstOrDefault(v => v.Path == "VorlageText").Value<string>();

            var channelId = teamsContext.Channel != null ? teamsContext.Channel.Id : "";

            this.createKachelInCosmosDb(teamsContext.Tenant.Id, channelId, ueberschrift, text);

            var listOfResults = new List<MessagingExtensionAttachment>();
            var heroCard = new HeroCard(ueberschrift, null, text);
            var thumbnailCard = new ThumbnailCard(ueberschrift, null, "");
            var resultListAttachment = heroCard.ToAttachment().ToMessagingExtensionAttachment(thumbnailCard.ToAttachment());
            listOfResults.Add(resultListAttachment);

            return new InvokeResponse
            {
                Body = new MessagingExtensionResponse
                {
                    ComposeExtension = new MessagingExtensionResult()
                    {
                        Type = "result",
                        AttachmentLayout = "list",
                        Attachments = listOfResults,
                    },
                },
                Status = 200,
            };
        }

Damit beim Anfragen der Vorlagen auch die Vorlagen angezeigt werden, die wir nun neu hinzugefügt haben, müssen wir diese in der „getInvokeResponse“ Methode noch ergänzen.

public InvokeResponse getInvokeResponse(ITeamsContext teamsContext, CancellationToken cancellationToken = default)
        {

            var listOfResults = new List<MessagingExtensionAttachment>();

            var heroCard = new HeroCard("Homeoffice Anfang", null, "Ich bin ab sofort im dem Homeoffice erreichbar.");
            var thumbnailCard = new ThumbnailCard("Homeoffice Anfang", null, "");
            var resultListAttachment = heroCard.ToAttachment().ToMessagingExtensionAttachment(thumbnailCard.ToAttachment());
            listOfResults.Add(resultListAttachment);

            listOfResults.Add(new HeroCard("Homeoffice Anfang Pause", null, "Ich beginne nun mit meiner Pause.").ToAttachment().ToMessagingExtensionAttachment(new ThumbnailCard("Homeoffice Anfang Pause", null, "").ToAttachment()));
            listOfResults.Add(new HeroCard("Homeoffice Ende Pause", null, "Ich bin ab sofort wieder aus dem Homeoffice erreichbar.").ToAttachment().ToMessagingExtensionAttachment(new ThumbnailCard("Homeoffice Ende Pause", null, "").ToAttachment()));
            listOfResults.Add(new HeroCard("Homeoffice Ende", null, "Ich mache nun Feierabend.").ToAttachment().ToMessagingExtensionAttachment(new ThumbnailCard("Homeoffice Ende", null, "").ToAttachment()));

            var channelId = teamsContext.Channel != null ? teamsContext.Channel.Id : "";
            var vorlageFromCosmos = this.getVorlagenFromCosmos(teamsContext.Tenant.Id, channelId);

            foreach (var vorlage in vorlageFromCosmos)
            {
                listOfResults.Add(new HeroCard(vorlage.Subject, null, vorlage.Body)
                    .ToAttachment()
                    .ToMessagingExtensionAttachment(new ThumbnailCard(vorlage.Subject, null, "")
                    .ToAttachment()));
            }

            return new InvokeResponse
            {
                Body = new MessagingExtensionResponse
                {
                    ComposeExtension = new MessagingExtensionResult()
                    {
                        Type = "result",
                        AttachmentLayout = "list",
                        Attachments = listOfResults,
                    },
                },
                Status = 200,
            };
        }

Nun kann der Benutzer Vorlagen für einen Channel hinzufügen indem er in diesen Channel geht und auf „Vorlage Hinzufügen“ klickt.

Es folgen nun noch die Methoden für die Nutzung der CosmosDb. Die Verbindung haben wir über „Microsoft.EntityFrameworkCore.Cosmos“ aufgebaut und daher kann dieser Codeteil auch durch normale EFCore SQL dbs ersetzt werden. Für die Lösung relevant ist hierbei vor allem,  dass wir die TenantId und die ChannelId nutzen, um zu bestimmen welche Vorlagen dem Benutzer angeboten werden.

private void createKachelInCosmosDb(string tenantid, string channelId, string subject, string body)
        {
            using (var context = new chatVorlagenDbContext())
            {
                var newVorlage = new ChatVorlage()
                {
                    TenantId = tenantid,
                    ChannelId = channelId,
                    Subject = subject,
                    Body = body
                };
                context.Add(newVorlage);
                context.SaveChanges();
            }
        }

        private List<ChatVorlage> getVorlagenFromCosmos(string tenantid, string channelId)
        {
            using (var context = new chatVorlagenDbContext())
            {
                return context.ChatVorlagen.Where(cv => cv.TenantId == tenantid && cv.ChannelId == channelId).ToList();
            }
        }